Ревью отметило устаревшие тексты после перехода на загрузку PNG: - assets.ts: заголовок описывал «процедурно, без внешних файлов» — теперь PNG из src/assets/<ключ>.png с процедурным фолбэком. - HOWTO §8: убран совет «заменить тело drawX() на TextureLoader» (уже сделано); актуальный путь — положить PNG с нужным именем в src/assets/. - README/ARCHITECTURE/ASSET_BRIEF: статус «подключено», корректный механизм (рантайм- загрузка по пути, не импорт-бандлинг). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
HOWTO — рецепты доработки
Практические инструкции «как сделать X». Каждый рецепт перечисляет все файлы,
которые нужно тронуть. После любой правки — bun run check и проверка в браузере
(bun run dev). Общие правила — в 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)
src/core/types.ts— добавь в union:export type EnemyType = 'normal' | 'fast' | 'boss' | 'tank';src/config.ts— строка характеристик вENEMY_STATS:tank: { size: 40, hp: 8, speed: 0.7, damage: 2 },src/core/systems/spawner.ts— реши, когда он спавнится (логика выбора типа в начале цикла). Напр. с шансом:rng.chance(0.15) ? 'tank' : rng.chance(ENEMY.fastChance) ? 'fast' : 'normal'.src/render/assets.ts— добавь ключ вSpriteKeyи ветку вAssets.sprite()(рисуется черезdrawCharacter(...)с твоими цветами).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.
Для третьего режима:
src/config.ts— константаMODE_X = 2и блок баланса.src/core/types.ts— расширьCombatMode(0 | 1 | 2).src/core/Game.ts— вhandleAttackдобавь ветку создания нужного снаряда/ эффекта; вconsumeActionsсмена оружия циклом по всем режимам.src/render/HudOverlay.ts— подпись/иконка режима.- Если это новый вид снаряда — заведи сущность в
src/core/entities/и обновляй её вGame.step(по образцуProjectile/updateTears), а вThreeRendererдобавь её отрисовку.
5. Добавить тип комнаты (например, shop)
src/core/types.ts— добавь вRoomType.src/core/world/RoomMap.ts— правило назначения типа вgenerate().src/core/systems/spawner.ts— сколько врагов (часто 0).src/core/Game.ts— если врагов 0, комната уже авто-зачищается при входе (см.enterRoom); особая логика комнаты — здесь же.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). Например:
{
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. Примеры: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. Логику отлаживай тестом, а не кликами.