# План развития и актуальные задачи

Живой документ планирования: сюда сходятся планы на развитие, актуальные
задачи и проработка крупных направлений. Срез-статус архитектуры —
`docs/demo.md` (дорожная карта там — историческая, актуальный план здесь).

Дата ревизии: 2026-09-07.

**Статус проекта (2026-09-06): техно-демка движка.** Основные силы — движок
(`packages/engine`); игра — витрина и полигон проверки API. Приоритет решений:
что двигает движок (API, тулзы, доки, границы), то и делаем; игрового контента
ровно столько, сколько нужно демонстрации.

---

## Актуальные задачи

| # | Задача | Статус |
|---|--------|--------|
| 1 | Срез «меньше ошибок»: реестры ID, pre-commit, разрезание LocationScene, фикстуры, fail-fast waitFor | ✅ сделано (c379f20, 16b59d8, 39c6349) |
| 2 | AI-генерация текстур и спрайтов (работа B, ниже) | B0+B1 ✅ (PoC); B3 проба ✅ (SD-атласы, 6.6 мин/вид); B2 отложено — RD API или ручной арт |
| 3 | AI-генерация аудио (работа A, ниже) | 📋 спланировано, отложено |
| 4 | Акт 2: новые локации/сюжет по `docs/world.md` | очередь после акта 1+AI-ассетов |
| 5 | Замена сгенерированных плейсхолдеров арта на AI/ручной арт | в составе работы B |
| 6 | Крупные объекты: footprint в движке, слоты арта (работа C, ниже) | в работе |
| 7 | Возможности аудио-движка (работа D, ниже): sfx-хендл, полифония, наблюдаемость, буферы | ✅ D0–D2 сделаны (2026-09-07) |
| 8 | Синтез звука по запросу для агента (работа E, ниже) | ✅ E1–E4 сделаны (2026-09-07) |
| 9 | Инфраструктура аудио + музыкальный слой (работы F/G, ниже) | ✅ сделаны (2026-09-07) |
| 10 | Освещение: ambient multiply + аддитивные источники (движок L1, игра L2, контент L3) | ✅ сделано (2026-09-07); точки расширения: day/night через `setAmbient`-лерп, динамические источники `addLight/removeLight`, импульсы переосвещения — позже |
| 10b | Освещение II: импульсы (`pulseLight`/`pulseAmbient`), день/ночь, виньетка (`setVignette`, низкий hp/накат), тёмные ауры зон (`upsertDarkSpot`), гало: 4 ступени + дизер-кольцо | ✅ сделано (2026-09-07): движок L1 + игра L2 (хуки боя, `scene:damagePlayer`) + контент L3 (ночь лугов/Звенца/прудов, свет колокольчиков, окна) |
| 10c | Время суток: `GameClock` (1 с = 1 игровая минута, старт 8:00), кривая `dayNightFactor` в движке, `GameLighting` по депу `timeHours` (`lerpAmbient`-микс, `whenNight`-источники, лампа по фактору), сейв `clock` (v4), миграция state v3 (−`evening`), мост `s.time`/`scene:setTime`/инвариант `time-bounded`, сценарий `checks/time.mjs` | ✅ сделано (2026-09-07); дальше — предмет «часы» в инвентаре |
| 11 | Катсцены v2: шаг `until` (ожидание события, timeout), tween `applyEase`, шаг `moveView` (движение вьюх/актёров), `skip()` раннера, диалог и катсцена в одном тике, актёры-ходоки (`CutsceneActors`) | ✅ сделано (2026-09-07): f398d83, 7478d90, К3 |
| 12 | UI: скролл списков (`ListScroll` + `MenuList.visibleRows`, hover→курсор, value-слот кнопки), инвентарь/настройки на скролле | ✅ сделано (2026-09-07): движок + eb1596b (игра) |
| 13 | Зум камеры: `Camera.setZoom/zoomTo`, шаг `cameraZoom`, интеграция в игре (клики/culling/свет/мост), debug-клавиша Z, чек `zoom.mjs` | ✅ сделано (2026-09-07): 878355c, 8730614 |
| 14 | Акт 2 (контент): карты Ржавая роща/Старый разъезд, Клинт + размещения NPC по флагу, квест резонатора, Старая Машина и финальная катсцена | ✅ сделано (2026-09-08): A1 fa38ee7 (карты/переходы); A2 58a8164 (Клинт, `NPC_PLACEMENTS`); A3 6068646 (квест резонатора, чек `resonator.mjs`); A4 ccc3ab3 (правильный звон, чек `final.mjs`, docs) |
| 15 | Уход от кликов: фикс радиуса взаимодействия (евклидова линейка вместо проекционной), ходьба WASD/стик/джойстик, взаимодействие кнопкой действия E/A в радиусе, подпись `[E]`, удаление клик-роутинга (`InteractRouting.ts`), мост `scene:walk`/`scene:approach` + переписывание чеков и смоуков | ✅ сделано (2026-09-08): C1 fde88b6, C2 50cfc15, C3, C4; тач-кнопки действий — позже (джойстик даёт только движение) |

