# 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 оверлей 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](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).