squad-proto/docs/02-architecture.md
z.kirill c1831ac370 Статусы стихий и реакции (M22)
Стихии сами по себе были только множителем урона: «верный ствол бьёт втрое
больнее». Этого мало, чтобы они стали ЗНАНИЕМ, ради которого заводились —
множитель одинаково работает у того, кто понимает систему, и у того, кто взял
ствол посильнее.

Статусы и реакции добавляют слой, который работает только у первого: наложить
ДВЕ разные стихии на одну цель — это решение о составе отряда, а не о
характеристиках оружия.

  sim/status.h      правила, потолки стаков, реакции, числа
  sim/reactions.*   наложение и запуск, единственное место
  UpdateStatuses    тик горения и сгорание стаков, из UpdateTargets

СИЛА РЕАКЦИИ СЧИТАЕТСЯ ОТ ГЛУБИНЫ, А НЕ ОТ УРОНА ОРУЖИЯ. Считай она от оружия —
на сотом этаже стала бы округлением, и весь слой стихий превратился бы в
украшение. От глубины она растёт сама, но только у того, кто умеет её запускать.

ТРИ ОГРАНИЧИТЕЛЯ, И ВСЕ ПРОВЕРЯЮТСЯ

  потолок стаков   один стак не значит ничего, полный набор меняет бой
  откат на цели    вторая пара внутри 1.2 c даёт 0.0 урона
  запрет рекурсии  урон ОТ реакции не кладёт статусов

Третий не теоретический: без него цепочка «реакция бьёт по площади -> урон
кладёт стихию -> новая пара -> реакция» уходит в самоповтор на первом же
скоплении. Именно так ломаются все игры с триггерами.

Четыре реакции из десяти. Остальные ждут систем, которых нет (крит,
воскрешение, притяжение), и дописывать их «пока не работает» значило бы завести
шесть мёртвых веток.

Два отступления от документа, оба сознательные: кислота и дуга увеличивают
получаемый урон вместо снятия брони и метки крита (у тварей нет ни брони как
числа, ни крита), SUPERCOND кладёт стаки пустоты вместо отдельного среза
сопротивлений — пустота в этой игре и означает «защита не работает».

Шаг симуляции 2.9 мкс из бюджета 16666: статусы на 64 целях ничего не стоят.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 05:45:17 +03:00

174 lines
15 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)
element.h стихии, сопротивления, единственная формула урона
status.h статусы стихий, реакции, три ограничителя
reactions.h/.cpp наложение стихий и запуск реакций
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/
tools.cpp балансовые инструменты: squad_proto --tool
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).