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

5.7 KiB
Raw Blame History

04 — Мир: карта, рейкаст, коллизии, мишени

Тайлмап

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, слой 4.

CircleHitsWall(pos, r) — быстрая проверка «в стене ли», для отбраковки кандидатов.

Мишени

src/sim/target.h. 12 штук, не атакуют и сами никуда не идут.

  • Расстановка кучками: 5 (арена) + 3 (север) + 3 (запад) + 1 (восток).
  • HP 500, урон пули 12 → 42 попадания. Урон клинка 130 → 4 удара.
  • Умерла → респавн через 3 с на том же месте. Прототип работает бесконечно.

Мёртвая мишень не рисуется, не выбирается целью и не участвует в оценке покрытия.

Отдача. Удар в ближнем бою отбрасывает мишень (meleeKnockback), импульс гасится (targetKnockDamp), и мишень возвращается на своё место home (targetReturnRate). Без отдачи попадание в упор визуально не отличается от промаха. Возврат обязателен: расстановка захардкожена, и уползание мишеней поломало бы все замеры разом — респавн дополнительно ставит pos = home.

Тело. Мишень расталкивает агентов (TargetPushForce, wTargetPush): до ближнего боя агент мог стоять внутри мишени, и удар не читался вообще.