Newer
Older
rpg / docs / plan.md

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

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

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

Статус проекта (2026-09-06): техно-демка движка. Основные силы — движок (v1/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 мин/вид); B4 ✅ (2026-09-09, RTX 3090: интерактивы/снаряд/иконки в игре, ~1.3 с/вид); B2 отложено — RD API или ручной арт
3 AI-генерация аудио (работа A, ниже) A0 ✅ (2026-09-09, AudioLDM2 на GPU, апрув пользователя; в игру — ponds, ponds_night, zvenets, tower_hum); дальше A1
4 Акт 2: новые локации/сюжет по docs/world.md очередь после акта 1+AI-ассетов
5 Замена сгенерированных плейсхолдеров арта на AI/ручной арт ✅ в основном сделано (2026-09-09, B4): интерактивы, снаряд плевка и иконки сумки — арт в игре; плейсхолдерный Graphics остался только у редких разовых объектов, догонять по мере надобности
6 Крупные объекты: footprint в движке, слоты арта (работа C, ниже) ✅ сделано (2026-09-09): C0–C2 см. работу C; додержано в игре — большой дом 2×2 в Звенце (первый проп на карте), минимапа красит footprint пропа (renderMinimap), чек checks/big_house.mjs (коллизия, approach, глубина севера/юга)
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; тач-кнопки действий — позже (джойстик даёт только движение)
16 Переделка по фидбеку после игры: свет заново («тьма с прорезями»), ветер лугов, квестовая ясность (HUD-трекер, тосты, направления, журнал), баги UI (флип side-анимации, подложка диалога, переносы шрифтов) ✅ сделано (2026-09-08): L1 920d0b0 (движок: RT-карта силы тьмы, erase-прорези, dirty-bake, day fast-path), L2 93491b7 (солнечный день, duskColor золотого часа, тюнинг радиусов/арт), L3 64e5ce8 (ветер: воздушная полоса 250–1800 Гц вместо «прибоя»), L4 64efcf5 (questTracker в HUD, тост «Тон (N/3)», направления в квестах/диалогах, журнал активных задач, фикс вталкивания врагов в стены), L5 8490bf9 (флип side-анимации, панель диалога сразу полной высоты, wordWrap имён/кнопок/тостов)
17 Второй раунд фидбека: баг мотов/сейвы/маркер NPC, свет +25% и фонарь, замах+след боя, деревья (варианты+качание+пепел), фасады домов, пепельная трава и читаемые зоны наката, амбиент день/ночь, миникарта (движок + предмет + оверлей) ✅ сделано (2026-09-08): B1 (моты/автосейв на переходах и закрытии вкладки/маркер), B2 (лампа 2.0/1.0, спрайт фонаря, центр внизу), B3 (сектор замаха + дуга-след, conePath в движке), B4 7f14fb0 (3 вида крон × 2 кадра качания, осыпание пепла), B5 7ae42cf (3 фасада домов, явная расстановка в Звенце), B6 1a906e0 (трава сквозь пепел, плотнее летающий пепел, тёплый DARK_SPOT_TINT), B7 19ce175 (ночные лупы областей + ashflow, кроссфейд по dayNightFactor, positions-слои зон), B8 ef8d9b7+8516872 (renderMinimap в движке, предмет map_scroll у Ирвина, оверлей MapScene)
18 Третий раунд фидбека: свет в центре персонажа, пепел с заметной травой, изометрические объёмные дома (5 видов), валун и куст, карты внешних локаций ×2, карта мира «Низина Звенца» (композит областей), амбиент лугов заново (тихий «дыхательный» ветер + события) ✅ сделано (2026-09-08): C1 25d2875 (свет лампы в центре тела героя), C2 6c3e23b (стебли травы в пепле заметнее, ярче, пучками), C3 9883a24 (изо-дома: две стены + двускатная крыша, 5 видов), C4 a8622f5 (валун BOULDER и куст BUSH — новые тайлы), C5 1dada11 (все внешние карты ×2, пересчёт координат контента/тулз), C6 b86b458 (карта мира — композит миниатюр областей с дорогами и подписями), C7 0dacab3 (новая фактура ветра: мягкий низкий шум с порывами, шуршание травы, скрипы и далёкий звон, тише)
19 Герой за tall-объектами: деревья/дома-тайлы/башня/валуны/кусты рисуются в objectsLayer карты — целиком под слоем actors, герой всегда поверх (пропы с footprint уже сортируются правильно через actors.addRect) ✅ сделано (2026-09-09): движок — `IsometricTileMap.setTallDepthLayer(layer\ null)переносит tall-вьюхи клеток в общийIsoDepthLayerс глубинойtx+ty(кулинг/setTileработают по ссылке,nullвозвращает в карту); игра —LocationSceneвызываетsetTallDepthLayer(this.actors)+actors.addRectдля пропов; тесты движка (2 новых), сценарийhero_depth.mjs` (численные zIndex-assert + скриншоты)
20 Пост-эффекты движка: переменный блюр (в точке с радиусом / на объекте / на всю сцену), хроматическая аберрация, марево («горячий воздух») ✅ сделано (2026-09-09): движок — render/postfx/ (postSim.ts чистая математика + 7 тестов, PostFx.ts — один кастомный Pixi-фильтр на worldRoot, API set*/pulse* по образцу Lighting, blurObject на вьюхе, resolution: 'inherit'); игра — блюр-волна резонанса, аберрация урона, авто-марево у Старой Машины/очага, мост scene:postfx (blur/aberration/shimmer/clear), сценарий checks/postfx.mjs (команды + скриншоты в пике)
20b Тени без 3D: блоб-тени актёров + направленные тени tall-объектов по солнцу ✅ сделано (2026-09-09): движок — render/shadowSim.ts (чистая математика солнца sunShadow/blobPlacement, 8 тестов) + render/shadows.ts (ступенчатая текстура блоба, раскладка); игра — GameShadows (ground-слой между картой и актёрами, блобы первого ребёнком вьюхи, чистка уничтоженных), сценарий checks/shadows.mjs (утро/полдень/вечер/ночь, скриншоты). Позже по желанию: контактный AO у подножий
21 Перепаковка репо под две версии движка: v1 (черновик + демо) → v1/ целиком, корень — диспетчер команд (--prefix v1); фиксы тихих поломок v1 (тени «сидят на базе», RT-тьма выводится на экран); скелет v2: v2/ workspace, packages/engine (@rpg/engine 2.0.0-alpha, three.js), маниалы CLAUDE.md двух уровней ✅ сделано (2026-09-09): см. «Уроки черновика» и «Направление v2» ниже; верификация — check:fast + agent:check из корня зелёные
22 Спайк v2: воксельный рендер — чистая математика вокселей (сетка, culled-меш, классический AO 0..3), VoxelRenderer на three.js (вершинные цвета, изо-ортокамера, dirLight+shadowMap), low-res 480×270 + nearest-апскейл, apps/demo со сценкой, скриншот-гейт через агента с первого дня (день/вечер, тени ползут) ✅ сделано (2026-09-09): движок — voxel/grid.ts + voxel/mesher.ts (12 тестов) + render/voxelRenderer.ts; тулзы — tools/agent-lib.mjs (каркас v2) + png.mjs (RGB-скриншоты Chromium); демка — сценка в палитре арт-библии, мост __agent.setSun/stats; гейт apps/demo/tools/checks/render.mjs зелёный (тени ползут: 76k пикселей разницы утро/вечер, апскейл 2×2 без нарушений, 67 цветов). Грабля спайка: минус в Y изо-направления = камера под землёй — снапшоты зелёные, увидел только скриншотом (урок №2 подтверждён в первый же день)
23 v2 трек 2: чанковый мир — Chunk (16×H×16), VoxelWorld (словарь чанков, get/set через границы, dirty-трекинг), meshRegion (мешер области по функции доступа — шов чанков бесшовный, AO через границы), демка на мире 3×3 чанков, мост setVoxel, гейт: воксель поставлен → кадр изменился, AO на шве не рвётся ✅ сделано (2026-09-09): движок — world/chunk.ts + world/world.ts (правка кромки помечает соседа) + world/worldMesher.ts; мешер обобщён до meshRegion (meshVoxels — обёртка); тест шва — меш 4 чанков ≡ меш одной сеткой (поз+AO попиксельно), 20 тестов v2. Демка — мир 6×6 чанков (96×96) с ремешингом dirty, мост setVoxel/chunkStats/perf (правка атомарна с ремешингом). Гейт 9/9: постройка 36 чанков + мешинг ~0.4–0.5 с (лимит 2 с), столбик на шве x=48 меняет кадр (~1000 пикселей). Урок счёта: интерфейсная грань двух вокселей — два квада (12−2=10, не 11); правка занятого вокселя лицом не меняет счётчики (тест-столбик дважды «исчезал»: вне кадра и внутри дома)
24 v2 трек 3: генераторы воксельных моделей — формат внутреннего JSON (size + dense base64, валидация агентом; .vox — позже), процедурные генераторы (деревья 3 варианта, валуны 3 варианта, детерминизм по сиду), штамповка в мир с клиппингом, демка на генераторах, гейт: варианты различаются, штампы на месте ✅ (2026-09-09). math/rng.ts (mulberry32), models/format.ts (encode/decode/validate), models/generators.ts (деревья: круглая/колонна/ель; валуны: union сфер + свет на верхних гранях), models/stamp.ts (клиппинг half-open). Демка: 12 деревьев + 3 валуна из генераторов, ручные tree/boulder удалены (урок №4). Гейт 12 проверок: + модельная статистика, + детерминизм через мост (stampTree дважды → тот же счётчик, вариант 1 ≠ вариант 0). Грабля: VoxelGrid.count() — метод, а не геттер (тесты дважды ловили).
25 v2 трек 4: turntable-листы — визуальный гейт генераторов: страница со всеми вариантами моделей (деревья/валуны) × 4 поворота, скриншот в /tmp, проверки: ячейки непустые, варианты попарно различаются; основа для апрува сгенерированного контента агентом ✅ (2026-09-09). apps/demo/turntable.html + src/turntable.ts: 6 моделей × 4 ракурса (0/45/90/135° — диагонали дают другой силуэт) на подставках 8×8×2, ракурс — mesh.rotation.y вокруг центра (geometry.translate −sx/2). Гейт tools/checks/turntable.mjs: центры ячеек считаются той же проекцией, что камера (зеркало lookAt-базиса), референсный кадр без моделей через setModels(false), выборка — эллипс 48×42 в ромбе подставки; 5/5 зелёных, minDiff 570/490 при пороге 150. launchBrowser({viewport}) в agent-lib (страница 720px выше дефолтного вьюпорта). Уроки: дробные координаты VoxelGrid.set молчу no-op (Uint8Array[4.5]); скриншот = вьюпорт браузера, канвас без центрирования; малые боксы у края ромба дают мало пикселей — выборка эллипсом.
26 v2 трек 5: .vox импорт/экспорт (MagicaVoxel) — parseVox/exportVox (чанки MAIN/SIZE/XYZI/RGBA, PACK>1 — ошибка), оси vox z-up → движок y-up, слоты модели → подряд 1..N при экспорте; тесты roundtrip; мост демки stampVoxB64 + гейт: штамп .vox меняет кадр ✅ (2026-09-09). models/vox.ts: полный обход чанков (неизвестные пропускаются с детьми, обрыв файла и обрыв в заголовке — ошибки), оси (x,y,z)_engine = (x,z,y)_vox, RGBA ровно 256 записей. Обнаружили «слоты модели ≠ слоты сцены» → models/paletteMap.ts: remapSlots (точный hex или ближайший RGB, детерминированно). Демка: мост stampVoxB64(b64,x,z) (парсинг → перекладка на палитру сцены → штамп + ремешинг). Гейт 13 проверок: .vox-фикстура (собрана exportVox-ом, base64 вкладывается в скрипт) штампует 12 вокселей и меняет кадр. Уроки: Node не импортирует @rpg/engine (TS-исходники с extensionless-импортами) — фиксстуры для гейтов генерятся tsx-ом и вкладываются в скрипт готовыми; точка штампа для проверки «кадр изменился» обязана быть в кадре камеры (штамп вне вью дал placed=12 при voxDiff=0); заголовок чанка — 12 байт (id+contentSize+childrenSize), обрыв в нём ловить границей в id(), иначе голый RangeError из DataView.
27 v2 трек 6: рига/анимация («анимация = данные») — рига: кости (имя, родитель, pivot в связочном пространстве), детерминированная привязка вокселей к ближайшей кости; клипы: ключевые кадры {t, кость: rot/pos}, линейный семплинг поз, числовая валидация (монотонность ключей, непрерывность цикла первый=последний, контакт стопы); процедурная локомоция (walk-цикл из параметров); генератор манекена с ригой; рендер деформированных вокселей (instanced-облако); превью-лист кадров + мост (числовые снапшоты поз) + гейт ✅ (2026-09-09). animation/mat.ts (чистая аффинная математика, порядок Эйлера X→Y→Z), animation/rig.ts (кости + pivot в связочном пространстве; привязка: у генератора — явная, для чужих — bindNearest по pivot; poseVoxels — жёсткая деформация на кость), animation/clip.ts (ключи {t, кость: rot/pos}, линейный семплинг с заворотом t для циклов, validateClip — границы/монотонность/замкнутость цикла, footMinY — контакт стопы), animation/locomotion.ts (walkClip: ноги в противофазе, руки против одноимённой ноги, корень приседает дважды и качается), models/mannequin.ts (5×11×3, 5 костей, явная привязка — Voronoi по pivot отнёс бы торс к рукам). Рендер: addVoxelCloud (InstancedMesh, ось-выровненные кубы, обновление позы без пересборки геометрии). Демка: anim.html + anim.ts — 8 кадров цикла (4×2), мост poseStats(i)/clipCheck()/setModels. Гейт anim.mjs 8 проверок: картинка (ячейки видны, соседние фазы различаются) + числа через мост (клип замкнут, противофазы, контакт стоп ≈0.5 в опоре / подъём в махе, приседание и качание корня). 91 юнит-тест. Уроки: composition affine — перенос parent добавлялся дважды (тест иерархии поймал: ребёнок уезжал на 2·t вместо t); binding индексируется по сетке, posed-массив только из занятых вокселей — сверять через index вокселя, не позицией в массиве; Voronoi-привязка по pivot семантически неверна для внутренних вокселей (торс у плеч ближе к кости руки) — генератор привязывает явно; соседние фазы 45° дают ~110 px разницы на мелком манекене (порог гейта 50, не 150); sheetInfo/gейт должен отдавать РЕАЛЬНЫЙ viewSize камеры (в turntable 30 vs 32 проходило только за счёт запаса эллипса — исправлено).
28 v2 трек 7: физика/коллизии воксельного мира — AABB-тело (pos = центр низа, vel, size, grounded) против вокселей (занятый воксель = твёрдый), гравитация, ось-раздельное разрешение X→Z→Y с зажимом к граням вокселей, сабстепы против туннелирования; юнит-тесты (падение/остановка, стена, угол, сход с уступа, потолок, тонкая стена); демка: тело-манекен в сцене с walk-анимацией в движении, мост bodySpawn/bodyStep/bodyInfo + гейт (гравитация, стена, кадр меняется) ✅ (2026-09-09). physics/body.ts: SolidWorld (структурный интерфейс — точечная выборка get, чистая математика без рендера), makeBody/stepBody (полунеявный Эйлер: скорость до позиции), collides (AABB по воксельным диапазонам, касание грани снаружи — не столкновение), ось-раздельные подшаги X→Z→Y с зажимом к грани вокселя и EPS-зазором (пол — ровно на грань, без ловли своей же опоры), сабстепы (смещение за подшаг < 0.9 вокселя) против туннелирования сквозь тонкие стены. 10 юнит-тестов: падение на грань, покой, сход с уступа, стена останавливает, туннелирование (50 вокс/с при dt=0.1), угол двух стен, потолок, число гравитации. Демка: тело-манекен (AABB 3×10×3 — узкий силуэт; махи ног визуально выходят за AABB, это норма) с walk-анимацией и поворотом по направлению движения; мост bodySpawn(x,z,vx,vz) (спавн в воздухе y=6)/bodyStep(n) (фиксированный dt=1/60 — детерминизм гейта)/bodyInfo/bodyRemove. Гейт physics.mjs 7 проверок: спавн в воздухе, падение на землю y=2, фаза walk-цикла растёт в движении, стена дома останавливает по грани (pos.x ≈ 44 − w/2 − eps), кадр изменился, манекен рисуется (без тела кадр возвращается). 101 юнит-тест. Уроки: аргумент VoxelWorld(n) — это ВЫСОТА чанка, а не число чанков (set вне y-границ молча возвращает false — тест-мир «потолка» не получил и тест падал загадочно); Vec3 в mat.ts — readonly: мутирующее состояние физики (pos/vel) — обычные кортежи [number,number,number], Vec3 только на входах; спавн тестового тела проверять на пересечение AABB с миром — тело 3×10×3 у плиты потолка цепляло её край и «мгновенно приземлялось» на y=6.
29 v2 трек 8: каркас ядра + герой — GameLoop (фиксированный шаг 60 Гц, ручной режим для моста/гейтов), минимальный ECS (решение открытого вопроса «каркас ядра»), ввод как данные (экранные оси, проекция в мир на границе), контроллер персонажа (тело физики + walk от скорости), камера следит; демка: герой на WASD и виртуальным стиком агента, мост setInput/heroInfo/heroTeleport/stepTicks/setManual/setFollow + гейт ✅ (2026-09-09). core/loop.ts (GameLoop: аккумулятор, maxStepsPerFrame, setManual+stepTick — детерминизм гейтов; now/schedule инъектируются для Node-тестов), core/ecs.ts (EntityWorld — перенос проверенной минимальной ECS v1: сущность-число, компоненты по имени, query, системы), core/controls.ts (ControlState + screenMoveToWorld — экранные оси → мировая горизонталь по yaw камеры, диагональ не быстрее), physics/character.ts (Character: тело + фаза клипа ∝ путь/stride — стопы не скользят, покой замирает, поворот по движению; spawnY — спавн в воздухе для гейта физики; мост тела физики переехал на Character — один путь, урок №4). Рендер: setTarget (камера+солнце за целью) + currentTarget. Демка: герой на WASD/стрелках (клавиатура демки, screenMoveToWorld с ISO_YAW), камера следит с шагом пиксельной сетки (round), мост setInput/heroInfo/heroTeleport/stepTicks/setManual/setFollow (гейт физики держит камеру фиксированной). Гейт hero.mjs 9 проверок: герой на земле, ввод агента двигает (+x мира из (1,1) экранного), фаза ровно 1.25 цикла/с (скорость/stride), стена по грани (42.4999), покой замирает, кадр меняется, камера следует, телепорт уезжает. 115 юнит-тестов. Уроки: фаза = путь/stride, не время — при скорости 4 и stride 3.2 это 1.25 цикла/с (совпало с длительностью клипа 0.8 с намеренно: stride = speed·duration); виртуальный стик агента живёт между вызовами моста — перед чистой проверкой кадра отпускать setInput(null,null); cancelAnimationFrame нет в Node — typeof-guard в GameLoop.stop (юнит-тесты с инъекцией schedule).
30 v2 трек 9: диалоги как данные + интерактивные точки — граф диалога (JSON/TS: узлы с репликами, выборами, условиями и эффектами), рантайм отделён от отрисовки, реестр точек взаимодействия; демка: NPC у дома, «E» открывает диалог, HTML-оверлей, 2 графа с эффектами + гейт ✅ (2026-09-09). dialogue/graph.ts — перенос типов v1 (узел/выбор/условия/эффекты), evalConditions (когда/когдаНе/переменные/предметы — все группы AND), validateDialogue (битые ссылки, тупики, выборы без текста — числовая валидация для агента); состояние — интерфейс DialogueState (развязка от GameState v1), мир — DialogueWorld (сумка). dialogue/runner.ts — рантайм семантики v1: узел с текстом ждёт игрока (advance → next, pick → вариант; advance во время выбора игнорируется), узел без текста — «действие»: применяет эффекты и проваливается в next сам, MAX_STEPS против циклов; onEffect — эффекты do[] движок только эмитит, исполнение — игра; геттеры nodeId/node/choices/path/result — снапшоты для моста. interaction/interactables.ts — реестр точек {id, pos, radius, label, dialogueId} + чистые nearestInteractable/interactablesInRadius (Y не учитывается). Демка: страж у дома (Character на скорости 0 — поза покоя, один путь с героем), табличка; клавиша E + мост; HTML-оверлей (панель + кнопки вариантов); 2 графа: страж (выборы → setFlags/setVars, условный узел ask2 по переменной, тост) и табличка (один узел + тост); мост interact/interactInfo/dialogueOpen/dialogueInfo/dialoguePick/dialogueAdvance. Гейт dialogue.mjs 15 проверок: графы валидны, подсказка появляется/пропадает по дистанции, подсказка и панель рисуются (сравнение ЗОН кадра — верх/низ, одна позиция камеры), открытие по «E», advance при выборе игнорируется, эффекты флагов/переменных/тостов, путь и результат, закрытие возвращает кадр. 136 юнит-тестов. Уроки: HUD-элементы обновляются только из тика — любое изменение состояния вне тика (мост, клик кнопки) обязано само дёргать обновление подсказки (гейт поймал: диалог открыт, подсказка «E» осталась висеть); скриншот-сравнение HUD делай по зонам кадра с одной позиции камеры — сравнение целых кадров с разных позиций даёт diff всего луга (324000 px) и проверяет не HUD, а камеру.
31 v2 трек 10: инвентарь — жанронезависимый контейнер предметов (перенос v1), эффекты диалогов giveItem/takeItem исполняются игрой, HUD сумки; демка: страж даёт/забирает цветок, выбор виден по hasItem + гейт ✅ (2026-09-09). inventory/inventory.ts — перенос Inventory v1 1:1: счётчики по id (Map), лимиты слотов/стака обрезают и возвращают факт (модель не бросает), onChange с деталями изменения (add/remove/clear/load), serialize/load (мусор игнорируется, кламп под лимиты), inventoryFromData для сейвов. Демка: сумка героя (maxSlots 8), DialogueWorld.hasItem смотрит в неё (выбор «Отдать цветок» виден только с цветком), onEffect исполняет giveItem/takeItem (движок эмитит — исполнение игрой), HUD #inv в углу перерисовывается по событию контейнера (не из тика — надёжно), ITEM_NAMES для отображения. Гейт inventory.mjs 8 проверок: сумка пуста на старте, скрытый/видимый выбор по hasItem, giveItem наполнил (цветок ×1), HUD рисуется (зона угла кадра), takeItem забрал (флаг flowerGivenAway), HUD скрылся (diff 0). 147 юнит-тестов. Урок: перерисовка HUD по событию контейнера, а не из тика — Inventory.onChange синхронен, изменения из моста/кнопок применяются сразу.
32 v2 трек 11: сохранение — SaveManager (JSON-слоты, StorageLike инъекцией, мета в параллельном ключе), сериализация состояния демки (герой/флаги/переменные/сумка), мост saveTo/loadFrom/saveList/saveDelete + гейт ✅ (2026-09-09). save/saveManager.ts — перенос SaveManager v1 1:1: save(slot, data, meta?)/load (битый JSON → null)/slotMeta/list/delete/has/listSlots; хранилище — StorageLike инъекцией (демка — window.localStorage, тесты — Map-заглушка), мета — в параллельном ключе meta:<slot> (старые сейвы читаемы). Демка: DemoSave {version, heroPos, heroYaw, heroPhase, flags, vars, inv}serializeSave/applySave (версия формата проверяется; активный диалог прерывается; HUD-подсказка обновляется сразу — урок трека 9); мост saveTo(slot, title)/loadFrom(slot)/saveList()/saveDelete(slot). Гейт save.mjs 9 проверок: слотов нет на старте, saveTo пишет слот+мету, состояние ломается (отдача цветка + уезд героя), loadFrom восстанавливает позицию/флаги (без flowerGivenAway)/сумку (цветок ×1), кадр после загрузки попиксельно равен кадру сохранения (diff 0), saveDelete чистит. 154 юнит-теста. Уроки: в ручном режиме кадр рендерится только в stepTicks — после loadFrom-мостов нужен тик перед скриншотом (иначе скриншот захватывает протухший канвас с прежней камерой); applySave меняет состояние вне тика — обновлять HUD сразу (тот же урок, что у диалогов).
33 v2 трек 12: сцены/переходы — стек SceneManager (меню поверх игры), фейд-переходы out→swap→in, параллельные переходы глотаются; демка: DOM-меню (старт/продолжить), автосейв при уходе в меню, Esc + гейт ✅ (2026-09-09). core/scene.ts — перенос схемы v1 без привязки к бэкенду: Scene {enter/exit/update/render}, стек push/pop/replace, переход {duration} фазами затухание → смена сцены → проявление (каждая фаза длится duration — полный переход 2×duration); FadeOverlay {setAlpha} инъекцией (демка — DOM-слой #fade, тесты — заглушка); параллельные переходы глотаются (pending-проверка в begin), enter новой сцены не ожидается (void), update тикает текущую сцену и во время перехода, render рисует весь стек (нижние сцены видны). Демка: gameScene (tick+рендер) внизу стека, menuScene (DOM-панель) сверху — мир живёт под меню с самого старта; Esc/мост gotoMenu — из игры автосохранение в слот auto + push меню, из меню — возврат; «Продолжить» грузит auto (кнопка disabled без сейва); мост sceneInfo/gotoMenu/startGame. Гейт scenes.mjs 11 проверок: старт в меню без перехода, startGame — переход идёт (сцена ещё меню), фейд в конце фазы out — кадр чёрный, переход завершён (меню скрыто), кадры меню/игры различаются, gotoMenu пишет автосейв, возврат в меню, «Продолжить» вернул героя на место сохранения, параллельный переход глотается, повторный переход работает. 164 юнит-теста. Уроки: CSS display:flex перебивает атрибут hidden — нужен #menu[hidden]{display:none} (гейт save поймал: меню «скрыто», но видно на каждом кадре, испортив попиксельное сравнение); полный переход — это out+in (2×duration = 48 тиков при 0.4 с), а не 24 — в ручном режиме гейты обязаны пройти все 48 тиков, иначе фейд висит частично затемняя каждый кадр (ловится только попиксельным сравнением); все гейты демки теперь входят в игру через startGame(false)+60 тиков — демка стартует в меню, старые гейты снимали кадры поверх панели меню.

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

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


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

  • Машина разработки (обновлено 2026-09-09): RTX 3090 24 ГБ VRAM (CUDA 13.3), 12 ядер CPU, 62 ГБ RAM. Прежняя запись про Intel HD 620 / 4 ядра / 15 ГБ относилась к старому ПК и устарела вместе с B3. GPU-инференс — основной путь (замер A0: 10 с аудио за ~15 с; SD-листы тоже ускоряются кратно). Квантизация и компактные модели больше не обязательны, ограничение — VRAM 24 ГБ и диск.
  • Стиль игры жёсткий: палитра 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, размер, статус
  → v1/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. Пробы: v1/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 качество токен-ракурса падает.
  • B4 (задача 5, 2026-09-09, новая машина с RTX 3090): замена плейсхолдеров интерактивов, снаряда и иконок сумки на арт. GPU меняет экономику: ~1.3 с/вид fp16 (SD1.5, 20 шагов) против 6.6 мин CPU — сиды бесплатны, кандидатов можно генерировать пачками.
    • Модель: Retro Diffusion (HF LostMedia/RetroDiffusion, Public Domain Mark 1.0, SD1.5-пиксель-финтюн) — одиночные объекты по промпту ведёт хорошо; B3-модель листов (SD_PixelArt_SpriteSheet_Generator) для объектов непригодна (всегда рисует 4 фигуры на кирпичном фоне). Sheet-модели не использовать.
    • Пайплайн: tools/aiart/gen_b4.py (GPU-партия, реестр LIST, CLI id:seed:…) → tools/aiart/post_b4.mjs (кроп фигуры → meanRect-даунскейл усреднением ячеек с фоновой маской → квантизация в палитру → обзорный лист). Фоновая маска — BFS-наращивание от краёв (фон RD — градиент, угловой пиксель его не описывает; допуск шага 40, у границы объекта скачок).
    • В игру: 7 интерактивов AI (столб, мот, верёвка колокола, прилавок, очаг, записка, плевок-снаряд) + 7 иконок сумки 8×8 (ui/icon_*) через механизм InteractableDef.view.tex (якорь низ-центр по тайлу, вместо Graphics- плейсхолдера) и Sprite в InventoryScene (строки с value: '' — метка прижата влево, иконки слева синхронны окну скролла). Старая Машина — AI.
    • Рукодельный фолбэк сработал: tone_tree/resonator (32×40+) у RD на тёплом градиентном фоне и с шумной деталью — даунскейл даёт кашу при любом кейнге (8+16 новых сидов перепробовано). Нарисованы кодом в gen.mjs (toneTreeTile/resonatorTile) — стиль единый с остальными тайлами. Вывод: AI — для мелких/средних объектов с чистым силуэтом; крупные — код.
    • Кандидаты: tools/aiart/review-b4/ (raw+processed, 160+), в git не идут.

Работа 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 (v1/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 зрелый, но на нашей машине безнадёжно медленный; пропускаем

Разведка A0 (2026-09-09, новая машина)

  • Железо не CPU-only, как предполагалось: RTX 3090 24 ГБ + 12 ядер + 62 ГБ RAM. Генерация 10 с аудио (100 шагов): GPU fp16 ~15 с (CPU fp32 — 3.5 мин). Это снимает оговорки про «терпимо» у ACE-Step и делает GPU-инференс основным путём для всех работ AI-контента (и аудио, и изображений).
  • Stable Audio Open Small закрыт: gated: auto, анонимно 401 — нужен HF-токен + принятие Stability Community License (весов Small-SFX/Small-Music как отдельных репо нет; доступен общий stable-audio-open-small).
  • PoC сделан на фолбэке AudioLDM2 (cvssp/audioldm2, CC-BY-NC-SA — в основной пайплайн с некоммерческими весами не идёт, как и договаривались). Стек: ~/.cache/rpg-ai/venv-aiaudio, torch 2.14.0+cu130, diffusers 0.35.2 + transformers 4.49.0 (diffusers 0.40 требует transformers 5.x и падает на AudioLDM2: _update_model_kwargs_for_generation удалён в 5.x).
  • Пайплайн: v1/apps/game/tools/aiaudio/gen_audioldm2.pypost.py (ресэмпл 22050 моно 16-bit, нормализация 0.85, loopify — хвост кроссфейдом в голову за 1 с, проверка стыка --check). Сырые клипы — review/ (в gitignore, перегенерируются по seed). Seed-детерминизм — по-устройству (fp16 GPU ≠ fp32 CPU).

Пайплайн (как сделано, A1)

npm run aiaudio gen [id[:seed,seed]...]   # AudioLDM2 + post.py (ресэмпл 22050 моно
                                          # 16-bit, нормализация, loopify 1 с, --check
                                          # шва; sfx — без лупа)
npm run aiaudio list | promote <id> <seed> # апрув-гейт: review/ → assets/audio/,
                                           # gate=approved в манифесте

Манифест — v1/apps/game/tools/aiaudio/manifest.json (промпты, сиды, duration, peak — цель нормализации, gate, заметки). Принципы aiart: манифест, апрув-гейт (ключ promote проверяется на объявление в data/audio.ts), seed-детерминизм, формат игры не меняется (шины master/music/sfx/ambience готовы). Лупы проверяются скриптом (стык начала/конца без щелчка) и метриками analyze.py (огибающая/ спектр — отбор при делегированном апруве). SFX остаются на спек-синтезе — промотировать некуда (SFX_KEYS пуст), файлового SFX-пути в движке нет. «Взгляд» агента на звук: движковая тулза v1/packages/engine/tools/spectro.mjs (спектрограмма log-частоты + огибающая RMS + маркер шва → PNG; npm run aiaudio view <id>) — замена числового отбора визуальной оценкой на гейтах A2/A3.

Этапы

  • A0 (PoC): локальный text-to-audio; амбиент-луп прудов. Критерий: звук лучше процедурного на слух и формат сходится. (SFX-замены выпали: после E4 файловых SFX нет — все на спек-синтезе data/sfxSpecs.ts.)сделано 2026-09-09 (PoC на фолбэке AudioLDM2, слушание — у пользователя).
  • A1: пайплайн aiaudio + манифест; амбиент-лупы областей. ✅ сделано 2026-09-09 (решение A1: AudioLDM2, игра некоммерческая): манифест-driven раннер npm run aiaudio (gen/post/promote/list, python из ~/.cache/rpg-ai/venv-aiaudio), пост-обработка с пер-позиционным peak, метрики analyze.py. Промотированы: роща (свой луп вместо подстановки прудов, seed 2), разъезд (seed 3), ветер (фидбек: завывание + периодическое затухание + тише — пик 0.6, seed 3). Ночью Звенца / дома / вода — оставлены процедурные (огибающая живее у эталонов, кандидаты hold в манифесте). SFX — остались спек-синтезом (см. выше).
  • A2: музыкальные лупы. Уточнение: в игре нет музыкальных WAV — темы это спек-музыка (data/music.tsrenderMusicStereo в рантайме), поэтому сравнение не с gen.mjs, а со спек-темами. Кандидаты на фолбэке AudioLDM2 сгенерированы (2026-09-09: meadows/combat/tension × 2 сида, швы чистые, на спектро-листах гармоническая структура без артефактов, music_* в манифесте). Решение A2 (2026-09-09, делегировано): спек-темы остаются — AI-музыка не слушалась, а AudioLDM2 музыкально слабейшая его часть; менять вслепую — риск регресса проверенной шины. Кандидаты hold в манифесте; файловый путь на шине music реализуем за час при первых апрувнутых лупах (движковых изменений не нужно: WAV → createBufferplayLoopBuffer, как tension-стем). Small-Music — главный путь при появлении HF-токена.
  • 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.mjsagent: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. Пересоздано 2026-09-09 на новой машине: venv на python3.11 (torch 2.14 CPU
    • diffusers 0.40 + sentence-transformers), веса PXG-Tiny / PixelGPT-24×24 / SD_PixelArt скачаны заново; gen_pxgpt.py восстановлен в репо (tools/aiart/gen_pxgpt.py, маппинг токенов сверен с review-b1: токен 0 — прозрачный, токен t — цвет t-1 палитры промпта). PXG-Tiny детерминирован между машинами (сверка с review B0 — идентичные пиксели); PixelGPT и SD — воспроизводимы до шума сэмплирования между версиями torch.
  4. Порог апрува: кто смотрит review-лист (пользователь) и где фиксируется вердикт (пометка в манифесте).

Что НЕ делаем

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

Уроки черновика → видение движка v2 (черновик, 2026-09-09)

Решение (2026-09-09): текущий движок считается черновиком (v1) — он остаётся рабочим витринным движком, чиним и допиливаем его без больших рефакторингов; накопленные уроки формируют видение v2. Этот раздел — живой: пользователь и агент пополняют его по мере осознания.

Что в v1 удалось (в v2 сохранить как принципы)

  • Фиксированный шаг и детерминизм (GameLoop 60 Гц + stepTicks, ручной шаг без рендера) — основа воспроизводимости и агентного моста.
  • Разделение «чистая математика / вьюхи»: lightSim, shadowSim, postSim, particleSim, fxSim — без Pixi, тестируются в Vitest без браузера. Лучшее решение проекта: баги ловятся тестами до запуска игры.
  • Агентный мост с первого дня: window.__agent, снапшоты-слои, инварианты, CLI-сценарии проверок, practices.md. Состояние читается снапшотом, скриншот — только для визуальных вопросов.
  • Жёсткая граница движок/игра с автоматическим гвардом.
  • Инъекция зависимостей (StorageLike и другие) — тесты без DOM.
  • Доки — точка входа: llms.txt, доки движка по модулям, «грабли».

Что v1 показал болью (в v2 переделать)

  1. Координатные пространства — главный источник багов: виртуальные px, CSS-px, мировые юниты, тайловые {x,y}, offset worldRoot смешивались в сигнатурах и головах; направление тени считалось прямо в экранных координатах (баг «летающих» объектов). В v2: все направления/позиции — в одном мировом пространстве, проекция — единственная точка на границе рендера; типы различают пространства (юниты ≠ px ≠ тайлы).
  2. «Сделано» ≠ «видно»: RT-тьма запекалась, но не выводилась — снапшот-пробы зелёные, визуально подсистема мёртвая. В v2: визуальная подсистема считается готовой только со скриншот-гейтом, а не только снапшот-ассертами.
  3. Сцена-оркестратор (LocationScene ~900 строк) разрезалась вручную и всё равно разрастается. В v2: декларативная композиция сцены из систем с явным порядком тика (карта → свет → тени → постэффекты).
  4. Pixi протекает в API (Renderer в сигнатурах хелперов). В v2: рендер-бэкенд за узким интерфейсом, ядро движка его не знает.
  5. Ручные пулы вьюх (toneFree/holeFree/...) — многословно и ошибкоёмко. В v2: декларативные слои с авторазмещением по ключу.
  6. Мёртвый код переживает проверки — нужен принцип: не оставлять подключённый, но неиспользуемый путь; удалять или связывать сразу.

Направление v2: воксельный движок (решено в беседе, 2026-09-09)

Фокус прежний: движок + инструментарий ИИ-агента; игра — демо-полигон. Демка остаётся изометрией (фиксированная камера), движок v2 — жанронезависимый.

  • Стиль: воксель как продолжение пиксель-арта — рендер сцены в низкое разрешение (nearest-апскейл, тот же приём, что виртуальные 480×270 в v1), палитра вокселей квантуется в арт-библию (B/F — «жизнь»). Настоящие свет/тени: shadow map + воксельный AO (по занятым соседям) вместо фейков v1.
  • Рендер-бэкенд: three.js на низком уровне (роль Pixi в v1 — «только рендер-бэкенд»), ядро движка своё и о бэкенде не знает (урок №4: рендер за узким интерфейсом). Свободная камера — позже, не в старте.
  • Формат моделей: гибрид — внутренний формат свой (валидация агентом), импорт/экспорт .vox (MagicaVoxel) как мост к готовым инструментам (аналогия: Tiled-импорт в v1).
  • Генерация моделей — 3 взаимодополняющих маршрута:
    1. Процедурные генераторы кодом (аналог tools/pixelart/gen.mjs) — основной путь: детерминизм, ревью диффом, юнит-тесты;
    2. Image/text-to-3D локально (RTX 3090: TripoSR/Shap-E-класс) → вокселизация → квантование в палитру → review-лист → апрув-гейт (аналог пайплайна A0/B4);
    3. Ручной MagicaVoxel для ключевых героев; агент — вариации и производные. Общий визуальный гейт — turntable-листы (6–8 ракурсов в один PNG) + апрув.
  • Анимация = данные, не кадры: рига + кривые ключевых кадров в JSON (кости деформируют близкие воксели линейным блендингом), процедурная локомоция (шаг от скорости, secondary motion), вторичная анимация кодом. Валидация числовая: непрерывность цикла, контакт стопы (y кости ≈ 0 в фазе опоры), дистанции костей — юнит-тесты + снапшот поз в агентный мост
    • превью-листы кадров. Агент авторит кривые/параметры текстом.
  • Перенос из v1: аудио-трак целиком (wav/spectro/манифесты/шины — рендер-агностичен); tools/pixelart остаётся для UI/иконок/частиц; агентный мост + CLI-чеки — ядро v2, а не «порт» (согласовано).

Открытые вопросы v2 (копить здесь): каркас ядра (ECS v1 или новый) — решено (трек 8, строка 29): минимальный ECS v1-схемы (EntityWorld), GameLoop v1-схемы с ручным режимом, ввод как данные; физика/коллизии в воксельном мире (трек 7), тайлинг мира (чанки) (трек 3), диалоги как данные (трек 9, строка 30: граф + рантайм семантики v1 + валидатор + точки взаимодействия), инвентарь (трек 10, строка 31: Inventory v1 1:1, эффекты giveItem/takeItem исполняются игрой), сохранение (трек 11, строка 32: SaveManager v1 1:1, состояние демки восстанавливается попиксельно), сцены/переходы (трек 12, строка 33: SceneManager — стек + фейд, параллельные переходы глотаются). Остаётся: производственный порядок следующих треков (бой, аудио-перенос — по мере движения к воспроизведению демки v1 на v2).