Отложено: **IsoLayout как параметр игры** — `DEFAULT_ISO` в `math/iso.ts` остаётся
единственной точкой входа; прокидывание конфига игры через ~15 мест движка не даёт
пользы демо и добавляет второй способ рассинхрона изометрии.

Приоритет между работами A и B: **B (изображения) выше** — визуальный стиль
блокирует восприятие акта 2; аудио сейчас закрывается процедурным генератором
(`tools/audio/gen.mjs`) приемлемо.

---

## Ограничения (читать первым)

- **Машина разработки: Intel HD 620 (iGPU, нет CUDA), 15 ГБ RAM, 4 ядра CPU.**
  Только CPU-инференс (подтверждено пробой ускорителей, см. B3). Модели класса
  SDXL/FLUX/SANA/DreamLite отпадают либо как «медленный офлайн-пакет» (десятки
  минут на картинку). Целевой класс — компактные модели (< 1.5B параметров),
  CPU-рантаймы; главный бесплатный рычаг скорости — меньше шагов диффузии,
  а не квантизация и не iGPU.
- **Стиль игры жёсткий**: палитра 32 цветов (`tools/pixelart/palette.mjs`),
  размеры спрайтов по `docs/art-style.md`, pixel-perfect. Любая AI-генерация —
  только как **сырьё** с обязательной пост-обработкой: даунскейл nearest,
  квантизация в палитру, проверка чек-листом арт-библии. AI-вывод не попадает
  в `assets/` напрямую.
- **Формат аудио фиксирован**: моно 22050 Гц, 16-бит WAV
  (`tools/audio/gen.mjs`). AI-вывод приводится к нему (ffmpeg: sample rate,
  моно, нормализация, обрезка тишины).
- **Лицензии весов**: для игры важен коммерчески-допустимый статус. Отмечено в
  таблицах; некоммерческие веса (CC BY-NC) — только для экспериментов/PoC.
- **Детерминизм**: у всего пайплайна фиксированный seed в конфиге генерации —
  регенерация не должна молча менять ассеты (аналогично `npm run maps:fresh`).

---

## Работа B — AI-генерация текстур и спрайтов

### Классы потребностей

1. **Тайлы** (трава, пепел, вода…) — процедурный генератор уже даёт приемлемый
   результат; AI ценен для сложных поверхностей (бутеи, руины, интерьеры).
2. **Спрайты объектов** (предметы, декор, интерактивы) — главная точка роста:
   сейчас Graphics-плейсхолдеры.
3. **Спрайты персонажей + атласы** (герой, NPC, враги, кадры анимаций) — самое
   сложное: консистентность кадров, фиксированные размеры листа
   (`hero_sheet.png` — контракт арт-библии).

### Кандидаты

