Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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>
8.5 KiB
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 оверлей F1–F4 поверх кадра (единственное место с 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,
10-atmosphere.md и 11-melee.md.
Правила зависимостей
RenderGameпринимаетGameпо константной ссылке. Поэтому свет, трассеры и тряска камеры физически не могут изменить симуляцию — это проверяет компилятор, а не внимательность. Всё render-only состояние живёт вView, которое headless-прогоны не создают вообще.ai/не знает ничего про рендер, изометрию и ввод. Толькоcore/,sim/,world/.sim/иworld/работают в декартовом мире (top-down), тайл = 1.0 юнита. Изометрия существует только вrender/и в оверлее.- Ввод в экранных осях переводится в мировое направление ровно в одном месте —
Game::Stepчерезiso::ScreenDirToWorld. Дальше по симуляции экранных осей нет. 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.