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 тиков — демка стартует в меню, старые гейты снимали кадры поверх панели меню.
34 v2 трек 13: бой — перенос боевой системы v1 на воксельный движок: PlayerCombat (конус «гулкий звон» будит пепел / заряженный резонанс усыпляет), EnemyBrain (стейты dormant/rise/chase/hurt/flee/sleeping, слух по уровням шума, LOS с кэшем), CombatWorld (спавн, снаряды плевуна, трупы, толкучка-сепарация); демка: сгустки-кучки на лугу, атака на Space, HUD сердец, мост combatSpawn/combatClear/combatInfo/combatStats/attackInput + гейт ✅ (2026-09-09). combat/ движка: shapes.ts/los.ts/stats.ts/enemies.ts/playerCombat.ts/brain.ts (StateMachine + Cooldown) + world.ts — CombatWorld с хуками {onEnemyHurt/onEnemyKilled/onEnemyAwake/onAttack/onSpit} вместо EventBus v1; deps {getPlayerPos, damagePlayer, solid, opaque} — мир инъекцией предикатами. 16 юнит-тестов (217 всего). Масштаб: 1 боевой юнит = 3 вокселя — боевые числа v1 в юнитах (герой ≈ 1), конвертация только на границе демки (позиции /3, отброс ×3), CombatWorld живёт в чистом юнит-пространстве. Демка: спавны (патруль (8,10)→(10,12), спящие плевун (19,20) и тяжёлый (22,13)), кучки — приплюснутые эллипсоиды из вокселей (спящий — нижний ряд, «блин»; hurt — вспышка 0.12 с), шум шагов 0.35, HUD #hp сердечками (скрывается вне игровой сцены — isBlack-проверка scenes-гейта сканирует весь кадр). Гейт combat.mjs 14 проверок: 3 сгустка/2 спят, кучка рисуется (diff ~1100), звон будит без урона (rise), погоня до равновесия 0.55, конус 3→1→труп→исчез, смерть героя 4,3,2,1,5 + респаун (40,60), плевун 5→4 + HUD-зона, резонанс усыпляет обоих. Уроки: dormant глух к близости героя — спящий вплотную не просыпается (только шум ≥ 0.7 или урон), гейт будит звоном издали; равновесие сепарации = сумма радиусов (0.55) > attackRange ползуна (0.4) — ползун «липнет», но стоящего героя не бьёт (v1-faithful; контактные удары в гейте — телепортом в упор, тяжёлый 0.65 ≤ 0.8 бьёт); толкучка вталкивает врага в strike-радиус — два сгустка с разных сторон дают удар без атаки, спавнить с одной стороны; combatClear обязан чистить и снаряды — снаряд плевуна летит после удаления врагов и «призраком» наносит урон; боевые точки проверять прогоном — у старта героя (40,60) пусто, у (14,84) препятствие (равновесие погони ровно 0.55 — маркер открытости).
35 v2 трек 14: аудио — перенос аудио-подсистемы v1 на воксельный движок: AudioManager (4 шины, спек-синтез sfx без WAV-пайплайна), Voices/Loops/BufferCache/SoundSpec/Music, synth-примитивы в src движка; демка: позиционный звук боя (затухание + изо-панорама), шаги с вариациями, DEV-шпион + гейт ✅ (2026-09-09). audio/ движка: types.ts (шины/хендлы/опции), decode.ts (BufferCache: in-flight дедуп декода), Voices.ts (полифония 24, вор самого тихого, restart по ключу, панорама в полёте), Loops.ts (слоты музыки/амбиента с кроссфейдом, независимые), SoundSpec.ts/Music.ts (спек-синтез: hit/chime/scrape/hum/tone + треки bpm/beats/tracks), synth.ts (примитивы v1 tools/synth.mjs в TS — рантайм и тулзы делят одни формулы), AudioManager.ts (фасад: prepare() синхронно заводит контекст без resume, unlock, playSpec кэширует буфер по ключу, шпион onPlayed). 44 юнит-теста (261 всего, 31 файл), тесты v1 перенесены 1:1 (фейковый граф AudioContext). Демка: src/audio.ts — спеки боя/урона/шагов (формулы v1 sfxSpecs), DemoAudio (playAt: (1−d/radius)^1.5 + панорама ∝ изо-(dx−dz)·16/240, дедуп 0.08 с; StepVariants: 3 варианта шага, сид = база + i·10), обвязка в main.ts — хуки боя (cone→bell_hit, resonance→bell_low, spit, awake→ash_hiss, kill→ash_die, playerHit→hurt), шаг на границе половины walk-цикла, unlock на первом жесте, prepare() на старте (буферы вариантов компилируются сразу); мост audioUnlock/audioInfo (кольцевой буфер 24 записей фактических запусков). Гейт audio.mjs 8 проверок: тишина на старте (амбиента нет, герой не двигался), шаги играют, атака→bell_hit, плевок→spit (позиционный, в радиусе слышимости 12), резонанс→bell_low, лог ограничен 24. Уроки: кулдаун конуса 0.45 с не сбрасывается телепортом/очисткой — атака-пробуждение через ~9 тиков после атаки-звонка не выходит (release глотает), между боевыми секциями гейта ждать ≥30 тиков; звук в headless работает без жестов (agent-lib уже передаёт --autoplay-policy=no-user-gesture-required), prepare() даёт компиляцию буферов до первого resume.