| Модель | Размер | Железо | Нативная палитра | Лицензия | Комментарий |
|--------|--------|--------|------------------|----------|-------------|
| **PXG-Tiny** (HF: Tarul/pxg-tiny) | 483K | CPU, NumPy, 1.8 МБ | да (прямые индексы) | MIT + CC0 | 16×16, 38 классов. Идеален по железу, но узкий домен |
| **PixelGPT-24×24** (github.com/unstonio/pixelgpt-24x24) | 68M | CPU, ~22 с | да, 5 цветов/спрайт, кастомная палитра на запрос | **нет (не указана)** | 24×24; отказов нет, домен тянет частично (см. B1). Веса в репо; датасет 20K — CC BY 4.0 |
| **Pixel Forge** (cochranblock) | 1.1–17M | CPU (Rust) | да, 7 встроенных палитр | Unlicense | 108 классов объектов; появилась фича 4-directional sprite sheet relight — кандидат пробы B3 (нужен Rust-тулчейн) |
| **SD_PixelArt_SpriteSheet_Generator** (Onodofthenorth) | ~1B (SD 1.5) | CPU 6.6 мин/вид fp32 (25 шагов — 11 мин; q8 отвергнуто: медленнее и шумит) | нет (пост-обработка) | Apache-2.0 | 4 ракурса по токенам (FSS/RSS/BSS/LSS); кадры — позы стоя, не walk-цикл. Проба B3 ✅ — рабочий путь для разовых атласов |
| **Retro Diffusion SD1.5** (LostMedia, архив) | ~1B | CPU минуты/картинку | нет (пост-обработка) | Public Domain | классика пиксель-диффузии; для офлайн-пакета |
| **Retro Diffusion API (RD Plus/Tile/Animation)** | облако | — | да | платный API | лучший результат для тайлов/анимаций; фолбэк, если локальное не тянет |
| **LPC-style (PIXEL-T2I и др.)** | research | GPU | нет | research | перспективное направление для атласов 4 ракурса × walk/slash |

Вывод по классам (после B0+B1): инвентарь/декор — **PXG-Tiny локально** (её
классы); простые объекты 24×24 — PixelGPT (но лицензии нет — только PoC);
критичные интерактивы и тайлы сложных поверхностей — **RD API (платный) или
ручной арт**; дообучение под игру не делаем. Персонажи-атласы — кандидаты
пробы: SD_PixelArt_SpriteSheet_Generator (Apache-2.0, CPU-минуты) и Pixel
Forge relight (нужен Rust); фолбэк — платный RD API или ручной арт. Отдельный
путь для полноразмерных атласов с боевыми кадрами — LoRA к Qwen-Image-Edit
(Apache-2.0), но база ~20B: только разовая генерация на Kaggle/Colab GPU,
результат — в наш пайплайн пост-обработки.

### Пайплайн интеграции (проект решения)

```
tools/aiart/gen.mjs        # вызов модели (seed, промпт из манифеста)
  → tools/aiart/post.mjs   # даунскейл nearest → квантизация palette.mjs → дедуп
  → tools/aiart/manifest.json  # список ассетов: промпт, seed, размер, статус
  → apps/game/assets/…     # только после ручного апрува (review-каталог)
```

Принципы: манифест-драйвен (как `maps/`); апрув-гейт (человек смотрит обзорный
лист перед попаданием в игру); ревизия в git; проверка стиля — чек-лист
`docs/art-style.md` + скриншот-смоук.

### Инструменты, которых не хватает (по итогам B0–B3)

1. **`sheet2atlas` — сборщик атласа из AI-листов** (главная дыра). SD-лист 512²
   → кроп фигур (bbox, отсев фона) → нормализация в слоты 16×24/32×48 →
   раскладка в сетку `hero_sheet.png` + `hero_sheet.json` (контракт арт-библии).
   ✅ **Сделан (2026-09-06)**: `tools/aiart/atlas.mjs` (gen/build/promote) +
   `atlas-manifest.json` + `sd_sheet.py`; движковая база — `findFigures`/
   `resizeRect`/`buildAtlas` в `@rpg/engine/tools`. Палитра — параметр манифеста
   (`game` или `palettes/<name>.json` на 64/128/256 цветов). Фон листов модели
   неоднородный + сплошная «земля» под фигурами — автодетект фигур не срабатывает,
   в манифесте работают crop-оверрайды (раскладка 4 фигуры в ряд стабильна).
2. **Палитровый ремапер (recolor)**: перекраска готового квантованного спрайта
   маппингом цветов палитры. Две потребности: сгладить «плавающие» промпт-цвета
   SD (приводить плащ к каноническому) и дешёвые цветовые вариации NPC из одного
   базового спрайта. ✅ **Сделан (2026-09-06)**: `tools/aiart/recolor.mjs` (CLI
   по ключам палитры/hex) + поле `remap` в манифесте атласов — применяется в
   build после квантизации (плащ звонаря приведён к C0/C1).
