# Ядро: Engine, GameLoop, EventBus, Tween, GameState, StateMachine, Settings

## Engine

Собирает все подсистемы и владеет игровым циклом:

```ts
const engine = new Engine({ virtualWidth: 480, virtualHeight: 270, scale: 3, background: 0x0b0b12, fixedFps: 60, parent });
await engine.init();   // WebGL, ввод, оверлей fade-переходов
await engine.start();  // запуск цикла
```

Доступные подсистемы: `engine.renderer`, `engine.camera`, `engine.scenes`,
`engine.input`, `engine.tweens`, `engine.events`.

Опции: `autoResize` (по умолчанию true) — пересчитывать целочисленный масштаб
при ресайзе окна.

## GameLoop (fixed timestep)

Логика — фиксированный шаг 60 Гц; рендер после каждого шага. `dt` в `Scene.update(dt)`
всегда `1/60`. После лага («вкладка спала») выполняется максимум 5 шагов за кадр —
игра замедляется, а не «телепортируется».

Вручную цикл обычно не трогают; Engine.tick делает по порядку:
`input.update()` (опрос геймпада) → `tweens.update(dt)` → `scenes.update(dt)` → `input.endTick()`.

## EventBus

Типизированные по темам события — связь систем без прямых зависимостей:

```ts
engine.events.on<{ gold: number }>('loot:picked', ({ gold }) => console.log(gold));
const off = engine.events.on('quest:done', () => { ... });
engine.events.emit('loot:picked', { gold: 5 });
off(); // отписка
```

## Tween (твины и таймеры)

Обновляется циклом автоматически. Все методы возвращают handle с `cancel()`.

```ts
engine.tweens.to(sprite, { x: 100, alpha: 1 }, {
    duration: 0.5,
    ease: cubicOut,      // linear, quadIn/Out, cubicIn/Out, sineInOut
    delay: 0.2,          // стартовые значения снимаются ПОСЛЕ delay
    onDone: () => console.log('готово')
});

engine.tweens.delay(1.5, () => spawnEnemy()); // таймер, сек
engine.tweens.cancelFor(sprite);              // отменить всё для объекта
```

Гарантии: значения не выходят за цель (зажим на последнем кадре), `onDone` вызывается
один раз, тик, пересекающий конец `delay`, не «съедает» часть движения.

## GameState

Состояние прохождения: флаги и именованные переменные. Жанронезависимо — какие флаги
есть, решает игра. Сериализуется в сейв (`GameStateData`).

```ts
const state = new GameState();
state.setFlag('met_elder');
state.setVar('gold', 30);
state.getNumber('gold');      // 30 (getString/getBool — аналогично)
state.hasFlag('met_elder');   // true

// Сейв/загрузка + миграции при изменении формата:
state.dataVersion = 2;
state.addMigration((d) => ({ ...d, version: 2, vars: { ...d.vars, gold: 0 } }));
const data = state.serialize();       // положить в сейв
state.load(data);                     // миграции применятся автоматически
```

## StateMachine

Конечный автомат для состояний сущностей (idle/walk/attack), AI и т.п.:

```ts
const sm = new StateMachine((state, event) => console.warn(`${state} не знает событие ${event}`));
sm.add('idle', { enter, exit, update: (dt) => {...} });
sm.add('chase', { enter, exit, update });
sm.transition('idle', 'seePlayer', 'chase');
sm.transition('chase', 'losePlayer', 'idle');

sm.change('idle');            // exit старого -> enter нового
sm.handleEvent('seePlayer');  // переход по таблице
sm.update(dt);                // тикает текущее состояние; sm.time — время в состоянии
```

Переход «из любого состояния» — для событий вроде hurt/die, чтобы не регистрировать
их из каждого состояния (обычные переходы имеют приоритет):

```ts
sm.transitionAny('die', 'dead');
```

## Cooldown

Кулдаун на фиксированном шаге (секунды, без `performance.now()`):

```ts
const cd = new Cooldown(0.45);
cd.trigger();          // запустить (false, если ещё не готов)
cd.triggerForce();     // перезапустить принудительно
cd.update(dt);         // тикать каждый шаг
cd.ready;              // готов?
cd.timeRemaining;      // секунд до готовности
cd.progress;           // 1..0 — удобно для полоски перезарядки
```

## Settings

Настройки игрока (громкости, язык, прочее) в отдельном слоте хранилища — не попадают
в игровые сейвы:

```ts
const settings = new Settings(localStorage, 'settings'); // или любой StorageLike
settings.load();

settings.update({ master: 0.8, music: 0.6 });
settings.data.master;           // 0.8
settings.update({ extra: { hintSeen: true } });
settings.getExtra('hintSeen');

const off = settings.onChange((s) => audio.setBusVolume('master', s.master));
```

Хранилище может быть недоступно (приватный режим) — Settings работает в памяти,
не падает.