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:
2026-06-18 14:07:06 +03:00
co-authored by Claude Opus 4.8
parent 46dc9f2fad
commit 0684368ac7
60 changed files with 2759 additions and 3364 deletions
+22 -35
View File
@@ -1,50 +1,37 @@
import { spawn } from "bun";
/**
* Дев-сервер: бандлит src/main.ts → dist/main.js в watch-режиме и раздаёт
* dist на http://localhost:3000. index.html копируется из корня (единый
* источник правды) — правь его там, не в dist.
*/
import { spawn } from 'bun';
// Ensure dist/ has the HTML shell
await Bun.write(
"./dist/index.html",
`<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dungeon Crawl — Ranged / Melee</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0a0a;display:flex;justify-content:center;align-items:center;height:100vh;font-family:monospace;overflow:hidden;user-select:none}
canvas{display:block;border:1px solid #222;border-radius:2px;cursor:none}
</style>
</head>
<body>
<canvas id="game" width="880" height="660"></canvas>
<script src="main.js"></script>
</body>
</html>`
const builder = spawn(
// --sourcemap=linked: в DevTools брейкпоинты/стек указывают на твой src/*.ts,
// а не на собранный main.js. .map отдаётся из dist тем же сервером.
['bun', 'build', 'src/main.ts', '--outdir', 'dist', '--target', 'browser', '--sourcemap=linked', '--watch'],
{ stdout: 'inherit', stderr: 'inherit' },
);
// Start build watcher
const builder = spawn(["bun", "build", "src/main.ts", "--outdir", "dist", "--target", "browser", "--watch"], {
stdout: "inherit",
stderr: "inherit",
});
// Start dev server
const port = 3000;
const server = Bun.serve({
port,
async fetch(req) {
const url = new URL(req.url);
let path = url.pathname === "/" ? "/index.html" : url.pathname;
const file = Bun.file("./dist" + path);
return new Response(file);
// index.html отдаём прямо из корня — правки CSS/разметки сразу живые;
// бандл main.js и карты — из dist (их пишет watch-сборщик).
if (url.pathname === '/' || url.pathname === '/index.html') {
return new Response(Bun.file('./index.html'));
}
const file = Bun.file('./dist' + url.pathname);
if (await file.exists()) return new Response(file);
return new Response('Not found', { status: 404 });
},
});
console.log(`\n Dev server: http://localhost:${port}`);
console.log(` Watching: src/\n`);
console.log(`\n Dev server: http://localhost:${server.port}`);
console.log(' Watching: src/\n');
// Graceful shutdown
process.on("SIGINT", () => {
process.on('SIGINT', () => {
builder.kill();
server.stop();
process.exit(0);