3. **Линтер арта (авто-чеклист арт-библии)**: скрипт-гейт перед попаданием в
   `assets/` — размер в слоте, все цвета из палитры, детект «мыла» (градиентные
   пары соседних пикселей — чужая плотность пикселя). ✅ **Сделан
   (2026-09-06)**: `tools/aiart/lint-art.mjs` (`npm run art:lint` по всем
   assets, негативы проверены); в `agent:check` не заведён — по мере надобности.
4. **Отложено**: клиент RD API (tile/animation) — только если решимся платить
   (B2, кадры walk-цикла); генерация/склейка walk-цикла из поз — пока ручной
   арт, инструмента не видно.

### Этапы

- **B0 (PoC, ~1 вечер)**: запустить PXG-Tiny/Pixel Forge локально на CPU;
  сгенерировать 10–20 объектов, прогнать пост-обработку, сравнить с текущими
  плейсхолдерами. Критерий: «хотя бы 30% годных без ручной правки».
  ✅ **Выполнен (2026-09-06)**: PXG-Tiny на CPU (~6 с/спрайт), 20 спрайтов,
  40% годных, стиль совпал с игрой на 100%. Пайплайн: `tools/aiart/`
  (`post.mjs` — кроп/квантизация в палитру + обзорный лист; `manifest.json`
  — вердикты). Pixel Forge отложен (нет Rust-тулчейна). Ограничение:
  критичные для игры ассеты (колодец, столб, рычаг, колокольчик, моты)
  вне 38 классов модели — для них дообучение / RD API / ручной арт.
- **B1 (домен, вторая модель)**: PixelGPT-24×24 на доменных интерактивах
  (промпты + обусловливание палитрой игры из 5 цветов).
  ✅ **Выполнен (2026-09-06)**: 18 спрайтов, 44% годных, **отказов нет** —
  доменные объекты (колокол, дверь, камин, столб, моты) модель рисует, где
  PXG-Tiny отказывала. Ограничения: даунскейл 24→16 губит деталь (колодец
  good на 24×24), фон часто залит, ровно 5 цветов, ~22 с/спрайт CPU,
  **лицензии на код/веса нет — в игру нельзя**. Манифест: `manifest-b1.json`.
- **B2**: тайлы сложных поверхностей (img-условность от существующих тайлов) —
  локальным моделям недоступно, кандидат RD API; отложено.
- **B3 (проба, 2026-09-06)**: SD_PixelArt_SpriteSheet_Generator на CPU
  (venv-pxgpt, fp32). Токен-ракурс работает: 4 фигуры читаются; но кадры —
  вариации позы стоя (не walk-цикл), промпт-цвета плавают, кадры ~120 px
  (даунскейл до 16×24 некратный). Тайминги: 25 шагов — 11 мин, **15 шагов —
  6.6 мин** (взято за рабочий режим). Квантование q8_0 (stable-diffusion.cpp,
  GGUF) отвергнуто: 34 мин (ggml-ядра int8 без VNNI медленнее fp32-oneDNN) и
  VAE в q8 даёт шум — `probe-b3/sd_q8_fss_42_broken.png`. Вердикт: путь
  рабочий для разовых атласов (4 вида ≈ 30 мин/персонажа), но кадры-позы
  придётся дорабатывать руками; полный прогон — при потребности акта 2.
  Пробы: `apps/game/tools/aiart/probe-b3/`. Фолбэк — RD API или ручной арт.
- **B3-доп (ускорители, 2026-09-06)**: альтернативные пути разгона проверены и
  **отвергнуты**, остаёмся на fp32-CPU (diffusers, 4 потока):
  - **iGPU HD 620 (Vulkan/ggml)** — не готовился к прогону осознанно: стек
    Vulkan в системе есть (Mesa ANV Gen9.5, glslc), но HD 620 — 24 EU и общая
    с CPU память ~30 ГБ/с, т.е. на порядок слабее CPU в матмулах; ggml-Vulkan
    на Gen9.5 обычно даёт 10–25 мин на SD1.5 512²×20 — сравнимо или хуже.
    OpenCL мёртв: intel-compute-runtime 26.x — только Tiger Lake+ (Gen9.5 —
    legacy-ветки). Оставлено как есть, без бенчмарка.
  - **Квантование q8_0/q4** — отвергнуто ранее: на 7-м поколении без VNNI
    ggml-ядра int8 медленнее fp32-oneDNN (34 мин против 11), VAE в q8 шумит.
  - **Меньше шагов** — единственный рабочий рычаг: 25→15 шагов даёт 11→6.6 мин
    без видимой деградации кадра; ниже 15 качество токен-ракурса падает.

