refactor: рабочая база на three.js + меню/правила уровней + доки

Полный рефакторинг проекта в стабильную расширяемую базу рогалика.

Почему: после разнесения single-file на модули потерялся вызов генерации
карты (RoomMap не генерировал комнаты) → игра падала на старте; баг скрывался
тем, что Bun-бандлер не проверяет типы.

Архитектура:
- Логика игры (src/core/) полностью отделена от рендера: без three.js и DOM,
  тестируется без браузера.
- Рендер мира на three.js с ортокамерой (2D-вид); HUD/миникарта — 2D-канвас поверх.
- Фиксированный игровой цикл 60 Гц + интерполяция (раньше скорость зависела
  от частоты кадров).
- Seeded-RNG, ввод через абстрактные «намерения» (InputState).

Возможности:
- Стартовое меню с выбором уровня (Esc → меню).
- Конфигуратор уровней: LevelRules + 5 пресетов (размер карты, плотность/сила
  врагов, HP, фиксированный seed).
- Тема внешнего вида (render/theme.ts) — задел под кастомные ассеты.

Качество:
- 27 юнит-тестов ядра (генерация, симметрия дверей, коллизии, спавн, правила).
- Два круга adversarial-ревью; исправлено 6 реальных багов
  (кнокбэк сквозь стены → софт-лок; незакрываемая сокровищница; фикс-сид после
  рестарта; перенос ввода между забегами; нет source maps; неточности в доках).
