# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Язык общения

**Всегда общайся с пользователем на русском.**

## Команды

```bash
npm install                          # установка зависимостей (npm workspaces)
npm run dev                          # Vite dev-сервер с HMR (apps/game)
npm run build                        # прод-сборка игры
npm test                             # все тесты (Vitest)
npx vitest run packages/engine/src/map/__tests__/pathfinding.test.ts  # один файл тестов
npm run typecheck                    # tsc --noEmit для обоих пакетов
npm run guard                        # гвард границы движок/игра (импорты, JSON)
npm run check:fast                   # typecheck + guard + все тесты (~30 с, это же делает pre-commit хук)
npm run art                          # перегенерация пиксель-арта из apps/game/tools/pixelart
npm run art:lint                     # линтер арта: палитра/слоты/«мыло» по apps/game/assets
npm run aiart                        # пайплайн AI-атласов: gen/build/promote <id> (apps/game/tools/aiart)
node apps/game/tools/aiart/tryon.mjs <id>      # примерка AI-сборки в игре до promote (скриншот /tmp/rpg_tryon.png)
npm run audio                        # перегенерация WAV (apps/game/tools/audio/gen.mjs)
npm run maps                         # перегенерация карт-файлов (apps/game/tools/maps + encodeMap)
npm run agent:check                  # полный прогон проверок через агентный мост (JSON)
node apps/game/tools/agent.mjs run apps/game/tools/checks/xxx.mjs  # один сценарий проверки
node apps/game/tools/agent.mjs snapshot --new-game       # снапшот игры (JSON)
node apps/game/tools/smoke.mjs                 # смоук-тест в реальном Chromium (скриншот + консоль)
node apps/game/tools/smoke-act1.mjs            # полный прогон акта 1 через агентный мост
```

Dev-сервер по умолчанию на http://localhost:5173; для отдельного порта: `npm run dev -- --port 5199`.

## Архитектура

Monorepo из двух workspace-пакетов с **жёсткой границей API**:

- `packages/engine` (`@rpg/engine`) — жанронезависимый движок-библиотека. **Не должен знать ничего о RPG-контенте** (квесты, предметы, сюжет). Вся игра импортирует движок **только через `packages/engine/src/index.ts`** — другие внутренние пути движка импортировать нельзя. Границу проверяет гвард `npm run guard` (проба `guard:boundary` в `agent:check`).
- `apps/game` (`@rpg/game`) — сама RPG: контент, сцены, геймплейные системы.

### Ключевые решения, которые нельзя сломать

1. **PixiJS — только рендер-бэкенд.** Игровой цикл, сцены, ввод, камера, изометрия — собственные (`packages/engine/src/core`, `scene`, `input`, `render`). Pixi используется как низкоуровневый слой отрисовки спрайтов.
2. **Пиксель-арт pixel-perfect, резкий UI.** Логика — в виртуальном разрешении 480×270 (`Game.VIRTUAL_W/H`); CSS-размер канваса — виртуальное × **целое** (`computeScale` в `@rpg/engine`, движок сам слушает `window.resize`), а бэкинг-стор — в пикселях устройства (scale × dpr): текст/UI резкие, мир пиксельный (текстуры `AssetLoader` сэмплируются `nearest`). `roundPixels: true`, `antialias: false`. При изменении разрешения следи, чтобы CSS-масштаб оставался целым.
3. **Фиксированный шаг.** `GameLoop` (60 Гц update) + аккумулятор с ограничением 5 шагов/кадр; `InputManager.endTick()` очищает «just pressed» в конце каждого тика — не вызывать update вне цикла движка.
4. **Изометрия 2:1, мир в юнитах.** Источник истины для позиций — **мировые юниты** (float в плоскости изометрии, 1 юнит = 1 тайл): `worldToScreen`/`screenToWorld`/`worldDist`/`worldNorm` в `math/iso.ts`. Две «линейки»: точка проецируется анизотропно (`tileW/2` px по X, `tileH/2` по Y), скаляры (дистанция, радиус, скорость, высота) — через `unitsToPx` (`px = units·tileW`, 32 px/юнит). Округление до целого px — только на границе мир→экран. Тайловые координаты всюду именуются `{x, y}` (как `Grid` в pathfinding); для них есть мосты `tileToWorld`/`worldToTile`.
5. **Собственные классы движка с инъекцией хранилища.** `SaveManager` принимает `StorageLike` (в браузере — `localStorage`), чтобы тесты работали без DOM. Чистую математику (iso, A*, ECS) держи без зависимостей от Pixi — она тестируется в Vitest без браузера.

