# 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.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](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`): до ближнего боя агент мог стоять внутри мишени, и удар не читался вообще.