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>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import type { Dir } from '../core/types';
|
||||
|
||||
/**
|
||||
* Снимок намерений игрока за один опрос ввода — абстракция над «железом».
|
||||
* Игровая логика (core/Game.ts) читает ТОЛЬКО это, ничего не зная про
|
||||
* клавиатуру. Захочешь геймпад или сенсор — просто сделай ещё один
|
||||
* контроллер, отдающий такой же InputState.
|
||||
*
|
||||
* Поля делятся на два вида:
|
||||
* • удерживаемые (move*, aimDir, attackHeld) — читаются каждый шаг симуляции;
|
||||
* • однократные «edge» (toggleWeapon, restart) — срабатывают один раз на нажатие.
|
||||
*/
|
||||
export interface InputState {
|
||||
moveX: number; // -1 влево, +1 вправо, 0 нет
|
||||
moveY: number; // -1 вверх, +1 вниз, 0 нет
|
||||
aimDir: Dir | null; // прицеливание стрелками (приоритетнее attackHeld)
|
||||
attackHeld: boolean; // атака «по ходу движения» (пробел)
|
||||
toggleWeapon: boolean; // сменить оружие (однократно)
|
||||
restart: boolean; // рестарт на экране конца игры (однократно)
|
||||
}
|
||||
|
||||
/** Нейтральный снимок — ничего не нажато. */
|
||||
export function emptyInput(): InputState {
|
||||
return {
|
||||
moveX: 0,
|
||||
moveY: 0,
|
||||
aimDir: null,
|
||||
attackHeld: false,
|
||||
toggleWeapon: false,
|
||||
restart: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Жмёт ли игрок в сторону dir (движением ИЛИ прицеливанием) — для переходов. */
|
||||
export function pressingDir(input: InputState, dir: Dir): boolean {
|
||||
switch (dir) {
|
||||
case 'up': return input.moveY < 0 || input.aimDir === 'up';
|
||||
case 'down': return input.moveY > 0 || input.aimDir === 'down';
|
||||
case 'left': return input.moveX < 0 || input.aimDir === 'left';
|
||||
case 'right': return input.moveX > 0 || input.aimDir === 'right';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user