# CLAUDE.md — движок v2 (воксельный)

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

Направление и решения — `docs/plan.md` (в корне репо), разделы «Уроки
черновика → видение движка v2» и «Направление v2». Открытые вопросы v2 —
дописывать туда, не решать молча.

## Что v2 есть (решено)

- **Воксельный движок** как продолжение пиксель-арта: рендер сцены в низкое
  разрешение (nearest-апскейл), палитра вокселей = арт-библии v1
  (`docs/art-style.md`), shadow map + воксельный AO.
- **three.js — только низкоуровневый рендер-бэкенд** (роль Pixi в v1); ядро
  движка о бэкенде не знает — рендер за узким интерфейсом.
- **Формат моделей**: внутренний JSON (валидация агентом) + импорт/экспорт
  `.vox` (MagicaVoxel).
- **Генерация моделей**: процедурные генераторы кодом (основной путь),
  image-to-3D + вокселизация с апрув-гейтом, ручной MagicaVoxel для героев;
  визуальный гейт — turntable-листы.
- **Анимация = данные**: рига + кривые ключевых кадров в JSON, процедурная
  локомоция; валидация числовая (непрерывность цикла, контакт стопы) +
  снапшоты поз в агентный мост.
- **Агентный мост и CLI-проверки — часть ядра v2**, не надстройка. Демка —
  изометрия с фиксированной камерой; фокус работы — движок и инструментарий.

## Уроки v1, обязательные к соблюдению (docs/plan.md)

1. Все позиции/направления — в одном мировом пространстве; проекция на
   границе рендера. Не смешивать пространства (главный источник багов v1).
2. Визуальная подсистема считается готовой только со скриншот-гейтом, не
   только снапшот-ассертами.
3. Чистая математика — отдельно от вьюх, тестируется без рендера.
4. Не оставлять подключённый, но неиспользуемый путь; удалять или связывать.

## Команды (из `v2/`)

```bash
npm install          # установка зависимостей (workspaces)
npm test             # юнит-тесты (Vitest)
npm run typecheck    # tsc --noEmit (движок + демка)
npm run check:fast   # typecheck + тесты
node apps/demo/tools/checks/render.mjs   # скриншот-гейт рендера (Chromium, /tmp/v2_render_*.png)
node apps/demo/tools/checks/physics.mjs  # скриншот-гейт физики (/tmp/v2_physics_*.png)
npx vite --port 5299                     # демка (в apps/demo), http://localhost:5299
```

## Структура

