Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (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>
5.7 KiB
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.5–8.5 → 7.5–10.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): до
ближнего боя агент мог стоять внутри мишени, и удар не читался вообще.