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

8.5 KiB
Raw Blame History

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, 10-atmosphere.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.