- `packages/engine` (`@rpg/engine`) — ядро:
  - `src/voxel/grid.ts` — воксельная сетка (Uint8, 0 = пусто, иначе индекс палитры);
  - `src/voxel/mesher.ts` — culled-меш + классический AO 0..3 (схема 0fps,
    флип диагонали); `meshVoxels` для сетки, `meshRegion` — область по
    функции доступа; чистая математика, тестируется без рендера;
  - `src/world/chunk.ts` — чанк 16×H×16; `src/world/world.ts` — мир-словарь
    чанков (get/set через границы, dirty-трекинг: правка боковой кромки
    помечает соседа); `src/world/worldMesher.ts` — `meshChunk` через
    `world.get`, шов чанков бесшовный (culled и AO через границы);
  - `src/render/voxelRenderer.ts` — three.js-бэкенд: ортокамера диметрии 2:1,
    dirLight + shadow map + hemisphere, низкое разрешение (480×270);
    `addVoxelCloud` — instanced-кубы для поз анимации (не выровнены по
    сетке; обновление позы без пересборки геометрии);
  - `src/math/rng.ts` — детерминированный Rng (mulberry32): next/int/range/chance/pick;
  - `src/models/format.ts` — формат модели (JSON: size + плотный base64 +
    слоты-палитра), encode/decode/validate (числовая валидация агентом);
  - `src/models/generators.ts` — процедурные генераторы: `treeModel` (варианты
    0 круглая / 1 колонна / 2 ель), `boulderModel` (union сфер, свет на
    верхушках столбцов); (вид, сид) → VoxelModel, детерминировано;
  - `src/models/stamp.ts` — `stampModel`: штамповка модели в мир с клиппингом
    (half-open прямоугольник + вертикаль мира), возвращает число вокселей;
  - `src/models/vox.ts` — импорт/экспорт .vox (MagicaVoxel): чанки
    MAIN/SIZE/XYZI/RGBA (PACK>1 — ошибка, неизвестные чанки пропускаются
    с детьми), оси (x,y,z)_engine = (x,z,y)_vox, RGBA ровно 256 записей;
    при экспорте слоты модели сжимаются в подряд 1..N;
  - `src/models/paletteMap.ts` — `remapSlots(model, palette)`: перекладка
    слотов модели на палитру сцены (точный hex или ближайший RGB,
    при равенстве — меньший слот); нужен после импорта .vox;
  - `src/models/mannequin.ts` — `mannequin()`: эталонная модель 5×11×3
    с ригой (root → legL/legR/armL/armR, pivot в связочном пространстве);
  - `src/animation/mat.ts` — аффинная математика без three (Vec3/Mat3/Affine,
    порядок Эйлера X→Y→Z, rotateAround); чистая, тестируется без рендера;
  - `src/animation/rig.ts` — рига: `makeRig` (привязка явная или `bindNearest`
    по ближайшему pivot), `validateRig` (родители раньше детей, длина и
    согласованность привязки), `poseVoxels(rig, model, pose)` — деформация;
  - `src/animation/clip.ts` — клипы: ключи {t, кость: rot/pos}, `samplePose`
    (линейный семплинг, заворот t для циклов), `validateClip` (границы,
    монотонность, замкнутость цикла), `footMinY` (контакт стопы);
  - `src/animation/locomotion.ts` — `walkClip(params)`: процедурный walk-цикл
    (ноги в противофазе, руки против одноимённой ноги, корень приседает
    дважды за цикл и качается вбок); детерминирован, проходит валидацию;
  - `src/physics/body.ts` — физика тела: AABB (pos = центр низа, vel, size,
    grounded) против вокселей (`SolidWorld` — точечная выборка get, чистая
    математика), `makeBody`/`stepBody` (полунеявный Эйлер), ось-раздельное
    разрешение X→Z→Y с зажимом к граням вокселей (пол — ровно на грань,
    EPS-зазор), сабстепы (< 0.9 вокселя за подшаг) против туннелирования;
  - `tools/agent-lib.mjs` — браузерный каркас проверок (launchBrowser с
    опциональным viewport/
    startDevServer/openGame/AgentClient/Checks), swiftshader в headless;
  - `tools/png.mjs` — PNG-кодек (colorType 6 и 2 — скриншоты Chromium RGB).
- `apps/demo` — демо-полигон: `src/main.ts` (мир 6×6 чанков 96×96 «пепельный
  луг», деревья/валуны — из генераторов через `stampModel`, дом — ручной путь,
  ремешинг по dirty-чанкам), `tools/checks/render.mjs` — скриншот-гейт:
  нет pageerror, меш собран, тени ползут (утро/вечер), апскейл кратен 2×2,
  многокрасочность, 36 чанков, постройка+мешинг < 2 с (замер ~0.5 с),
  `setVoxel` на шве меняет кадр, 12+ деревьев всех 3 вариантов, генератор
  детерминирован и варианты различаются, `.vox`-штамп меняет кадр (13 проверок).
  Мост демки: `window.__agent.setSun(k)`/`stats()`/`setVoxel(x,y,z,c)`/`chunkStats()`/`modelStats()`/`stampTree(v,x,z,seed)`/`stampVoxB64(b64,x,z)` (парсинг → `remapSlots` на палитру сцены → штамп)/`perf()`
  /`bodySpawn(x,z,vx,vz)` (тело-манекен в воздухе y=6 с walk-анимацией и
  поворотом по движению)/`bodyStep(n)` (n шагов физики с фиксированным
  dt=1/60)/`bodyInfo()` (pos/vel/grounded/size/walkT)/`bodyRemove()`.
- `apps/demo/tools/checks/physics.mjs` — скриншот-гейт физики (7 проверок):
  спавн в воздухе, падение на землю y=2 ровно на грань, фаза walk-цикла
  растёт в движении, стена дома останавливает по грани
  (pos.x ≈ 44 − w/2 − eps), кадр изменился от движения, манекен рисуется
  (после `bodyRemove` кадр возвращается к пустому месту).
- `apps/demo/turntable.html` + `src/turntable.ts` — turntable-лист: все варианты
  генераторов (деревья/валуны) × 4 ракурса на подставках 8×8×2; мост
  `sheetInfo()` (геометрия листа для расчёта ячеек — viewSize как у камеры!)
  и `setModels(on)` (скрыть модели для референсного кадра). Канвас в левом
  верхнем углу — пиксели
  скриншота считаются от (0,0). Гейт `tools/checks/turntable.mjs`: проекция
  ячеек той же матрицей, что камера, эллипс 48×42 в ромбе подставки,
  5 проверок (ячейки непустые, варианты попарно различаются).