---

## Работа C — крупные объекты (footprint + большие спрайты)

Акт 2 захочет здания и крупные деревья — объекты больше 1×1 тайла. Сегодня
движок умеет только `TallSpec` (1 тайл footprint + произвольная высота) и
ручной `blocked`. Большой спрайт, якоренный в один тайл, сортируется глубиной
по точке — герой сбоку от дома перекрывается неверно.

### Что нужно

1. **Движок: footprint w×h** — коллизия по всему прямоугольнику, земля под ним,
   спрайт якорем низ-центр footprint'а; глубина по прямоугольнику
   (`(x+w-1)+(y+h-1)`, `IsoDepthLayer.addRect`), а не по точке якоря.
2. **Арт-библия: слоты крупных спрайтов** — фиксированные размеры с якорями
   (дом 3×3, большое здание 4×4…), чтобы любой источник рисовал в готовый слот.
3. **Арт: три пути** — крупный объект кодом в `gen.mjs` (композит: модули стен +
   крыша — наш бесплатный путь); апскейл nearest из 24×24 (только болванка —
   чужая плотность пикселя рядом со спрайтами 1×); RD API (умеет большие
   размеры и тайлы, платный).

### Этапы

- **C0 (движок) ✅**: `props` в `TileMapData` (footprint, коллизия по всему
  прямоугольнику, земля под ним), `IsoDepthLayer.addRect` (глубина по
  диагонали юго-восточного угла footprint'а), валидация в `mapFormat`,
  тесты; `LocationScene` вставляет `map.propViews` в общий depth-слой.
- **C1 (арт-библия) ✅**: слоты крупных спрайтов в `docs/art-style.md`
  (дом 2×2 — 64×80, дом 3×3 — 96×96, здание 4×4 — 128×112; якорь ставит
  движок). Плотность пикселя — как у тайлов; AI-апскейл в слоты не попадает.
- **C2 (проба) ✅**: большой дом 2×2 кодом в `gen.mjs`
  (`tiles/house_big.png`, 64×80, композит: модули щитов + крыша шпал +
  козырёк + окна) против апскейл-болванок 24→48 из B1
  (`apps/game/tools/aiart/probe-c2.png`). Вывод: **для крупных объектов —
  код-композит в `gen.mjs`**; апскейл даёт мыло с чужой плотностью пикселя и
  залитым фоном (только черновая болванка), нативно больших размеров локальные
  модели не умеют. RD API остаётся опцией, если код-композита не хватит.
  Неквадратные footprint'ы (2×1, 3×1, 4×2): движок умеет (`props` w×h;
  поверхности заполняются обычными тайлами 1×1); AI-генерация — только квадраты
  (неквадрат собираем из 1×1 на уровне карты), код-композит — любой размер,
  RD API Tile — нативно прямоугольные.

---

## Работа A — AI-генерация аудио

### Классы потребностей (по пользовательскому сценарию)

| Класс | Что это сейчас | AI-кандидат | Железо |
|-------|----------------|-------------|--------|
| **Окружение** (ветер, пепел, вода прудов) | процедурный шум в gen.mjs | **Stable Audio 3 Small-SFX** (433M, CPU, 120 с) — длинные лупы; луповальность через crossfade-склейку | CPU ок |
| **SFX взаимодействия** (шаги, клик, удар, звон) | gen.mjs, приемлемо | **Stable Audio 3 Small-SFX** (тот же рантайм; seed-детерминизм для вариантов) | CPU ок (~секунды-десятки секунд на клип на 4 ядрах) |
| **Музыка: фоновые лупы** | gen.mjs (треки 3 областей) | **Stable Audio 3 Small-Music** (433M, CPU, 120 с) — короткие лупы; либо **ACE-Step 1.5** 2B turbo INT8 (CPU-медленно: минуты-десятки минут на трек, но офлайн-пакет) | Small-Music — да; ACE-Step — терпимо для пакета |
| **Музыка: катсцены/сюжетные моменты** | нет (трек + звон) | **ACE-Step 1.5** — полноценные треки с настроением/структурой, стемы; MusicGen small — фолбэк для мелодических заготовок | офлайн-пакет, не hot-reload |

