squad-proto/docs/02-architecture.md
z.kirill 4382af2322 Документация: границы движка, работа через модель, дизайн игры до лейтгейма
18-engine.md    границы Tile2D, проекции 2D, формат проекта, второй проект
  19-agent.md     инструменты --tool, скиллы, терминал в редакторе
  20-gdd.md       акты, петли, рост отряда, закон масштаба, бюджет 200 часов
  21-damage.md    семь стихий, статусы, десять реакций, формулы, триггеры
  22-loot.md      редкости, аффиксы, артефакты, зачарования, материалы
  23-endgame.md   Бездна, мутации, лидерборды, 262 ачивки

Плюс правки 01/02/07/15/16/17 под переехавшие пути и новые правила забега.

Дизайнерская часть держится на одном законе: угроза растёт квадратично, сила
отряда — линейно с затуханием, разрыв закрывается ЗНАНИЕМ, а не числами. Из
него выведены и экономика опыта (41 млн против 44 млн дохода), и то, почему
реакции считаются от глубины, а не от урона оружия, и почему в Бездне
множитель HP разрешён, а во всём авторском контенте запрещён.

.claude/commands: скиллы /tile2d, /room, /floor — правила этого движка,
которые нельзя вывести из кода. Главное записано первым: не отчитываться об
успехе без прохода --tool validate.

Числа в этих документах — вход для инструментов, а не украшение. Расхождение
между таблицей и выводом инструмента означает, что неправ документ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:07:45 +03:00