### Где что лежит

- `docs/llms.txt` — **точка входа для ИИ-агента**: карта всех док одной строкой на док.
- `docs/engine/` — **документация движка** (по-русски): `README.md` (архитектура и принципы), `getting-started.md`, `core.md`, `render.md`, `input.md`, `maps.md`, `ui-and-dialogue.md`, `cutscene.md`, `assets-audio-save.md`, `art-pipeline.md`, `recipes.md`, `agent.md` (агентный мост), `practices.md` (живой документ практик агента). При изменении API движка обновляй соответствующий файл и `practices.md` (если появился новый приём) **в том же коммите**.
- `docs/demo.md` — **внутренняя проектная дока среза**: матрица «подсистема движка → где показана в игре» + статус, боевая модель, архитектура, дорожная карта.
- `docs/plan.md` — **живой план развития**: актуальные задачи, проработка крупных направлений (AI-генерация спрайтов/аудио, ограничения железа). Новые планы вести здесь.
- `docs/world.md` — библия мира (сеттинг, локации, персонажи, сюжет). **Любой новый контент сверять с ней.**
- `docs/art-style.md` — арт-библия: палитра (32 цвета), размеры спрайтов, правила стиля, чеклист ассетов.
- `packages/engine/src/` — модули движка: `core/` (Engine, GameLoop, Tween, GameState, StateMachine, Settings, EventBus), `scene/` (SceneManager + fade-переходы), `render/` (Renderer, Camera, IsoDepthLayer, Particles, scale), `input/` (действия, геймпад, VirtualJoystick), `map/` (изометрия, A*, mapFormat, Tiled-импорт), `ui/` (PixelText/VT323, Panel, Button, MenuList, DialogueBox), `dialogue/` (DialogueRunner — view-агностик), `audio/` (шины master/music/sfx), `assets/` (AssetLoader + атласы), `save/`, `math/` (iso, rng, easing).
- `apps/game/src/data/` — весь контент: карта (`map.ts`), переходы и области (`locations.ts` — `TransitionDef`/`AREAS`), интерактивные объекты (`interactables.ts` — `INTERACTABLES`), диалоги (`dialogues.ts` — графы `DialogueGraph`), NPC (`npcs.ts`); `validate.ts` — runtime-валидация контента → `Invariant[]` (проверяется в `agent:invariants` и юнит-тестах).
- `apps/game/src/agent/` — контентный слой агентного моста: `snapshot.ts` (сборка слоёв), `GameAgent.ts` (`window.__agent`, только DEV).
- `apps/game/tools/` — **сценарии применения тулз под эту игру** (знают контент: палитру, тайлы, локации, сценарии акта): `agent.mjs` (диспетчер агентных проверок) + `lib.mjs` (маяк загрузки поверх движкового каркаса), `checks/`, `pixelart/` (`gen.mjs` + `palette.mjs`), `audio/gen.mjs`, `aiart/`, `maps/`, смоуки; `guards/boundary.mjs` — сценарий запуска гварда (`npm run guard`, входит в `agent:check`).
- `packages/engine/tools/` — **жанронезависимые тулзы движка** (ничего не знают об игре, палитра/маяк/корни — параметры): `png.mjs` (PNG-кодек), `canvas.mjs` (мини-канвас, ASCII-карты), `imaging.mjs` (кроп/квантизация/обзорный лист), `wav.mjs` (WAV-энкодер + синтез), `agent-lib.mjs` (браузерный каркас агентных проверок), `boundary.mjs` (правила гварда + тесты). Импорт из игры — только через `@rpg/engine/tools/*` (см. `exports` движка). Правило: тулза знает контент игры — ей место в `apps/game/tools`; нет — в движке.
- `apps/game/src/scenes/` — BootScene (грузит ассеты и шрифт) → MenuScene (MenuList) → LocationScene; сцены меняются через `SceneManager.replace/push/pop` (опционально с fade). LocationScene — только оркестратор вьюх: клик-роутинг вынесен в `systems/ClickRouting.ts` (`resolveClick` — чистый резолвер приоритетов + `InteractionRouter`), агентный мост — в `agent/SceneAgentView.ts`.
- `apps/game/src/systems/` — геймплейные механики: движение героя (A* + плавный путь + анимация из атласа), диалоги (обёртка над DialogueRunner + DialogueBox), интерактивные объекты (`Interactables.ts` — резолв реакций, used-флаги, `InteractSink`), маршрутизация кликов по миру (`ClickRouting.ts` — приоритеты: NPC → переход/заперто → интерактив → цветок → враг → движение).
- Флаги/переменные сюжета — в `GameState` (`game.state`), сериализуются в автосейв `autosave` (Esc в локации); настройки — `game.settings` (отдельный слот, не в сейвах). **Флаги/вары — только через реестры `data/ids.ts`** (`FLAGS`/`VARS`, типы `FlagId`/`VarId`, `usedFlag(id)` для used:<id>): строковое упоминание вне реестра ловится `validateReferences()` как error, незадействованный ключ — как warn.