### Кандидаты (сводно)

| Модель | Размер | VRAM/железо | Длина | Лицензия | Роль у нас |
|--------|--------|-------------|-------|----------|------------|
| **Stable Audio 3 Small-SFX / Small-Music** | 433M | CPU (TFLite), ~1.9 ГБ пик | 120 с | веса: Stability Community License; код MIT | основной локальный рантайм для SFX + лупов |
| **ACE-Step 1.5** | 2B (turbo INT8) | CPU (медленно), GPU <4 ГБ | 10 с–10 мин, стемы | MIT | музыка катсцен/сюжета, офлайн-пакет |
| MusicGen small (AudioCraft) | 300M | CPU ~4 ГБ RAM | ~30 с | MIT/CC-BY-NC веса | фолбэк, мелодия-условность |
| Stable Audio Open 1.0 | 1.2B | GPU ~2.5–8 ГБ | 47 с | Stability Community | зрелый, но на нашей машине безнадёжно медленный; пропускаем |

### Пайплайн интеграции (проект решения)

```
tools/aiaudio/gen.mjs       # вызов модели, seed, промпт из манифеста (аналог aiart)
  → post: ffmpeg → 22050 моно 16-bit → нормализация → crossfade-луп для ambience/sfx
  → review-каталог → apps/game/assets/audio/{sfx,ambience}/*.wav
```

Принципы те же: манифест, апрув-гейт, seed-детерминизм, формат игры не меняется
(шины master/music/sfx/ambience в движке уже готовы). Лупы проверять скриптом
(стык начала/конца без щелчка).

### Этапы

- **A0 (PoC)**: Stable Audio 3 Small-SFX локально на CPU; сгенерировать 5
  SFX-замен (step, ui_click, bell_hit) + 1 амбиент-луп прудов. Критерий: звук
  лучше процедурного на слух и формат сходится.
- **A1**: пайплайн aiaudio + манифест; замена SFX, амбиент-лупы областей.
- **A2**: музыкальные лупы (Small-Music), сравнение с gen.mjs-треками.
- **A3**: ACE-Step офлайн-пакет для катсцен акта 2 (1–2 трека под моменты).

---

## Работа D — возможности аудио-движка

Итог аудио-работ (2026-09, коммиты 57bce47…4c1c56c): в движке — 4 шины,
кроссфейд слотов music/ambience, `playLoop` без слота, pan/rate, шпион
`onPlayed`; в игре — позиционный `AudioSystem`, локальные амбиент-слои,
событийные звуки боя, DEV-лог `__gameAudioLog`. Ниже — потребности, которые
упёрлись в границы `AudioManager`, по убыванию полезности. Игровая логика
(кто слушатель, затухание по дистанции, `playAt`) остаётся в игре — в движок
её не тащить.

### Потребности

1. **Управление играющим разовым звуком.** `play()` возвращает `void`:
   нельзя остановить, перезапустить «с нуля» (опция «убить прежний экземпляр
   ключа») или изменить volume/pan/rate в полёте. Самый чувствительный кейс —
   движущийся источник: панорама фиксируется в момент запуска.
2. **Лимит полифонии в движке.** Число одновременных источников ничем не
   ограничено — игру спасает только `MIN_INTERVAL` на ключ в игровом
   `AudioSystem`. Нужен `maxVoices` с воровством самых тихих/старых голосов —
   защита от каскада (десяток `combat:*` в один тик).
3. **Шпион на все запуски + окончание.** `onPlayed` зовётся только для sfx —
   лупы (музыка/амбиент/слои) шпион не видит, игре пришлось логировать
   амбиент вручную в `Game.playAmbience`. Нет `onEnded` — нечем ловить «звук
   доиграл» для синхронизации с анимацией.