170 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 02 — Архитектура
## Карта модулей
Целей сборки три, и граница между ними — линковка, а не договорённость
([15-engine-and-editor.md](15-engine-and-editor.md)):
```
tile2d (STATIC) engine/ — окно и цикл, кадр, проекции 2D, тайлы, комнаты, проекты
├── squad_proto.exe игра Descent: src/ + projects/descent/
├── tile2d_editor.exe редактор комнат: editor/, инструмент ДВИЖКА
└── tile2d_walk.exe пример: samples/walk/, второй потребитель движка
```
Движок лежит в своём каталоге не для красоты: состав `tile2d` определяется тем,
куда положен файл, и попасть в движок случайно нельзя
([18-engine.md](18-engine.md)).
```
engine/ ДВИЖОК Tile2D: про правила боя не знает ничего
config.h размеры кадра, тайла и карты — то, что нельзя менять без пересборки
app.h/.cpp окно, кадр, fixed timestep. Цикл остаётся у приложения
math.h Vec2, отрезки, DistancePointToSegment, углы. Header-only
hash.h детерминированные хеши: один и тот же пол в каждом прогоне
projection.h/.cpp ЕДИНСТВЕННОЕ место, где движок знает про изометрию
view2d.h камера, список отрисовки, сортировка по глубине
framebuffer.h/.cpp софтверный буфер RGBA8, блит (в т.ч. со светом), аддитивные примитивы
pixel.h/.cpp ромб, обводка силуэта, ShadeColor
microfont.h/.cpp растровый шрифт 3x5: номер этажа формой не покажешь
theme.h сколько тем отделки и как они называются
tileset.h/.cpp пол и стены по темам; ими рисуют и игра, и редактор
catalog.h/.cpp каталог размещаемых сущностей (данные, не enum)
room.h/.cpp формат комнаты-шаблона, набор комнат
level_gen.h/.cpp сборка этажа из комнат по сиду. ЕДИНСТВЕННЫЙ RNG
progression.h/.cpp что означает глубина N; коэффициенты приходят из проекта
project.h/.cpp манифест проекта: пути, вид отображения, кривая
tilemap.h/.cpp карта, DDA-рейкаст, circle-vs-AABB
tilemap_draw.h/.cpp показать карту тайлсетом; дефолт для проектов без света
sfa_library.h/.cpp чтение библиотеки SpriteForge (см. docs/12-assets.md)
text_parse.h разбор строчных текстовых форматов движка
editor/ РЕДАКТОР: отдельный exe, линкуется ТОЛЬКО с движком
main.cpp окно, ключи, --check
editor.h/.cpp холст, кисти, палитра, таблицы весов, предпросмотр
samples/walk/ ПРИМЕР: минимальное приложение на движке (docs/18-engine.md)
projects/ СОДЕРЖИМОЕ. Игра — один из проектов, а не единственный
descent/ project.txt, rooms/, catalog/ — эта игра
hollow/ второй проект: вид сверху, своя кривая
src/
main.cpp окно, ввод, главный цикл, fixed timestep. Игровой логики нет
game.h/.cpp склейка: карта + отряд + мишени + пули + метрики, порядок шага
tuning.h ВСЕ числа ИГРЫ + таблица runtime-подкрутки
core/
input.h InputState — ввод, влияющий на симуляцию
render/
view.h/.cpp ВСЁ render-only состояние + RenderGame(const Game&, ...)
sprites.h/.cpp процедурная генерация всех спрайтов при старте
hud.h/.cpp полосы отряда поверх кадра: ни одной буквы текста
scene.h/.cpp проход по полу + общий список сущностей и стен с сортировкой
lighting.h/.cpp свет и туман войны (см. docs/10-atmosphere.md)
fx.h/.cpp трассеры, вспышки дула, искры попаданий
post.h/.cpp напряжение (Dread) и слитый пост-проход
noise.h единственное место render-only псевдослучайности
sim/
loadout.h класс оружия, параметры ствола и клинка, фазы удара
agent.h/.cpp агент, слои движения (seek/separation/targetPush/wallAvoid)
squad.h/.cpp якорь и поводок, строй, ADVANCING/HOLDING, сборка слоёв
target.h/.cpp мишени, респавн, отдача от удара и возврат на место
bullet.h/.cpp снаряды, коллизии с тайлами и капсулами
events.h кольцевой журнал боевых событий: единственный канал к эффектам
items.h каталог предметов и инвентарь бойца (docs/13-combat.md)
enemy.h/.cpp поведение тварей: заметил -> идёт -> замах -> удар
spawn_catalog.h/.cpp единственный перевод id движка в EnemyKind игры
ai/
firing_solver.h/.cpp LaneClear, выбор цели, решатель огневых позиций
lane_registry.h/.cpp активные линии огня и секторы клинков, импульс расступания
melee.h/.cpp цель ближнего боя, подход вплотную, чистота сектора
ui/
menu.h/.cpp меню, пауза, экран отряда, поражение (второе место с raylib)
debug/
overlay.h/.cpp оверлей F1F4 поверх кадра (raylib-примитивы)
metrics.h/.cpp FRIENDLY_HITS, FIRE UPTIME, SETTLE TIME, журнал попаданий по своим
harness.h/.cpp прогоны --headless и --accept
```
Против структуры из спека добавлены: `game.h/.cpp`, `core/input.h`,
`render/scene.h/.cpp`, `debug/harness.h/.cpp`, хоррор-слой
(`render/view`, `lighting`, `fx`, `post`, `noise`, `sim/events.h`) и ближний бой
(`sim/loadout.h`, `ai/melee.h/.cpp`). Зачем — [08-decisions.md](08-decisions.md),
[10-atmosphere.md](10-atmosphere.md) и [11-melee.md](11-melee.md).
## Правила зависимостей
1. **`RenderGame` принимает `Game` по константной ссылке.** Поэтому свет, трассеры и
тряска камеры физически не могут изменить симуляцию — это проверяет компилятор,
а не внимательность. Всё render-only состояние живёт в `View`, которое
headless-прогоны не создают вообще.
2. **`ai/` не знает ничего** про рендер, проекцию и ввод. Только `core/`, `sim/`
и тайлмап движка.
3. **`sim/` и тайлмап работают в декартовом мире**, тайл = 1.0 юнита. Про то,
ромбами картинка или квадратами, знает одна `engine/projection.h`; всё
остальное считает через неё ([18-engine.md](18-engine.md)).
4. Ввод в экранных осях переводится в мировое направление ровно в одном месте —
`Game::Step` через `v2d::ScreenDirToWorld`. Дальше по симуляции экранных осей нет.
5. `tuning.h` — числа ИГРЫ, и от движка он зависит ровно одним включением
`engine/config.h`. Обратной стрелки нет: движок не включает из игры ничего,
и это проверяет компилятор — `src/` просто не лежит в путях включения `tile2d`.
6. **Боевой режим не касается приёмки.** Твари, снаряжение и урон по бойцам
живут под `Game::combatMode`; `--accept` и `--headless` его не включают
([13-combat.md](13-combat.md)). Мишень `EnemyKind::DUMMY` ведёт себя ровно
как до этой вехи.
7. **Движок не знает про правила боя.** `engine/` и `editor/` не
включают ничего из `sim/`, `ai/` и `ui/`, и это проверяет компоновщик:
редактор просто не линкуется с ними. Содержимое игры доходит до редактора
данными — каталог сущностей и файлы комнат ([15-engine-and-editor.md](15-engine-and-editor.md)).
8. **Формат `.sfa` знает только `engine/sfa_library`.** Остальной рендер видит
обычный `Sprite`, и ему безразлично, процедурный тот или собранный. Игра без
собранных ассетов запускается и считает те же цифры: базовый набор спрайтов
процедурный и ни от одного файла на диске не зависит ([12-assets.md](12-assets.md)).
## Порядок одного шага симуляции
`Game::Step(dt)` — фиксированный порядок, важен именно он:
| # | Что | Почему здесь |
|---|---|---|
| 1 | пресет строя из ввода | до всего остального |
| 2 | мишени: респавн, отдача, возврат на место | состояние мира на начало шага |
| 3 | якорь + поводок + состояние отряда (`ADVANCING`/`HOLDING`) | движение зависит от состояния |
| 4 | **движение агентов** (слои 6.7) | реестр линий берётся с прошлого шага, задержка 1/60 с |
| 5 | ручное прицеливание, назначенная цель | до выбора целей |
| 6 | **оценка линий огня** и **выбор цели клинка** по новым позициям | целеуказание, `hasLane`, `melee.inReach` |
| 7 | пересборка реестра: линии огня + секторы клинков | для расступания и оценки кандидатов |
| 8 | решатель огневых позиций / подход клинка вплотную | решатель — только в `HOLDING` и только без линии |
| 9 | доворот ствола (у клинка на замахе ось зафиксирована) | визуальная часть |
| 10 | **атака**: огонь, затем взмах | позиции те же, что в п.6 |
| 11 | пули | коллизии со стенами и капсулами |
| 12 | метрики | снимок конца шага |
Ключевое: пункты 6 и 10 разделены только доворотом ствола, позиции между ними
не меняются. Поэтому отрезок, который проверил `LaneClear`, и отрезок, по которому
полетела пуля, — это буквально один и тот же отрезок. Для клинка то же самое
верно про сектор: проверенный в п.6/8 и рассечённый в п.10 — один и тот же.
## Детерминизм
- Фиксированный шаг 1/60, накопитель времени, максимум 5 шагов за кадр.
- Ни одного вызова RNG в симуляции. Уровень и расстановка мишеней захардкожены.
Уровень, собранный из комнат (`--rooms`), этого не меняет: RNG крутится один
раз, в `engine/level_gen.cpp`, до первого `Game::Step`, и полностью определён
сидом. `--accept` и `--headless` набор комнат не подключают вообще.
- Вырожденные случаи разрешаются по индексу слота, а не случайно
(например, расталкивание двух агентов, стоящих в одной точке).
- Камера обновляется в рендере по реальному времени кадра и на симуляцию не влияет.
Поэтому `--headless` воспроизводит один и тот же прогон и годится как тест.
## Рендер и симуляция
`RenderGame(const Game&, View&, ...)` не может изменить симуляцию — это гарантирует
подпись. Позиции агентов и пуль интерполируются `Lerp(prevPos, pos, alpha)`, камера
сглаживается по `frameDt` и округляется до целого пикселя, чтобы картинка не дрожала.
Единственный канал в обратную сторону — кольцевой журнал `sim/events.h`: симуляция
пишет в него боевые события, эффекты читают. Правила его не читают, в headless
передаётся `nullptr`. Подробности — [10-atmosphere.md](10-atmosphere.md).