Стихии сами по себе были только множителем урона: «верный ствол бьёт втрое больнее». Этого мало, чтобы они стали ЗНАНИЕМ, ради которого заводились — множитель одинаково работает у того, кто понимает систему, и у того, кто взял ствол посильнее. Статусы и реакции добавляют слой, который работает только у первого: наложить ДВЕ разные стихии на одну цель — это решение о составе отряда, а не о характеристиках оружия. 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>
174 lines
15 KiB
Markdown
174 lines
15 KiB
Markdown
# 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 оверлей 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).
|