Отложено: 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 — стек + фейд, параллельные переходы глотаются), бой (трек 13, строка 34: PlayerCombat/EnemyBrain/CombatWorld — перенос v1, хуки вместо EventBus, 1 боевой юнит = 3 вокселя, конвертация на границе демки), аудио (трек 14, строка 35: AudioManager/Voices/Loops + спек-синтез без WAV, позиционный звук и шаги в демке, DEV-шпион). Все 14 треков переноса подсистем v1 на v2 закрыты — дальше: воспроизведение демки v1 («Пепельные луга») на v2 по мере движения.

Воспроизведение демки v1 на v2 — дорожная карта (2026-09-09)

Цель: на движке v2 воспроизвести демо-срез v1 («Пепельные луга») — 8 областей (5 уличных + 3 интерьера), 3 NPC, 2 квеста, переходы, пикапы, накат-зоны, фауна, день/ночь, темы областей. Референс — v1; точность визуала не критична (воксель ≠ спрайт), но раскладка тайлов v1 (позиции NPC/переходов/квестов) переносится данными — сходится сюжет. Состав референса (разведка 2026-09-09): v1/apps/game/src/data/ — locations.ts (AREAS: meadows/ponds/zvenets/rust_grove/old_siding/ machine_hall/house_elder/shop, переходы step/click с requiresFlag/ requiresItem/lockedText, hazard-зоны наката, свет/атмосфера/fauna), map.ts (TILES ~35 id, BLOCKED, tall-объекты), карты assets/maps/*.map (RLE, parseMap), npcs.ts (Ирвин, Мила; Клинт по флагу act2_hook), dialogues/ (9 графов: elder × 3, trader × 3, clint × 3), ids.ts (18 флагов + vars), items.ts (7 предметов), interactables.ts, quests.ts + quest/FlowerQuest.ts, ResonatorQuest.ts, music.ts (THEMES — спек-музыка, рендер уже портирован в v2), fauna, пепел/моты.

Треки (нумерация продолжается с 15):

  • Трек 15 — карты и области ✅ (2026-09-10, коммит): конвертация карт v1 (apps/game/tools/convertMaps.mjssrc/data/maps.json, RLE как есть), воксельный генератор областей (src/areas.ts: 1 тайл = 3×3 вокселя, земля y0-1, вода слоем y=2 = blocked, штампы — компактные генераторы в тайл 3×3 mapTree/mapBush/mapBoulder + дома/колодец/башня/машина/ рельсы/стены/декор, всё детерминировано); новое приложение apps/game (демка не тронута — её 11 гейтов завязаны на сценку); переходы step/click с фейдом out→swap→in и стеком возврата 'return' (src/main.ts); мост areaInfo/gotoArea/setManual/stepTicks/heroInfo/heroTeleport/setInput/ setFollow/setSun/solidAt/interact; гейт tools/checks/areas.mjs — 16 проверок (детерминизм пересборки, blocked, зажим у воды, step-переходы луга⇄Звенец/луга⇄пруды, люк→зал→return по стеку, click «E», интерьер). Уроки: экранная ось (1,0) — диагональ мира (триггер срезается углом — чистые оси комбинированным стиком); штампы шире тайла запирают проходы — контент карты строго в тайл 3×3; solidAt — колонка над землёй (y≥2).
  • Трек 16 — NPC и квестовый каркас ✅ (2026-09-10, коммит): контент v1 1:1 — src/data/ids.ts (FLAGS 17/VARS/usedFlag), src/data/items.ts (7 предметов), src/data/npcs.ts (Ирвин/Мила статично в Звенце, Клинт — NPC_PLACEMENTS по флагу act2_hook), 9 диалоговых графов v1 в src/data/dialogues/*.json + реестр с validateDialogue при старте, src/data/quests.ts (стадии с doneFlag/ready/progressKey, questDialogueFor/questLog/questTracker), src/state.ts (GameState — DialogueState движка, сериализуема под сейв). В main.ts: NPC-манекены (Character speed 0) + Interactable-точки (radius 5), выбор графа — квест-стадия → repeat → dialogueFirst; в диалоге мир замер; onEffect — giveItem/takeItem в Inventory, toast/custom → тосты; HUD: трекер/сумка/ тосты; E = диалог/дальше, цифры = выбор варианта. Мост: npcInfo/talkTo/ dialogueInfo/dialoguePick/dialogueAdvance/dialogueAbort/setState/stateInfo. Гейт tools/checks/npc.mjs — 27 проверок (раскладка NPC, появление Клинта по флагу, выбор графа по стадиям включая сдачи flowers/tones ≥3, выбор ответа, сумка, флаги, трекер, motes_note по whenVar, мир замер, панель). Уроки: после сдачи квеста активная стадия (npc trader) перекрывает dialogueFirst этого NPC графом эпилога — порядок знакомств в сценарии гейта существенен; подстроки имён с падежами («Мила» в «Милой») в проверках текста ненадёжны. Экспортирован evalConditions из движка (ready-условия стадий — те же предикаты, что в графах).
  • Трек 17 — интерактив, пикапы, квест «Три цветка» ✅ (2026-09-10, коммит): контент v1 1:1 — src/data/interactables.ts (столбы у троп, моты, деревья-механизмы тонов, резонатор, сундук/записка/очаг, прилавок, верёвка колокола, Старая Машина; реакции по when с флагами/vars, requiresItem → lockedText, once → used:), src/data/hazards.ts (накат прудов/разъезда), src/areas.ts + interactables/hazards/ harvestTile (цветки — только пруды). Рантайм: src/interactables.ts (resolveInteractable → apply через sink: флаги → вар → предмет → used → custom → тост с прогрессом), src/props.ts (пропы строго в тайл 3×3: мот — искра, столб/сундук/записка/верёвка/резонатор; clearProp при once), src/flowers.ts (FlowerQuest: сбор/посадка/пересев/зелень; правки — воксель в центре тайла + журнал FlowerEdits по областям, переживает пересборку мира в setArea). В main.ts: кандидаты «E» с приоритетом v1 (npc → click-переход → интерактив → цветок; радиус 4.5 вокселя, цветы — окно ±2 тайла по isFlower), накат — тост при входе + ход ×0.5 без полотна (масштабирование вектора движения), катсцены — стек шагов {dur, fn?, cam?} с глайдом камеры (мир замер, детерминизм в stepTicks); custom-эффекты: диалоговые — plant_flowers отложен до onFinish (v1 pendingCustom), интерактивные — sink.onCustom (lull_machine → тост). Мост: interactInfo (kind/near/target/flower), voxAt, flowerTiles. Гейт tools/checks/interact.mjs — 27 проверок (реакции/ветки объектов, накат ×2 с полотном, сбор цветка, Машина: заперто → усыплена → спит, катсцена сдачи: герой стоит, камера у башни на середине глайда, 3 тайла цветут, кадр изменился). Уроки: квест надо взять ДО сдачи (questDialogueFor по активной стадии — без elder_first сдача не открывается, зависший выбор блокирует interact); vars.<ключ> до первой записи undefined (не === 0); setTimeout-тосты в headless не тикают и копятся (чистые скриншоты — отдельным прогоном); тайлы зелени башни — тропа (PATH=1), а raiseTowerGreen сажает только на GRASS/ASH (v1-faithful — зелень у башни не всходит и в v1).
  • Трек 18 — время суток и атмосфера ✅ (2026-09-10): часы суток (GameClock, 1 сек = 1 игровая минута, старт 8:00 — порт v1 1:1), кривая день/ночь из движка (dayNightFactor + dayNightAmbient, пик золотого часа 0.45 в 18:54) через новые API рендера: setNight(k, tint) (hemisphere → tint, фон = tint, солнце × (1 − 0.92·k)) и setDayAmbient(color) (дневная база области; тёмные интерьеры гасят hemisphere и солнце по яркости — аналог multiply-ambient v1), addPointLight (distance = radius×UNIT, decay 1.6). Игра: данные освещения 8 областей v1 1:1 (ambient/nightAmbient/duskColor/sources с whenFlag/notFlag/whenNight), GameLighting (diff по id, мерцание flickerFactor, лампа героя HERO_LAMP в темноте/ночью, MAX_LIGHTS 24), пепел лугов (AshParticles, 36 вокселей, детерминированный дрейф от elapsed), темы областей (GameAudio: playTheme с кроссфейдом, тема только у лугов — v1 1:1). Мост: clockInfo/setClock/lightingInfo/ audioInfo/setAsh. Гейт tools/checks/atmosphere.mjs — 18 проверок (кривая ночи по часам, источники по флагам/ночи, мерцание, кадр ночи темнее, пепел дрейфует, очаг hearth_low → hearth_high, отсвет башни по rang_tower, тема лугов / тишина в интерьере). Вне скоупа (отложено): туман прудов, тёмные ауры наката (darkSpot), виньетка, локальные амбиент-слои. Уроки: страница в гейте живёт в реальном времени до setManual (время уплывает — фиксировать setClock в начале); пепел — живой слой кадра (гейты кадр-равенств зовут setAsh(false) + stepTicks перед скриншотами); интерьеры без nightAmbient — только setDayAmbient; пик золотого часа t = 18 + 0.45×2 = 18:54, не 18:48.
  • Трек 19 — акт 2 ✅ (2026-09-10): контент акта 2 уже лёг с треками 16-17 (диалоги Клинта, квест resonator, интерактивы тон-деревьев и Машины) — трек добавил рантайм и фауну: Клинт появляется по act2_hook (спавн при перезаходе), сдача резонатора — катсцена настройки (камера к резонатору → звон → роща отвечает → тост), эпилог machine_hook, усыпление Машины — катсцена «правильного звона» (камера к устью), олени — воксельная FaunaSystem v1 1:1 (брожение по соседним тайлам, подход на звон, детерминированный Rng). Мост: faunaInfo/startleFauna/ setCam/cutsceneInfo/tileCenter + тулза tools/shot.mjs — оперативный скриншот участка карты с состоянием сюжета (агентный просмотр). Гейт act2.mjs — 27 проверок. Грабли: questTracker показывает первую активную стадию первого квеста (bells) — квест резонатора в трекере не виден до сдачи цветов (v1-faithful); тосты катсцен — в финальном шаге (гейтам stepTicks до конца); pkill -f убивает свою сессию — зомби-vite только по PID. Вне скоупа: sfx-звуки реакций (поле sound v1), боевой звон как триггер оленей (мост startleFauna до переноса боя).
  • Трек 20 — скрипты как данные + агентный просмотр (ЗАКРЫТ 2026-09-10): катсцены — третий повтор паттерна «стек шагов {dur, fn?, cam?}» в main.ts — поднят в движок жанронезависимо (packages/engine/src/core/cutscene.ts: CutsceneRunner — стек шагов, глайд камеры по инъекции CutsceneCamera, глайд по времени после инкремента — последний тик достигает цели точно; действие шага может перезапустить сцену; validateCutscenes — числовая валидация каталога, 8 юнит-тестов). Игра даёт данные (src/data/cutscenes.ts: метки точек камеры CUTSCENE_CAMS тайлами v1, реестр CUTSCENES — hand_in / tune / machine_lull) и валидацию при старте (resolveCutscenes(actions) — неизвестная метка/действие → throw, как validateDialogue). Мост: playCutscene(id) + каталог в cutsceneInfo; dry-run гейт tools/checks/cutscenes.mjs (13 проверок: каждая сцена доигрывается stepTicks-ом до конца и возвращает камеру к герою). Грабли: 40 тиков = 0.67 с — ещё первый шаг (dur 0.8–1.2 с): прогресс проверять t > 0, не step >= 1. Полноценную событийную систему (триггеры onEnter/onFlag → скрипты) не заводить до третьего кейса, который не ложится на квест-стадии.
  • Трек 21 — сборка (ЗАКРЫТ 2026-09-10): сейв полного состояния и меню закрыты раньше срока раундом полировки (ниже). Полный прогон: 11 гейтов демки + 6 гейтов игры — все зелёные на текущем коде, 295 юнит-тестов. Сравнительные скриншоты с v1 (зеркальные тулзы v1/apps/game/tools/compare-shot.mjs и v2/apps/game/tools/compare-shot.mjs): 5 общих участков (поляна лугов, пруды, Звенец у колодца, дом Ирвина, роща у резонатора) — герой в одном тайле, день 10:00, листы попарно v1|v2 (/tmp/cmp_sheet_*.png). Итог сверки: раскладка мира, объекты, переходы и подсказки «E» совпадают 1:1 (карты конвертированы без искажений); допустимые расхождения референса — v2-камера крупнее (~2×, UNIT=3), тон тёмных областей светлее (пруды/роща/дом без сумеречного затемнения v1), вода бледнее, интерьер дома беднее деталями (полки/камин v1 — пропы строго в тайл 3×3).
  • Трек 22 — масштаб мира UNIT 3→5 (ЗАКРЫТ 2026-09-10, фидбек: «персонажи крупнее деревьев и соразмерны домам»): 1 тайл v1 теперь 5×5 вокселей (герой 11 ≈ 2 тайла ростом — дерево ~2× выше героя, дом в ~1.5×). Все генераторы переписаны под тайл 5×5 и новые высоты (дерево 17–23, дом 12×14–18×9, башня 21, Машина 12×17×9, колодец, кусты/валуны), пропы и олень подросли соразмерно; стена интерьера 8 (ниже героя — комнаты читаются). Скорости: герой 2.1 юнита/с (v1 1.75
    • фидбек «быстрее»), олени 0.45/1.0 юнита/с. Камера viewSize 40 — тот же охват 8 тайлов, что был при UNIT=3. Высота мира 20→26. Гейты: дубль tileCenter в гейтах был с хардкодом ×3 — заменён на ×5+2.5, подходы ±3→±5, диапазон камеры катсцены tune пересчитан. Все 6 гейтов игры зелёные, 295 тестов. Грабля: центр тайла при нечётном UNIT — tile*UNIT + 2, не 1.5; рельсы — нить d=0..UNIT-1 (была с дырками); spotVoxel тайла — tile*5+2.
  • Трек 23 — масштаб UNIT 5→10 + детализация героя + частицы из мира (ЗАКРЫТ 2026-09-10, фидбек после прогона: «частицы летают вместе с персонажем, как приклеенные»; «персонажи низкодетализированные, не читаются; объекты всё ещё сильно меньше»): 1 тайл v1 = 10×10 вокселей (герой 22 ≈ 2.2 тайла, дом ~4 тайла — вдвое выше героя, дерево ~3.6). Частицы (пепел, мотыльки) отвязаны от камеры: мировой якорь + дрейф, окно камеры заворачивает (wrap) — при движении героя стоят на месте. Генераторы под тайл 10×10 (ствол 2×2, дерево ~30–38, дом 24×30–40×18, башня 47, Машина 24×34×18), высота мира 52, стены 13. Манекен движка переписан: 5×11×3 → 11×21×4 (голова-куб, торс, руки/ноги 2×2 — силуэт читается; 220 вокселей; тест связности — BFS по 6-связности вместо лесенки-сорта). Герой 2.1 юнита/с (stride 12.8 — темп v1), камера viewSize 80, радиусы интеракта 15. Гейты переведены (t*10+5, ±10, диапазон камеры tune 125..255): 6 игры + 11 демок зелёные, 295 тестов.
  • Трек 24 — дизайн персонажей и объектов, персонажи мельче (ЗАКРЫТ 2026-09-10, фидбек: «задизайнить персонажей и объекты, а персонажей и мобов сделать меньше»): герой 22 → 16 вокс (~1.6 тайла). Манекен движка 11×21×4 → 9×16×4 (180 вокс) и новое API — mannequin(palette?: MannequinPalette): палитра {torso/legs/head/arms} красит слоты, форма не зависит (палитра на model.palette); тип экспортируется из движка. У героя и NPC свои палитры (NpcDef.colors): герой — светлый лик/ржаво-красный плащ, Ирвин — седина/зелёный плащ, Мила — каштан/сарафан, Клинт — шапка/медный фартук; олень ~10 вокс (ноги 4). Объекты: дом — двускатная крыша коньком вдоль X + деревянная дверь на видимом фасаде (z=D−1; z=0 от камеры невидим — изометрия показывает фасады +x/+z) + труба; дерево — крона с профилем (низ вздут, верх сходит на нет; ель ярусами). Гейты аним-листа под мелкий манекен (diff фаз 50→40, подъём стопы 0.8→0.7). 6 гейтов игры + 11 демок зелёные, 296 тестов. Тулза crop-zoom.mjs — зум-кроп снимка. Грабли: UNIT=10 чётный — центр тайла tile*UNIT + 5; ствол дерева 2×2 (1×1 на этом масштабе — щепка); в проверках гейтов легко оставить старый +5 (сравнение возврата камеры) — искать grep-ом + 5) после смены масштаба.
  • Раунд полировки (2026-09-10, «улучшим игру технически и визуально»): недоделки визуального слоя трека 18 + техполировка. Меню (Esc) + автосейв/«Продолжить» (движковый SaveManager, localStorage rpg2:; GameSave — полный снапшот сюжета: область/тайл, флаги+вары, сумка, часы, правки цветков — применяются до setArea, мир строится цветущим); виньетка опасности + пелена наката (screenFx.ts, DOM); частицы v1 Atmosphere (ambientParticles.ts: мотыльки лугов, дымка прудов — клочья якорятся на клетки воды); шиммер воды прудов (waterShimmer.ts: редкие блики-вспышки по фазам sin). Мост: ambientInfo, menuInfo, startGame(load); страница стартует в меню — всем гейтам и shot.mjs сначала startGame(false). Грабли: addVoxelCloud после пересоздания облака возвращает castShadow=true — гасить после каждого update; z-fighting копланарных instanced-кубов с world-мешем (блик на воде мерцает на кромках пруда от порядка отрисовки, кадр-равенства падают) — сдвиг EPS 0.03 из плоскости; зомби-vite переживает process.exit тулзы — kill по PID из ss -tlnp.
  • Трек 25 (2026-09-10, «персонажи — не ходячие кубики, покажи максимум дизайна персонажей»): манекен движка 9×16×4 → 14×24×8 «путник» (~2.4 тайла UNIT=10, 498 вокс): торс силуэтом (юбка плаща шире, пояс, грудь, плечи), рукава с кистями кожи, сапоги с мысками, шея, голова с лицом — глаза в отдельном слоте EYE, нос выступает к +Z, шапка волос. Палитра 8 слотов: MannequinPalette {skin?, hair?, torso?, legs?, arms?, boots?, belt?, eyes?}. Главная грабля: камера смотрит сверху под ~55° — луч к фасаду проходит через воксель уровнем выше и той же z-колонкой, шапка волос на весь фасад прятала глаза (снапшоты зелёные!) — череп ступенькой: лицо до y21, шапка назад до z4. Ирвину тёплая кожа в тон седине сливалась в «череп». 6 гейтов игры + 11 демок зелёные, 297 тестов; визуальная итерация по shot.mjs --at 16,28 + crop-zoom.mjs.
  • Трек 26 (2026-09-10, «гигант; натяните текстуру и сделайте меньше вокселей»): деталь — текстурой, не геометрией. Формат моделей — слой tiles/tex: тайль = пиксель-арт на грани вокселя (VoxelTile, тексель = слот палитры — перекраска красит и текстуры), tex — id тайля на воксель; валидация числовая. Рендер — addVoxelCloud(voxels, tiles): атлас-канвас (ячейка = 6 суб-граней тайля, 0 — белая), инстанс-атрибут aTileId + шейдерная инъция (грань куба из нормали); воксель с тайлем белый, цвет дают тексели. Манекен 14×24×8 → 10×16×4 (164 вокс): лицо — арт 8×8 текселей (бахрома, глаза, рот) тайлями 2×2 на фасаде головы, волосы — макушка/затылок; правило ракурса ~55° сохранено. Игра: BODY_SIZE [5,16,5], stride 9.6; порог маха anim-гейта 1.0→0.8 (ноги вдвое короче). 6 гейтов игры + 11 демок зелёные, 300 тестов.

  • Трек 27 (2026-09-11, «воксели должны быть частью модельки и поворачиваться вместе с ней — на диагонали торчат углами»): поворот персонажа стал жёстким — PosedVoxel.rotY (опциональный поворот самого куба), Character.visual() ставит rotY = yaw на каждый воксель, рендер строит матрицу инстанса makeRotationY(rotY) + setPosition (раньше крутились только позиции — кубы оставались осями-выровненными и «пилили» силуэт). Олень без изменений: четверти оборота держат сетку на осях. Проба apps/game/tools/probe-diag.mjs (диагональ, yaw 3π/4). 6 гейтов игры + 11 демок зелёные, 301 тест.

  • Трек 28 (2026-09-11, «лоад-скрин, сейвы, инвентарь; баг — застревание при выходе из дома; дев-панель с просмотром объектов»): пять подзадач. Баг: дома штампуются шире тайла — тайл входа мог быть внутри стен; safeEntry в main.ts ищет ближайшую свободную позицию спиралью (collides). Лоад-скрин #loading до первого кадра. Слоты сейвов в меню: auto + 3 ручных, сохранить/загрузить/удалить, мета с временем (мост menuInfo.slots, saveTo/loadFrom/saveDelete). Панель сумки по I (#bag, мир стоит; мост bagInfo/toggleBag). Движок — ModelViewer (src/render/modelViewer.ts): орбитальная камера поверх VoxelRenderer, клипы риги, turntable, зум, статы modelStats (юнит-тесты чистой орбиты и статов). Дев-панель игры (F3, devview.ts): каталог 14 объектов (персонажи со скинами, природа, строения, олень), вьювер с паузой клипа и селектом скинов; мир стоит. Гейт apps/game/tools/checks/panels.mjs (21 проверка). Открытый вопрос: эффекты у объекта в вьювере (частицы/свет) — не делались, отложено. 6+1 гейтов игры + 11 демок зелёные, 303 теста.