# Ассеты, аудио, сейвы

## AssetLoader

Загрузчик поверх `Assets` из Pixi. URL решает приложение — движок получает функцию-резолвер:

```ts
import { AssetLoader } from '@rpg/engine';

const assets = new AssetLoader((key) => `assets/${key}.png`);
await assets.load(['tiles/grass', 'chars/hero_down_1'], (p) => setProgressBar(p));

assets.texture('tiles/grass'); // Texture; бросает, если не загружено
assets.has('tiles/water');
```

**Грабли**: `Assets.load` без предварительного `Assets.init({})` висит навсегда
без ошибок в консоли. AssetLoader делает `ensureInit()` сам.

### Атласы

```ts
// Spritesheet JSON должен ссылаться на PNG относительным путём — Pixi загрузит сам
const sheet = await assets.loadAtlas('chars/hero_sheet');

assets.frames('chars/hero_sheet', 'hero_walk');   // кадры hero_walk_1, hero_walk_2, ...
assets.animation('chars/hero_sheet', 'walk');     // готовая анимация из sheet.animations
```

`frames()` сортирует кадры по числу в конце имени. Пример генерации атласа —
в [art-pipeline.md](art-pipeline.md).

## FrameAnimation

```ts
import { FrameAnimation } from '@rpg/engine';

const sprite = new Sprite(frames[0]);
const anim = new FrameAnimation(sprite, frames, 8 /* fps */, true /* loop */);
anim.update(dt);          // каждый тик
anim.setFrames(otherFrames, true); // сменить кадры (поворот, состояние)
```

## AudioManager

Три шины (master/music/sfx), кроссфейд музыки, кэш сэмплов:

```ts
import { AudioManager } from '@rpg/engine';

const audio = new AudioManager((key) => `audio/${key}.mp3`);

// Расблокировать из обработчика пользовательского ввода (требование браузеров):
uiButton.onSelect = () => void audio.unlock();

await audio.load('bell');
await audio.play('bell', 0.8);                       // sfx
await audio.playMusic('meadows_theme', { fade: 2 }); // зациклится с нарастанием 2 сек
audio.stopMusic(1);                                  // затухание 1 сек
audio.setBusVolume('music', 0.6);                    // громкость шины
```

Связка с Settings (громкости применяются при изменении):

```ts
settings.onChange((s) => {
    audio.setBusVolume('master', s.master);
    audio.setBusVolume('music', s.music);
    audio.setBusVolume('sfx', s.sfx);
});
```

`play`/`playMusic` не падают, если звук не загружен или AudioContext не разблокирован —
тихо ничего не делают.

## SaveManager

JSON-слоты в Storage-подобном хранилище (localStorage в браузере, Map в тестах):

```ts
import { SaveManager } from '@rpg/engine';

const saves = new SaveManager(localStorage);
saves.save('autosave', { pos: { x: 5, y: 5 }, state: gameState.serialize() });
const data = saves.load<typeof myData>('autosave');
saves.has('autosave');
saves.delete('autosave');
```

Что класть в сейв: позицию/прогресс игры + `gameState.serialize()`.
Настройки — отдельно через `Settings` (слот `settings`), в сейв они не попадают.

### Рекомендуемая схема сейва игры

```ts
interface SaveData {
    pos: { x: number; y: number };
    state: GameStateData;      // из gameState.serialize()
    savedAt: number;           // Date.now()
}

// Загрузка:
const data = saves.load<SaveData>('autosave');
if (data) gameState.load(data.state);
```

Версионирование контента — через `GameState.dataVersion` + `addMigration`:
при изменении формата флагов старые сейвы чинятся автоматически при загрузке.