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

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

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

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`/`choices` → `error`, недостижимые
   узлы → `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:{}).