4. **Длительность буфера наружу.** `duration(key)` — сейчас её не достать,
   а нужна катсценам и генератору шагов (не играть следующий шаг раньше конца
   предыдущего).
5. **Играть буфер из памяти.** `playBuffer(AudioBuffer)` — синтез сейчас
   только оффлайн (`tools/wav.mjs`); генерировать вариации звука в рантайме
   движок не может (сейчас джиттер имитируется `rate`).

### Этапы

- ✅ **D0 (движок): sfx-хендл + полифония.** `SfxHandle { stop(fadeSeconds?),
  setVolume(v), setPan(v), setRate(v) }` — по образцу `MusicHandle`; опция
  `restart`/«один экземпляр на ключ»; `maxVoices` с воровством голосов.
- ✅ **D1 (движок): наблюдаемость.** `onPlayed` на все запуски (sfx + лупы,
  опции нормализованы); `onEnded(key)`; `duration(key)`. Игра: ручное
  логирование амбиента убрано (лог целиком из шпиона).
- ✅ **D2 (движок + игра): буферы из памяти.** `playBuffer(key, buffer, opts)`
  + `createBuffer(data)`; примитивы синтеза вынесены в чистый
  `@rpg/engine/tools/synth.mjs` (без node-импортов — wav.mjs в браузере не
  соберётся); игра — `systems/StepVariants.ts`: 3 процедурных варианта шага
  на поверхность (сид ~253–256 + сдвиг, полосы как в gen.mjs), до unlock —
  файловый шаг. Гвард: `@rpg/engine/tools/*` — публичное API.

### Работа E — синтез звука по запросу (агент без слуха)

Движок строится агент-первым: у агента нет слуха, и искать готовые сэмплы
в библиотеках он не может. «Генерация по запросу» для агента — параметрический
синтез: агент описывает звук **спеком** (форма, полоса шума, тон, огибающая,
сид), движок детерминированно компилирует спек в буфер (база — D2:
`createBuffer`/`playBuffer` + `tools/synth.mjs`), факт запуска наблюдаем через
`__gameAudioLog`. Нейросеть позже — опциональный «описание → спек» маппер
поверх того же API; сэмпл-библиотеки как основной путь не используются.

- **E1 (движок) ✅ (2026-09-07): компилятор спеков.** `audio.playSpec(key, spec, opts?)` —
  kinds `hit`/`chime`/`scrape`/`hum`, параметры: `dur` (обяз.), `low`/`high`
  (полоса шума), `tone` (низкая подложка/основной тон гула), `freq` (звон),
  `power` (крутизна затухания), `seed` (детерминизм), `peak`, `loop`
  (кроссфейд краёв). Кэш буферов по ключу. Тесты: детерминизм (одинаковые
  спеки/сиды → одинаковые сэмплы), kinds, кэш.
- **E2 (мост) ✅ (2026-09-07): агентный доступ.** Op `scene:synthesize` (спек →
  playSpec), документация форм спеков в `docs/engine/agent.md`; проба
  `checks/synth.mjs` (в `agent:check`) — агент слепо синтезирует удар/звон/
  скрип/гул, проверка фактов запуска в аудио-логе + отрицательные случаи.
- **E4 (палитра спеков + полная миграция) ✅ (2026-09-07):** новый kind
  `tone` (чистый тон, свип `freqTo`) и общие параметры `w`/`attack`/`fadeTo`/
  `wobbleHz|Depth|Base|Phase` (тремоло) / `layers` (доп. голоса `SpecVoice` с
  `delay`). **Все файловые SFX переведены на спеки** (16 ключей в
  `data/sfxSpecs.ts`, включая шаги — `StepVariants` рендерит варианты
  `renderSpec` с сидами base+i·10); `tools/audio/gen.mjs` остался только
  амбиенту, каталог `assets/audio/sfx/` удалён. Парити-тест `sfxSpecs.test.ts`
  сверяет каждый спек с формулой бывшего генератора (bit-в-бит или eps 1e-5) —
  звучание игры не изменилось. Ауди-тулза игры больше не нужна для новых
  sfx: ключ + спек в `SPEC_SFX` + `playSfx` на play-сайте.
