# 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(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/assets.ts`** (класс `Assets`, кэширует текстуры three.js). - **Свои PNG вместо процедурных:** замени тело нужного `drawX()` на загрузку картинки `new THREE.TextureLoader().load(url)` и верни её из соответствующего геттера — рендер берёт текстуры по ключам, больше ничего менять не нужно. Полный список нужных ассетов с размерами и готовый промпт для дизайн-ИИ — в **`docs/ASSET_BRIEF.md`**. - **Цвета/тинты/фон** (не текстуры) — в **`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` (камера уже наклонная `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`. Логику отлаживай тестом, а не кликами.