### Пиксель-арт

- `apps/game/tools/pixelart/gen.mjs` — генератор ассетов (PNG из кода, без зависимостей): `node apps/game/tools/pixelart/gen.mjs`.
- Выход — `apps/game/assets/` (`tiles/*.png`, `chars/*.png`); загружается `AssetLoader` в BootScene, ключ = путь без расширения.
- Атласы (кадры героя): `gen.mjs` собирает `chars/hero_sheet.png` + `hero_sheet.json` (Pixi Spritesheet); загрузка — `assets.loadAtlas('chars/hero_sheet.json')`, кадры — `assets.frames(key, prefix)`.
- Палитра — только из `apps/game/tools/pixelart/palette.mjs` (она же в `docs/art-style.md`); тёплые цвета (B*/F*) — только «жизнь»: цветы, бронза, огонь.
- Имена файлов и размеры спрайтов фиксированы арт-библией — при замене сгенерированного арта нарисованным руками их не менять.
- Тайл «дерево» — высокий объект: `TileMapData.tall` c `TallSpec { height, ground }` (спрайт якорится низом в центр ромба, под ним рисуется ground-тайл).

### Известные грабли (уже собранные)

- **Pixi `Assets.load` без `Assets.init()` виснет навсегда** (без ошибок!) — `AssetLoader` делает init сам, не обходи его.
- **`app.renderer` недоступен до завершения `app.init()`** — `Renderer.setup()` ждёт init-промис; не трогай renderer раньше.
- **Изометрический «ромб» уходит в минус по X** (экранный bbox западного угла при 28×28 ≈ -448) — но границы камеры задаются в юнитах через `map.worldBounds` (`{0, 0, w, h}`), клэмп через проекцию сам учитывает отрицательный угол.
- **Vite-алиас `@rpg/engine` — только regex** (`{ find: /^@rpg\/engine$/, replacement: ... }`). Строковый алиас перехватывает и под-пути (`@rpg/engine/assets/...`), ломая их в `index.ts/...`.
- **Игровые PNG должны попадать в dist**: в `apps/game/vite.config.ts` стоит `publicDir: 'assets'`, а `resolveUrl` не добавляет префикс `assets/`. Без этого прод-сборка пустая.
- **Шрифт движка подключается `?url`-импортом** (`import fontUrl from '@rpg/engine/assets/fonts/VT323-Regular.ttf?url'`) — для этого в exports движка есть `"./assets/*"`. Обычный `new URL(...)` вне корня Vite не обрабатывает.
- **Атласы грузятся только через `assets.loadAtlas('<ключ>.json')`** — ключ с `.json` резолвится как есть, без `.png`. `Assets.load` на JSON вернёт Texture без `.textures` — не перепутай с `texture()`.
- **Fade-оверлей переходов должен быть поверх UI**: `SceneManager` поднимает его наверх `uiRoot` в `begin()` — сцены добавляют свои view позже, и «наивный» оверлей из `Engine.init()` оказывается под ними (карты просвечивают сквозь меню).
- **TweenManager.tick учитывает пересечение границы `delay`**: часть dt, приходящаяся на delay, вычитается из движения — не упрощай тик до последовательных прибавлений.
- **Headless-скриншоты с `--virtual-time-budget` зависают на загрузке картинок** (колбэки декодинга не срабатывают) — для визуальных проверок использовать `apps/game/tools/smoke.mjs` (puppeteer-core + системный Chromium, реальное время).
- **В headless Chromium `ctx.resume()` без флага `--autoplay-policy=no-user-gesture-required` не резолвится** — смоук-скрипты обязаны передавать этот флаг, а навигация не должна ждать `.then` у `audio.play`.
- **Одна клавиша маппится на несколько действий** (`bindActions` дополняет, а не перезаписывает): Space = `advance` в диалоге и `attack` вне его — не развести обратно нельзя, гардится уровнем сцены.
- **Координаты указателя и worldRoot — виртуальные пиксели** (480×270), экранные CSS-пиксели — это виртуальные × масштаб: `экран = мир + worldRoot.position` (перевод в CSS — домножением на scale). Цепочка клика по миру: `pointer px − worldRoot.position → screenToWorld → юниты → worldToTile`. Не смешивай пространства: позиция героя/врагов — юниты, вью — px.
- **Поворот героя (facing) — по экранным компонентам мирового смещения** (`worldToScreen(delta)`), не по мировым: экранные оси — мировые диагонали, кадры down/up/side соответствуют экранному направлению.
- **Клик по тайлу вне экрана не работает**: если целевой тайл за краем канваса (виртуальный y > 270 или x < 0), клик уходит мимо канваса — герой не двигается. В смоук-скриптах целиться в промежуточный тайл рядом с героем по направлению к цели (шаг по доминирующей оси).
- Мир/арт сверять с `docs/world.md` и `docs/art-style.md`: тёплые цвета — только жизнь; палитра — только из `apps/game/tools/pixelart/palette.mjs`.
- **Агентный мост** (`window.__agent`, DEV): предпочитай `snapshot`/`agent:check` скриншотам — состояние игры читается снапшотом, скриншот только для визуальных вопросов. Fade-переход молча глотает параллельные `begin()` и клики (`transitioning`) — перед действием `waitFor('!s.transitioning')`. Инъекция ввода и шаг — атомарны (один JS-стек): высокоуровневые `tapTile/press/key/walkTo` моста уже атомарны, сырые `inject*` из страницы + отдельный шаг — гонка с rAF. Подробности: `docs/engine/agent.md`, приёмы: `docs/engine/practices.md` (пополнять в том же коммите).

## Рабочие привычки

- Для задач из 3+ шагов веди список задач (todowrite/TaskCreate) и иди по нему автономно, не спрашивая подтверждения на каждый шаг.
- Подражай существующему стилю кода; комментарии и докстринги — по-русски; хелперы держи до ~70 строк.
- Плейсхолдеры графики рисуются `Graphics`-ом в коде (пока нет арта); настоящий пиксель-арт в будущем кладётся в `apps/game/assets/`.