- **E3 (контент) ✅ (2026-09-07): миграция файловых SFX на спеки.**
  `sfx/bell_hit` и `sfx/whoosh` — теперь спек-синтез в `data/sfxSpecs.ts`
  (реестр `SPEC_SFX` + хелпер `playSfx`, единая точка проигрывания sfx);
  мёртвый `sfx/door.wav` (ключ без play-сайтов) удалён. Для свуша у scrape
  добавлен флаг `swell` (огибающая «0 → 1 → 0»), у chime — 4-й партиал и
  константная амплитуда клика: формулы совпали с gen.mjs дословно, парити-тест
  `sfxSpecs.test.ts` сверяет `renderSpec` с формулами генератора бит-в-бит
  (генератор формулы экспортирует, WAV при импорте не пишет — CLI-гвард).
  Остальные файловые SFX не выражаются текущими kinds (тон/тремоло/свип) —
  по мере расширения палитры спеков мигрируются так же.

### Работа F — инфраструктура аудио (хвосты D)

✅ **Сделана (2026-09-07)**: mute/suspend при `visibilitychange` (опция
`pauseOnHide`, по умолчанию включена); `bus` в `PlayOptions` разового `play`
(по умолчанию sfx); exponential-фейд на затуханиях (нарастание — линейное:
экспонента не определена от нуля).

### Работа G — музыкальный слой

- Движок: секвенсор (ноты → инструменты поверх `synth.mjs`: осциллятор +
  огибающая, партия = трек = стем), плейлист/секвенция тем («тема лугов →
  тема боя → назад»), стем-миксинг — параллельные лупы-слои с независимой
  громкостью (спокойно/настороженно/бой; ложится на существующий `playLoop`).
- Тулзы: правило-базовый генератор тем офлайн (детерминированный, сиды), тема
  лугов — первый носитель. Позже — маленькая нейронка «описание → спек/ноты»
  как опция, не как база.

✅ **Частично сделано (2026-09-07)**: секвенсор движка (`renderMusic`, чистый,
инструменты pluck/bell/pad) + `playLoopBuffer` (луп из памяти на любой шине);
правило-базовый генератор темы лугов в `data/music.ts` (сид, пентатоника),
компиляция в рантайме — WAV для музыки не нужен; тема области — поле
`theme` + `Game.playTheme` (guard + кроссфейд), проба `checks/music.mjs`.
Стем-миксинг уже покрыт (`playLoop` + тик-фейд, амбиент-слои).
✅ **Динамическая музыка (2026-09-07)**: стерео-рендер `renderMusicStereo`
(interleaved L,R, `pan` треков equal-power, нормализация по общему пику
каналов — `createBuffer(data, RATE, 2)`); тема боя + tension-стем в
`data/music.ts`; threat-тик в `LocationScene` (состояния врагов: chase →
тема боя, wary → тихий стем под темой области, деаггро → тема назад);
room tone интерьеров (`ROOM_TONES` — hum-спеки, шина ambience).
**Осталось**: нейронка «описание → ноты» — опционально.

---

## Открытые решения (принять перед стартом работ)

1. ~~B0: какая из GPT-пиксель-моделей заходит лучше~~ — **закрыто B0+B1**:
   PXG-Tiny — инвентарь/декор, PixelGPT-24×24 — домен частично, но лицензии нет.
2. ~~Персонажи-атласы: дообучение локально vs RD API vs ручной арт~~ —
   дообучение под игру **отвергнуто**; осталось: платный RD API vs ручной арт.
3. ~~Python-окружение~~ — **закрыто**: venv вне репо (`~/.cache/rpg-ai/venv`,
   `venv-pxgpt`), веса в `~/.cache/rpg-ai/`; в git — манифесты и review-PNG.
4. Порог апрува: кто смотрит review-лист (пользователь) и где фиксируется
   вердикт (пометка в манифесте).

## Что НЕ делаем

- Облачные API в основном пайплайне (только как опциональный фолбэк для спрайтов).
- Модели с некоммерческими весами в основном пайплайне (DreamLite CC BY-NC —
  только эксперименты); веса без лицензии (PixelGPT) — то же.
- Дообучение моделей под конкретную игру.
- TTS-голоса персонажей — вне среза, кандидат на будущее: Kokoro-82M (CPU).