squad-proto/docs/04-world.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

81 lines
5.7 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.

# 04 — Мир: карта, рейкаст, коллизии, мишени
## Тайлмап
[src/world/tilemap.h](../src/world/tilemap.h). Сетка 48×48, тайл `EMPTY` или `WALL`.
Тайл = 1.0 юнита, координаты сущностей — `float`.
**Вне карты считается стеной.** Это убирает граничные проверки из рейкаста и коллизий.
## Планировка уровня
Генерируется кодом, без RNG, полностью детерминированно (`Tilemap::Generate`).
```
y 0..1 сплошная рамка (2 тайла по всему периметру)
y 2..12 северная комната: кучка из 3 мишеней + колонны
y 13..15 стена-разделитель на всю ширину
проход A: x=10..11 проход B: x=30..31 (оба шириной 2 тайла)
y 16..45 центральная арена: кучка из 5 мишеней, колонны, огрызки стен, блок 2x2
x 12..14 западная стена-разделитель (y 20..44), проход: y=30..31
x 38..40 восточная стена-разделитель (y 18..40), проход: y=27..28
```
Отряд стартует в `(24, 26)` — открытая арена, кучка из 5 мишеней в радиусе оружия.
Колонны и огрызки стен раскиданы по всей карте намеренно: без них не проверить
ни LOS, ни обход. Три прохода шириной 2 тайла — это тест-кейс, где правило
«не стрелять сквозь своих» упирается в геометрию.
Северная кучка мишеней стоит **напротив прохода B** (`x=29.5/31.5`), так что из
коридора до неё есть чистая линия. Без этого проверить критерий «в узком проходе
отряд вытягивается в колонну, стреляет передний» было бы не на чем.
Кучка обязана быть **в пределах дальности от середины прохода** (`y≈14`), иначе
тест-кейс проверяет пустоту. При укорочении дальности до 7.2 её пришлось
подтянуть на два тайла к югу (`y` 5.58.5 → 7.510.5): ближняя мишень теперь в
~6.5 юнита. Меняешь `weaponRange` — пересчитывай и эти координаты.
## DDA-рейкаст
`Tilemap::RaycastBlocked(a, b, tHit)` — Amanatides & Woo, без аллокаций.
Возвращает, пересекает ли отрезок стену, и опционально параметр входа в стену `[0..1]`.
Используется в трёх местах:
- `LaneClear` — пункт «геометрия» (спек 6.3);
- пуля — поиск столкновения со стеной за шаг;
- оценка кандидатов решателя — там же, через `LaneClear`.
## Коллизии
`ResolveCircle(pos, r)` — circle vs AABB тайла: для каждой стены в охватывающем
прямоугольнике ищется ближайшая точка AABB и круг выталкивается по нормали.
Три итерации — из-за углов, где выталкивание одной стеной загоняет в другую.
Вырожденный случай (центр внутри тайла) выталкивается по ближайшей грани.
**Скольжение вдоль стен** делается в `IntegrateAgent`: после выталкивания из
скорости вычитается только нормальная составляющая, касательная остаётся.
Плюс к этому `WallAvoidForce` даёт касательную силу обхода — см.
[05-squad-ai.md](05-squad-ai.md), слой 4.
`CircleHitsWall(pos, r)` — быстрая проверка «в стене ли», для отбраковки кандидатов.
## Мишени
[src/sim/target.h](../src/sim/target.h). 12 штук, не атакуют и сами никуда не идут.
- Расстановка кучками: 5 (арена) + 3 (север) + 3 (запад) + 1 (восток).
- HP 500, урон пули 12 → 42 попадания. Урон клинка 130 → 4 удара.
- Умерла → респавн через 3 с на том же месте. Прототип работает бесконечно.
Мёртвая мишень не рисуется, не выбирается целью и не участвует в оценке покрытия.
**Отдача.** Удар в ближнем бою отбрасывает мишень (`meleeKnockback`), импульс
гасится (`targetKnockDamp`), и мишень возвращается на своё место `home`
(`targetReturnRate`). Без отдачи попадание в упор визуально не отличается от
промаха. Возврат обязателен: расстановка захардкожена, и уползание мишеней
поломало бы все замеры разом — респавн дополнительно ставит `pos = home`.
**Тело.** Мишень расталкивает агентов (`TargetPushForce`, `wTargetPush`): до
ближнего боя агент мог стоять внутри мишени, и удар не читался вообще.