squad-proto/docs/02-architecture.md
z.kirill 7880f275bd Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).

Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.

Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.

Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».

Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.

Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.

Документация — CLAUDE.md как оглавление, содержание в docs/.

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

104 lines
8.5 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 — Архитектура
## Карта модулей
```
src/
main.cpp окно, ввод, главный цикл, fixed timestep. Игровой логики нет
game.h/.cpp склейка: карта + отряд + мишени + пули + метрики, порядок шага
tuning.h ВСЕ числа прототипа + таблица runtime-подкрутки
core/
math.h Vec2, отрезки, DistancePointToSegment, углы. Header-only
input.h InputState — ввод, влияющий на симуляцию
render/
view.h/.cpp ВСЁ render-only состояние + RenderGame(const Game&, ...)
framebuffer.h/.cpp софтверный буфер RGBA8, блит (в т.ч. со светом), аддитивные примитивы
sprites.h/.cpp процедурная генерация всех спрайтов при старте
iso.h проекция мир↔экран, камера, ключ сортировки по глубине
scene.h/.cpp проход по полу + общий список сущностей и стен с сортировкой
lighting.h/.cpp свет и туман войны (см. docs/10-atmosphere.md)
fx.h/.cpp трассеры, вспышки дула, искры попаданий
post.h/.cpp напряжение (Dread) и слитый пост-проход
noise.h единственное место render-only псевдослучайности
world/
tilemap.h/.cpp карта, генерация уровня, DDA-рейкаст, circle-vs-AABB
sim/
loadout.h класс оружия, параметры ствола и клинка, фазы удара
agent.h/.cpp агент, слои движения (seek/separation/targetPush/wallAvoid)
squad.h/.cpp якорь и поводок, строй, ADVANCING/HOLDING, сборка слоёв
target.h/.cpp мишени, респавн, отдача от удара и возврат на место
bullet.h/.cpp снаряды, коллизии с тайлами и капсулами
events.h кольцевой журнал боевых событий: единственный канал к эффектам
ai/
firing_solver.h/.cpp LaneClear, выбор цели, решатель огневых позиций
lane_registry.h/.cpp активные линии огня и секторы клинков, импульс расступания
melee.h/.cpp цель ближнего боя, подход вплотную, чистота сектора
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/`, `world/`.
3. **`sim/` и `world/` работают в декартовом мире** (top-down), тайл = 1.0 юнита.
Изометрия существует только в `render/` и в оверлее.
4. Ввод в экранных осях переводится в мировое направление ровно в одном месте —
`Game::Step` через `iso::ScreenDirToWorld`. Дальше по симуляции экранных осей нет.
5. `tuning.h` не зависит ни от чего; его включают все.
## Порядок одного шага симуляции
`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 в симуляции. Уровень и расстановка мишеней захардкожены.
- Вырожденные случаи разрешаются по индексу слота, а не случайно
(например, расталкивание двух агентов, стоящих в одной точке).
- Камера обновляется в рендере по реальному времени кадра и на симуляцию не влияет.
Поэтому `--headless` воспроизводит один и тот же прогон и годится как тест.
## Рендер и симуляция
`RenderGame(const Game&, View&, ...)` не может изменить симуляцию — это гарантирует
подпись. Позиции агентов и пуль интерполируются `Lerp(prevPos, pos, alpha)`, камера
сглаживается по `frameDt` и округляется до целого пикселя, чтобы картинка не дрожала.
Единственный канал в обратную сторону — кольцевой журнал `sim/events.h`: симуляция
пишет в него боевые события, эффекты читают. Правила его не читают, в headless
передаётся `nullptr`. Подробности — [10-atmosphere.md](10-atmosphere.md).