- Документация: README, docs/ARCHITECTURE.md, CLAUDE.md, docs/HOWTO.md.
- dist/ исключён из гита; bun.lock зафиксирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 14:07:06 +03:00
co-authored by Claude Opus 4.8
parent 46dc9f2fad
commit 0684368ac7
60 changed files with 2759 additions and 3364 deletions
+161
View File
@@ -0,0 +1,161 @@
# HOWTO — рецепты доработки
Практические инструкции «как сделать X». Каждый рецепт перечисляет все файлы,
которые нужно тронуть. После любой правки — `bun run check` и проверка в браузере
(`bun run dev`). Общие правила — в [`CLAUDE.md`](../CLAUDE.md).
---
## 1. Покрутить баланс (скорость, HP, урон, число врагов)
Всё в одном файле — **`src/config.ts`**. Например:
- игрок быстрее: `PLAYER.speed`;
- больше HP у босса: `ENEMY_STATS.boss.hp`;
- больше врагов в комнате: `SPAWN.normalMin` / `SPAWN.normalExtra`;
- крупнее данжен: `MIN_ROOMS` / `EXTRA_ROOMS` / `MAP_RADIUS`.
Код менять не нужно. Это и есть смысл `config.ts`.
---
## 2. Добавить новый тип врага (например, `tank`)
1. **`src/core/types.ts`** — добавь в union: `export type EnemyType = 'normal' | 'fast' | 'boss' | 'tank';`
2. **`src/config.ts`** — строка характеристик в `ENEMY_STATS`:
```ts
tank: { size: 40, hp: 8, speed: 0.7, damage: 2 },
```
3. **`src/core/systems/spawner.ts`** — реши, когда он спавнится (логика выбора типа
в начале цикла). Напр. с шансом: `rng.chance(0.15) ? 'tank' : rng.chance(ENEMY.fastChance) ? 'fast' : 'normal'`.
4. **`src/render/ThreeRenderer.ts`** — цвет в `COLOR` и выбор цвета/геометрии в
`syncEnemies` (квадрат `unitPlane` или круг `unitCircle`).
ИИ, урон, отбрасывание, мигание при попадании — общие, их трогать не нужно.
Добавь тест в `tests/spawner.test.ts`, если ввёл особое правило спавна.
---
## 3. Добавить новое поведение врага (особый ИИ)
Сейчас все враги ведут себя одинаково (преследование игрока) в
`Game.updateEnemies` (`src/core/Game.ts`). Чтобы развести поведение:
- по `e.type` внутри `updateEnemies` развилкой (просто, для 2–3 типов), **или**
- вынеси ИИ в `src/core/systems/ai.ts` как функции `update<Type>(enemy, player, room)`
и диспетчеризуй по типу (чище, когда типов много).
Держи это в `core/` (без рендера) и старайся писать чистыми функциями — их легко
покрыть тестом.
---
## 4. Добавить оружие / третий режим боя
Режимы — это `MODE_RANGED = 0` / `MODE_MELEE = 1` и тип `CombatMode = 0 | 1`.
Для третьего режима:
1. **`src/config.ts`** — константа `MODE_X = 2` и блок баланса.
2. **`src/core/types.ts`** — расширь `CombatMode` (`0 | 1 | 2`).
3. **`src/core/Game.ts`** — в `handleAttack` добавь ветку создания нужного снаряда/
эффекта; в `consumeActions` смена оружия циклом по всем режимам.
4. **`src/render/HudOverlay.ts`** — подпись/иконка режима.
5. Если это новый вид снаряда — заведи сущность в `src/core/entities/` и обновляй
её в `Game.step` (по образцу `Projectile`/`updateTears`), а в `ThreeRenderer`
добавь её отрисовку.
---
## 5. Добавить тип комнаты (например, `shop`)
1. **`src/core/types.ts`** — добавь в `RoomType`.
2. **`src/core/world/RoomMap.ts`** — правило назначения типа в `generate()`.
3. **`src/core/systems/spawner.ts`** — сколько врагов (часто 0).
4. **`src/core/Game.ts`** — если врагов 0, комната уже авто-зачищается при входе
(см. `enterRoom`); особая логика комнаты — здесь же.
5. **`src/render/HudOverlay.ts`** — цвет на миникарте и подпись (`drawMinimap`/`drawHud`).
---
## 6. Поменять управление
**`src/input/KeyboardController.ts`** — метод `poll()` (маппинг клавиш на
`InputState`) и набор `PREVENT` (клавиши, у которых гасим поведение браузера).
Логику игры это не затрагивает — она читает только `InputState`.
Геймпад/тач: сделай новый контроллер с тем же `poll(): InputState` и подставь его
в `main.ts`.
---
## 7. Добавить уровень (пресет правил) в меню
**`src/core/rules.ts`** — добавь объект в массив `PRESETS`. Он сразу появится
кнопкой в стартовом меню (меню строится из `PRESETS`). Например:
```ts
{
id: 'swarm',
name: 'Орда',
description: 'Очень много слабых быстрых врагов.',
map: { minRooms: 10, extraRooms: 4, mapRadius: 4 },
player: { maxHp: 6, speed: 3.2 },
enemies: { densityMul: 2.5, fastChance: 0.9, hpMul: 0.5, speedMul: 1.2, bossHpMul: 1 },
},
```
- `seed` (необязательный) фиксирует данжен — одинаковый забег каждый раз.
- Нужен новый «рычаг» (например, шанс сокровищниц)? Добавь поле в `LevelRules` и
читай его там, где раньше брал константу из `config.ts` (генерация — `RoomMap`,
спавн — `spawner`). Тест на влияние правил — в `tests/game.test.ts`.
---
## 8. Кастомные ассеты (своя тема / текстуры)
Сейчас вид мира — это цвета в **`src/render/theme.ts`** (`Theme` + `DEFAULT_THEME`).
- **Своя палитра:** сделай ещё один объект `Theme` и передай его в
`new ThreeRenderer(canvas, myTheme)` в `main.ts`. Сейчас рендер создаётся один
раз при старте с темой по умолчанию и переиспользуется между забегами, поэтому
чтобы **выбирать тему по правилам уровня**, нужно либо перенести
`new ThreeRenderer(...)` внутрь `startGame(rules)` (и `dispose()` предыдущий),
либо добавить рендеру метод `setTheme(theme)`.
- **Перейти на спрайты/текстуры:** расширь `Theme` полями с путями к картинкам
(см. комментарий-задел в `theme.ts`), загрузи их через `THREE.TextureLoader` в
`ThreeRenderer` и положи текстуру в `material.map` соответствующих мешей вместо
(или вместе с) `color`. Логика игры при этом НЕ меняется — это чисто рендер.
---
## 9. Поменять или нарастить рендер (вплоть до 3D)
Рендер изолирован за интерфейсом **`src/render/Renderer.ts`** (`render(game, alpha)`
+ `dispose()`). Варианты:
- **Доработать вид** (спрайты, текстуры, частицы) — внутри `ThreeRenderer`.
- **Сделать 3D** — поменяй `OrthographicCamera` на `PerspectiveCamera`, добавь
свет и 3D-меши. Мир рисуется по тем же координатам сущностей из `Game` — логику
менять не нужно.
- **Другой рендер целиком** (например, Canvas2D для отладки) — реализуй `Renderer`
и подставь в `main.ts`. Ядро не трогается вообще.
Помни про управление ресурсами (правило 6 в `CLAUDE.md`).
---
## 10. Отладка
- В консоли браузера доступен `game` — текущий `Game`. Примеры:
```js
game.player.hp = 99 // бессмертие на тест
game.curRoom.enemies.length // сколько врагов в комнате
game.reset() // новая карта
```
- **Детерминизм:** чтобы воспроизвести конкретную карту, задай `seed` правилам —
проще всего поставить `seed: 42` нужному пресету в `core/rules.ts` (см. пресет
`daily`). Тот же seed — та же генерация и спавн, в т.ч. после рестарта.
В тестах можно передать свой ГПСЧ **вторым** аргументом: `new Game(rules, new Rng(42))`
(первый аргумент — правила, не ГПСЧ).
- **Тесты ядра** гоняются мгновенно и без браузера: `bun test`. Логику отлаживай
тестом, а не кликами.