# 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 art                          # перегенерация пиксель-арта из tools/pixelart
npm run maps                         # перегенерация карт-файлов (tools/maps + encodeMap)
npm run agent:check                  # полный прогон проверок через агентный мост (JSON)
node tools/agent.mjs run tools/checks/xxx.mjs  # один сценарий проверки
node tools/agent.mjs snapshot --new-game       # снапшот игры (JSON)
node tools/smoke.mjs                 # смоук-тест в реальном Chromium (скриншот + консоль)
node tools/smoke-act1.mjs            # полный прогон акта 1 через агентный мост
node tools/smoke-ponds.mjs           # смоук перехода луга -> Серые пруды
node tools/smoke-zvenets.mjs         # смоук перехода луга -> Звенец
node tools/smoke-quest.mjs           # смоук диалога с Ирвином и сумки/журнала
```

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

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

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

- `packages/engine` (`@rpg/engine`) — жанронезависимый движок-библиотека. **Не должен знать ничего о RPG-контенте** (квесты, предметы, сюжет). Вся игра импортирует движок **только через `packages/engine/src/index.ts`** — другие внутренние пути движка импортировать нельзя.
- `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/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/src/scenes/` — BootScene (грузит ассеты и шрифт) → MenuScene (MenuList) → LocationScene; сцены меняются через `SceneManager.replace/push/pop` (опционально с fade).
- `apps/game/src/systems/` — геймплейные механики: движение героя (A* + плавный путь + анимация из атласа), диалоги (обёртка над DialogueRunner + DialogueBox), интерактивные объекты (`Interactables.ts` — резолв реакций, used-флаги, `InteractSink`).
- Флаги/переменные сюжета — в `GameState` (`game.state`), сериализуются в автосейв `autosave` (Esc в локации); настройки — `game.settings` (отдельный слот, не в сейвах).

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

- `tools/pixelart/gen.mjs` — генератор ассетов (PNG из кода, без зависимостей): `node 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)`.
- Палитра — только из `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` зависают на загрузке картинок** (колбэки декодинга не срабатывают) — для визуальных проверок использовать `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`: тёплые цвета — только жизнь; палитра — только из `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/`.