- `apps/demo/anim.html` + `src/anim.ts` — аним-лист: 8 кадров walk-цикла
  манекена (4×2, фазы i/8) на подставках; клип — `walkClip`, позы —
  `samplePose`→`poseVoxels`, рисуются `addVoxelCloud`. Мост: `sheetInfo()`,
  `poseStats(i)` (числовой снапшот позы: углы, качание корня, `footMinY`
  обеих стоп), `clipCheck()` (validateClip через мост), `setModels(on)`.
  Гейт `tools/checks/anim.mjs`: 8 проверок — картинка (ячейки видны,
  соседние фазы различаются, порог 50) + числа через мост (клип замкнут,
  противофазы ног/рук, контакт стоп в i=0/i=4, приседание и качание корня).
  Фазы 0 и 4 двухшагового цикла совпадают — это норма, не баг.

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

- **Знак Y в изо-направлении камеры**: вектор «target → камера» должен иметь
  y = +1. С минусом камера встаёт под землю — сцена рендерится «из-под»,
  верхние грани без света и всё почти чёрное (снапшоты при этом зелёные —
  ловится только скриншот-гейтом).
- **Скриншоты headless Chromium — RGB (colorType 2), без альфы** — декодер
  `tools/png.mjs` v2 это умеет (у v1 умел только RGBA).
- **three без типов**: нужен `@types/three` в devDependencies движка.
- **`VoxelGrid.count()` — метод, а не геттер**: в тестах легко написать
  `g.count` без скобок и получить функцию (дважды ловили).
- **Дробные координаты в `VoxelGrid.set` молча игнорируются** (индекс
  Uint8Array 4.5 — no-op): центры ячеек считать целыми (`floor(pitch/2)`).
- **Скриншот = вьюпорт браузера**, не страница: канвас выше дефолтного
  вьюпорта 960×540 обрезается снизу; `launchBrowser({viewport})` и канвас
  без центрирования — иначе все координаты гейта плывут.
- **Node-скрипты гейтов не импортируют `@rpg/engine`** (TS-исходники с
  extensionless-импортами): фикстуры движка (модели, .vox-байты) генерятся
  один раз tsx-ом и вкладываются в скрипт гейта готовыми (base64).
- **Точка правки для проверки «кадр изменился» должна быть в кадре камеры**:
  штамп вне вью дал `placed=12` при `voxDiff=0` — счётчик зелёный, кадр нет.
- **Заголовок .vox-чанка — 12 байт** (id + contentSize + childrenSize);
  обрыв в заголовке ловить границей в `id()`, иначе голый RangeError
  из DataView вместо понятной ошибки.
- **Привязка вокселей индексируется по сетке, а posed-массив — только
  занятые воксели**: сопоставлять через `PosedVoxel.index`, не позицией
  в массиве (footMinY из-за этого дважды бы соврал).
- **Voronoi-привязка по pivot семантически неверна** для внутренних
  вокселей: торс у плеча ближе к кости руки, чем к корню. Генератор
  привязывает части явно; `bindNearest` — только для чужих моделей.
- **sheetInfo/мост должен отдавать РЕАЛЬНЫЙ viewSize камеры**: в turntable
  стояло 30 при камере 32 — гейт проходил только за счёт запаса эллипса.
- **Соседние фазы walk-цикла (45°) на мелком манекене дают ~110 px
  разницы** — порог гейта «фазы различаются» 50, не 150 как у вариантов
  моделей; а фазы 0 и 4 совпадают полностью (симметрия двух шагов).
- **Аргумент `VoxelWorld(n)` — это ВЫСОТА чанка, а не число чанков**
  (`new VoxelWorld(4)` = плоский мир высотой 4): `set` вне y-границ молча
  возвращает false. Тест-мир с «потолком» на y=7 требует `new VoxelWorld(16)`,
  иначе тесты падают загадочно («тело не упало»).
- **`Vec3` в mat.ts — readonly** (`readonly [n,n,n]`): мутирующее состояние
  физики (Body.pos/vel) — обычные кортежи `[number,number,number]`; Vec3 —
  только на входах/выходах чистых функций.
- **Спавн тела в тесте/демке проверять на пересечение AABB с миром**:
  тело 3×10×3, заспавненное краем под плитой потолка, цепляет её воксель
  и «мгновенно приземляется» — тест падает с непонятной позицией.

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

- Для задач из 3+ шагов веди список задач и иди автономно.
- Комментарии и докстринги — по-русски; хелперы до ~70 строк.
- Изменения API — с доками в том же коммите.