- Рендер переведён в псевдо-3D: наклонная PerspectiveCamera, пол лежит плашмя, персонажи/враги — вертикальные спрайты-биллборды, стены с высотой. Починен баг: пол создавался без поворота и стоял вертикально → «вывернутая» перспектива. - Двери видны всегда: закрыты (засов) во время боя, открытый проём после зачистки. - render/assets.ts: процедурные текстуры (спрайты персонажей/врагов, пол, стены, двери, снаряд, тень) + эффекты оружия (вспышка из дула, искры, облачко гибели). - Светлее палитра — исправлено «тёмное на тёмном». - Игра переименована в «Биндим Фигняшку»; полная русификация UI (ДАЛЬНИЙ/БЛИЖНИЙ, ИГРА ОКОНЧЕНА, ПОБЕДА); клавиши-подсказки оставлены латиницей. - Ввод по event.code → WASD/Q/R работают в любой раскладке (вкл. русскую). - theme.ts урезан до реально используемых тинтов (bg/swing/flash). - docs/ASSET_BRIEF.md — бриф и промпт на полную художку; доки синхронизированы. - Ядро (src/core) не тронуто — раунд чисто render/UI. 27 тестов + типы зелёные. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
172 lines
10 KiB
Markdown
172 lines
10 KiB
Markdown
# 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. Кастомные ассеты (спрайты, текстуры, тема)
|
||
|
||
Графика (спрайты персонажей/врагов, пол, стены, двери, снаряд, эффекты, тень) рисуется
|
||
процедурно в **`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`. Логику отлаживай
|
||
тестом, а не кликами.
|