Files
Binding-Fignyaac/docs/HOWTO.md
T
mayatnikovandClaude Opus 4.8 0684368ac7 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>
2026-06-18 14:07:06 +03:00

162 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`. Логику отлаживай
тестом, а не кликами.