Newer
Older
rpg / docs / engine / practices.md

Практики работы агента с движком и игрой

Живой документ: каждая новая находка (грабля, удачный приём, экономящая время последовательность) пополняет этот файл в том же коммите, где она появилась. Структура — по ситуациям агента, не по подсистемам: нашёл свою ситуацию → скопировал последовательность шагов.

Ситуация: проверяю своё изменение

  1. npm run typecheck — быстрый отсев.
  2. npx vitest run <затронутый тест> — точечный прогон.
  3. node tools/agent.mjs check --pretty — полный прогон (typecheck + тесты + карты + карты-файлы равны генераторам + браузерная проверка инвариантов + сценарий переходов tools/checks/transitions.mjs).
  4. Смоук акта 1 (node tools/smoke-act1.mjs) — не сломал ли существующий геймплей.

Ситуация: добавляю локацию

  1. Генератор в tools/maps/gen.mjs (детерминированный сид) → npm run maps.
  2. AreaId + AreaDef в data/locations.ts: spawn/npcs/enemies/transitions.
  3. Экспорт через AREAS/areaOf и загрузка в BootScene по Object.keys(AREAS).
  4. Валидатор проверит спавн/врагов/переходы автоматически (agent:invariants): инвариант in-wall у врага — почти всегда реальный баг данных (враг поставлен на воду/дерево). Чинить данные, а не валидатор.
  5. Сценарий в tools/checks/: переход туда и обратно через walkTo + waitFor (готовый образец — tools/checks/transitions.mjs).

Ситуация: добавляю переход (дверь, портал, колодец)

  1. Один механизм на всё — TransitionDef в data/locations.ts: trigger: 'step' (наступил, выходы между областями) или 'click' (двери/колодцы; герой подходит к тайлу сам). Условия — requiresFlag/requiresItem + lockedText (тост).
  2. Карту грузит npm run maps; тайл-триггер может быть непроходимым (колодец) — клик-переход обрабатывается до маршрутизации движения.
  3. npm run maps при провале «fresh»-теста чинит файлы, но сначала прочти провал: расхождение «тайлов N, ожидалось W×H» = индекс за пределами карты в генераторе (разреженный массив дорос, см. граблю ниже).
  4. Проверка: tools/checks/transitions.mjs — шаг до перехода, клик-переход туда и обратно, снапшот s.transitions (триггеры и цели).

Ситуация: добавляю интерьер / интерактивный объект

  1. Карта интерьера: buildInteriorMap() в data/map.ts (рамка WALL, пол FLOOR, дверной проём снизу) — не рисуй рамку руками.
  2. Дверь внутрь — TransitionDef { trigger: 'click' } в AreaDef улицы; выход — TransitionDef { target: { kind: 'return' }, trigger: 'step' } в интерьере (проём). returnTo фиксируется в момент клика по двери (тайл героя), не в момент выхода — в сценарии храни тайл клика и сравнивай с ним после возврата.
  3. Объекты — InteractableDef в INTERACTABLES (data/interactables.ts): реакции по when (первая подходящая), once поднимает флаг used:<id>, эффекты — setFlags/setVar/gives, текст виден в s.lastToast.
  4. Валидатор сам проверит объекты в границах и переходы; полная проверка — tools/checks/interact.mjs (образец: вход → сундук → used → выход → возврат на тайл клика).
  5. waitFor моста видит полный снапшот игры (s.flags, s.inventory, s.interactables) — предикаты над прогрессом пиши против него, а не против сценических полей.

Ситуация: добавляю NPC/диалог

  1. NpcDef в data/npcs.ts, граф в data/dialogues.ts.
  2. Валидатор проверит: несуществующие next/choiceserror, недостижимые узлы → warn (сироты допустимы, но проверь, что это не забытая ветка).
  3. Проверка через мост: walkTo до соседнего тайла → tapTile(NPC)runDialogue() → прочитать flags/dialogue.text из снапшота.

Ситуация: упал смоук / проверка

См. agent.md «waitFor и переходы сцен». Частые причины, по частоте:

  1. Действие проглочено fade-переходом: begin() молча отбрасывает параллельный переход, сцена игнорирует клики при transitioning. Перед действием — waitFor('!s.transitioning').
  2. Клик по тайлу вне экрана: в смоук-скриптах с реальными кликами цель за краем канваса (виртуальный y > 270) клик мимо. Через мост не актуально (tapTile в юнитах), но в старых скриптах — целься в промежуточный тайл.
  3. walkTo в занятый тайл (NPC, высокий объект): тайл непроходим, маршрут не найден. Кликни в сам NPC-тайл (tapTile) — сцена подведёт героя сама.
  4. Гонка rAF и инъекции: инъекция и шаг должны быть в одном JS-стеке — высокоуровневые методы моста (tapTile/press/key/walkTo) уже атомарны; сырые inject* из страницы + отдельный step — гонка с rAF.
  5. s.error в снапшоте — мост поймал исключение, читай текст.

Ситуация: меняю API движка

  1. Обнови соответствующий docs/engine/*.md и этот файл (если появился новый приём) в том же коммите.
  2. Экспорт только через packages/engine/src/index.ts — под-пути движка импортировать нельзя.
  3. npm run typecheck ловит разрывы в обоих пакетах; тесты движка — в Vitest без браузера (Pixi-зависимости — через стабы, см. GameLoop.manual.test.ts).

Ситуация: пишу новый сценарий tools/checks/

  1. Каркас: import { startDevServer, openGame, Checks } from '../agent-lib.mjs'; export default async function ({ pretty }); вернуть c.finish({pretty}).
  2. Проверка: возвращает false или кидает исключение при провале (строка — это детали успеха!); удобнее c.expect(cond, msg, details).
  3. Ввод — через мост в юнитах (walkTo/tapTile/press), не через реальные клики по CSS-пикселям.
  4. Текст реакций мира читай из s.lastToast (текст + тик) — не из скриншотов.

Грабли среды (кратко, подробности в CLAUDE.md)

  • Assets.load без Assets.init() висит навсегда — грузи только через AssetLoader.
  • Headless Chromium: --autoplay-policy=no-user-gesture-required обязателен; ctx.resume() без него не резолвится.
  • Маяки консоли: [boot] ассеты загружены (BootScene), [location] <id> (вход в локацию) — на них строится openGame.
  • window.__agent только в DEV-сборке; для прод-сборки — VITE_AGENT=1 (когда понадобится).
  • Vite-алиас @rpg/engine — только regex; строковый перехватывает под-пути.
  • Запись мимо карты в генераторе не падает: tiles[at(x,y)] с индексом за W×H молча дорастит разреженный массив — ловится только тестом «fresh» («тайлов 583, ожидалось 560»). После правки генератора всегда npm run maps.
  • Асинхронный шаг в каркасе Checks: stepResult обязан await fn() — без await промис считается успехом (ok:true, ms:1, details:{}).