Полный рефакторинг проекта в стабильную расширяемую базу рогалика. Почему: после разнесения 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>
44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
import { describe, it, expect } from 'bun:test';
|
|
import { spawnEnemies } from '../src/core/systems/spawner';
|
|
import { Room } from '../src/core/world/Room';
|
|
import { Rng } from '../src/core/rng';
|
|
import { OX, OY, TILE, DOOR, SPAWN } from '../src/config';
|
|
import { dist } from '../src/core/util';
|
|
|
|
describe('spawner', () => {
|
|
it('в обычной комнате врагов в ожидаемом диапазоне', () => {
|
|
const room = new Room(1, 0, 'normal');
|
|
for (let s = 0; s < 30; s++) {
|
|
const enemies = spawnEnemies(room, 'left', OX + 5 * TILE, OY + 5 * TILE, new Rng(s + 1));
|
|
expect(enemies.length).toBeGreaterThanOrEqual(SPAWN.normalMin);
|
|
expect(enemies.length).toBeLessThanOrEqual(SPAWN.normalMin + SPAWN.normalExtra);
|
|
}
|
|
});
|
|
|
|
it('в комнате-босс ровно один враг типа boss', () => {
|
|
const room = new Room(1, 0, 'boss');
|
|
const enemies = spawnEnemies(room, 'left', OX + 5 * TILE, OY + 5 * TILE, new Rng(1));
|
|
expect(enemies.length).toBe(1);
|
|
expect(enemies[0].type).toBe('boss');
|
|
});
|
|
|
|
it('в сокровищнице врагов нет', () => {
|
|
const room = new Room(1, 0, 'treasure');
|
|
const enemies = spawnEnemies(room, 'left', OX + 5 * TILE, OY + 5 * TILE, new Rng(1));
|
|
expect(enemies.length).toBe(0);
|
|
});
|
|
|
|
it('враги не спавнятся вплотную к двери входа', () => {
|
|
const room = new Room(1, 0, 'normal');
|
|
const door = DOOR.left;
|
|
const doorX = OX + door.cx * TILE + TILE / 2;
|
|
const doorY = OY + door.cy * TILE + TILE / 2;
|
|
for (let s = 0; s < 20; s++) {
|
|
const enemies = spawnEnemies(room, 'left', OX + 7 * TILE, OY + 5 * TILE, new Rng(s + 100));
|
|
for (const e of enemies) {
|
|
expect(dist(e.x, e.y, doorX, doorY)).toBeGreaterThanOrEqual(SPAWN.minDistFromDoor);
|
|
}
|
|
}
|
|
});
|
|
});
|