# 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/assets.ts`** — добавь ключ в `SpriteKey` и ветку в `Assets.sprite()`
   (рисуется через `drawCharacter(...)` с твоими цветами).
5. **`src/render/ThreeRenderer.ts`** — добавь строку в `enemyMatKey`, сопоставив новый
   `Enemy['type']` этому `SpriteKey` (без этого не пройдёт проверка типов).

ИИ, урон, отбрасывание, мигание при попадании — общие, их трогать не нужно.
Добавь тест в `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. Кастомные ассеты (спрайты, текстуры, тема)

Графика грузится из PNG в **`src/assets/`** (класс `Assets` в `src/render/assets.ts`
тянет `assets/<ключ>.png` через `THREE.TextureLoader`); если файла нет — рисуется
процедурный фолбэк на canvas, и игра не ломается.

- **Свой/новый ассет:** просто положи PNG с именем `<ключ>.png` в **`src/assets/`**
  (dev-сервер отдаёт их сразу, прод-сборка копирует в `dist/assets`). Код менять не нужно.
  Ключи = именам файлов (`player-ranged`, `enemy-boss`, `floor`, `door-closed`, `tear`,
  `heart-full`, `icon-ranged`, …); полный список и промпт для дизайн-ИИ — в **`docs/ASSET_BRIEF.md`**.
- **Запасной рисунок (фолбэк):** функции `drawX()` в `assets.ts` — правь их, только если
  нужен другой плейсхолдер на случай отсутствия PNG.
- **Цвета/тинты/фон** (не текстуры) — в **`src/render/theme.ts`** (`Theme` + `DEFAULT_THEME`).
- **HUD-ассеты** (сердечки `heart-*`, иконки `icon-*`) грузит `HudOverlay`; меню (`logo`,
  `menu-bg`) — `index.html`. Те же имена в `src/assets/`.

- **Своя палитра:** сделай ещё один объект `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` (камера
  уже наклонная `PerspectiveCamera`, сущности — биллборд-спрайты).
- **Усилить 3D** — для настоящего объёма замени `MeshBasicMaterial` (он без света)
  на `MeshStandardMaterial`, добавь источники света и объёмные меши вместо плоскостей.
  Мир рисуется по тем же координатам сущностей из `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`. Логику отлаживай
  тестом, а не кликами.
