# 13. Бой: оружие, твари, снаряжение, экраны Прототип из вех M0–M6 проверял ОТРЯД: строй, линии огня, правило «не бить своих». Противника там не было — были мишени, которые стоят и не отвечают. Эта веха достраивает вторую половину: твари, которые идут и бьют, каталог оружия, снаряжение и экраны вне боя. ![вылазка](raid.png) ## Два режима, и это важно | Режим | Что в мире | Кто им пользуется | |---|---|---| | боевой (по умолчанию) | твари 5 видов, на бойцах комплекты снаряжения | игра | | песочница (`--sandbox`) | прежние мишени, отряд без снаряжения | ручные проверки | | приёмка (`--accept`, `--headless`) | всегда песочница | замеры критериев | Разделение не косметическое. Броня несёт множитель скорости, а на скорости движения меряются критерии 11.4 и 11.5; твари бьют, а от урона меняется состав живых. Поэтому `Game::combatMode` выключен во всех прогонах приёмки, мишень `EnemyKind::DUMMY` ведёт себя ровно как раньше, и `--accept` считает те же цифры, что считал до этой вехи ([09-acceptance.md](09-acceptance.md)). ## Оружие Каталог — [src/sim/weapon_defs.h](../src/sim/weapon_defs.h). Карабин и сабля по-прежнему читают числа из `tuning.h`: это оружие по умолчанию, и все прежние ручки оверлея крутят именно их ([08-decisions.md](08-decisions.md)). | Ствол | Дальность | Интервал | Урон | Магазин | Чем отличается | |---|---|---|---|---|---| | CARBINE | 7.2 | 0.35 | 12 | 24 | штатный: ровный во всём | | PISTOL | 4.6 | 0.22 | 8 | 7 | ближе и слабее, зато быстрая перезарядка | | SMG | 5.4 | 0.09 | 6 | 40 | шквал; увод копится быстрее, чем оседает | | SHOTGUN | 3.4 | 0.85 | 26 x6 | 5 | залп дробин, в упор рвёт | | MAGNUM | 8.6 | 0.95 | 78 | 6 | дальше всех и валит с ног, но раз в секунду | | Клинок | Досягаемость | Дуга | Урон | Чем отличается | |---|---|---|---|---| | SABER | 1.40 | 100° | 130 | штатный | | KNIFE | 1.00 | 55° | 62 | почти без замаха; узкий сектор чаще чист | | CLEAVER | 1.65 | 135° | 240 | тяжёлый; занести его в тесноте негде | **Картечь и правило 6.3.** Дробины уходят в ТОТ ЖЕ конус `SpreadRad`, который проверен на своих перед спуском. Правило не ослаблено ни на градус — просто у дробовика конус широкий, и выстрел отсекается заметно чаще. Это его цена, а не исключение из правила. ## Твари Каталог — [src/sim/target.cpp](../src/sim/target.cpp), поведение — [src/sim/enemy.cpp](../src/sim/enemy.cpp). | Вид | HP | Скорость | Урон | Замах | Атака | Роль | |---|---|---|---|---|---|---| | SHAMBLER | 210 | 0.95 | 13 | 0.42 | SWIPE | основа толпы; страшен числом | | RUSHER | 110 | 3.3 | 14 | 0.28 | LUNGE | добегает до перезарядки | | SPITTER | 110 | 1.15 | 16 | 0.55 | SPIT | единственный, кто достаёт издалека | | BRUTE | 520 | 1.05 | 46 | 0.95 | SLAM | разбирать вдвоём | | LURKER | 150 | 1.90 | 9/тик | 0.50 | GRAB | вырывает бойца из строя | Поведение намеренно простое: заметил ближайшего живого бойца — идёт — дошёл — **замахивается** — бьёт. Замах (`windupTimer`) виден всегда: тело подсвечивается и глаза вспыхивают ровным светом даже в темноте. Между замахом и ударом у игрока есть время увести бойца. Без телеграфа удар твари неотличим от случайной потери здоровья, и бой перестаёт быть боем. Ось удара на замахе НЕ доводится: тварь бьёт туда, куда замахнулась. Ровно это и делает уклонение возможным. ### Пять атак — пять разных задач игроку Один удар на всех делал любую тварь одинаковой: подошла — сняла здоровье. Разная ГЕОМЕТРИЯ (`AttackKind`, [src/sim/target.h](../src/sim/target.h)) — то единственное, ради чего отряд приходится расставлять по-разному. | Атака | Форма | Что требует от игрока | |---|---|---| | SWIPE | сектор `swipeArcDeg` (100° у бредущего) в пределах `attackRange` | уходить **вбок**: назад от короткого удара не успеть | | LUNGE | рывок на `lungeDist` (3.4) за `lungeTime` (0.20 с); урон всем, кого задело по пути | освобождать **ось**, а не точку: на прямой броска безопасных мест нет | | SPIT | снаряд `SpawnHostileBullet`, летит `spitRange` (8.4) — дальше, чем плевок «достаёт» | **единственная** атака, которую перекрывает стена | | SLAM | круг `slamRadius` (2.15) вокруг твари | не толпиться возле громилы: второй боец платит наравне с целью | | GRAB | захват на `grabTime` (2.6 с), урон тикает раз в `grabTick` (0.45 с) | вытаскивать своего **огнём** | Рывок — это движение по времени, а не телепорт: позиция бегуна меняется каждый шаг и упирается в стены как обычно (`map.ResolveCircle`). Каждого бойца один рывок задевает не больше одного раза (`lungeMask`). Откат считается от КОНЦА броска, а не от его начала. Захват держит **ноги и только их**. `Squad::UpdateMovement` не двигает бойца с `grabbedTimer > 0` — ни строй, ни решатель, ни расступание из чужой линии. Стрельбу и клинок захват не читает вообще: правило 6.3 и сектор удара работают как обычно, иначе единственным выходом из хватки был бы чужой выстрел. Сам таймер гаснет, а держащая тварь продлевает его каждый шаг на `enemyGrabHold`: убили хвата — продлевать некому, и боец свободен через одну шестую секунды. Поэтому про захват не знают ни `bullet.cpp`, ни `melee.cpp`. Общих ручек всего две, обе в [src/tuning.h](../src/tuning.h): `enemyWindupScale` (длина всех телеграфов разом) и `enemyGrabHold`. Формы — `swipeArcDeg`, `lungeDist`, `lungeTime`, `spitRange`, `slamRadius`, `grabTime`, `grabTick` — свойства ВИДА и живут в `EnemyDef` рядом с остальными статами. Эффекты получают отдельные события (`sim/events.h`): `ENEMY_WINDUP`, `ENEMY_LUNGE`, `ENEMY_SLAM` — `owner` = индекс ТВАРИ; `ENEMY_HIT`, `ENEMY_GRAB`, `ENEMY_RELEASE` — `owner` = индекс БОЙЦА. Индексы из разных массивов, путать их нельзя. **DUMMY по-прежнему инертна.** У мишени приёмки вид атаки выставлен формально, с нулевыми уроном и дальностью, а `UpdateEnemies` до неё вообще не доходит: `--accept` меряет ровно то же, что мерял. Упавшего бойца твари не добивают — иначе отряд, потерявший одного, теряет всех подряд, пока игрок ничего не может сделать. Расстановка: `NUM_TARGETS` кучек по прежним точкам приёмки плюс `NUM_EXTRA_ENEMIES` тварей по всей карте — без вторых зачистка первой кучки заканчивала вылазку. Хваты стоят только в глубине: захват — ловушка для отряда, который уже разошёлся по позициям, а не встречающий у порога. ## Разрушаемая обстановка Ящики, бочки и шкафчики (`CRATE`, `BARREL`, `LOCKER`) живут **в том же пуле `Target`**, что и твари. Заводить под них вторую сущность было незачем: пуля, клинок, стена и правило 6.3 работают с ними одинаково — как с телом, у которого есть позиция, радиус и HP. Ни одну из этих систем трогать не пришлось. Отличий ровно четыре, и каждое проверяется через `Target::IsProp()`: | Где | Что иначе | |---|---| | `UpdateEnemies` | не шевелится: ни скорости, ни агро, ни удара | | `UpdateTargets` | не воскресает — разбитый хлам остаётся разбитым | | выбор цели в `ai/` | **последний приоритет** (ниже) | | события | `HIT_PROP` / `PROP_BREAK` вместо `HIT_TARGET` / `HIT_KILL` | Отдельные виды событий нужны не для красоты: слой крови ([src/render/blood.cpp](../src/render/blood.cpp)) знает только позицию события, и отличить попадание в бочку от попадания в тварь можно ТОЛЬКО видом события. Иначе ящик кровоточит. Заодно разбитый хлам не идёт ни в `KILLS`, ни в напряжение, ни в счётчик ударов клинком. ### Приоритет: обстановка — всегда последняя Правило одно на оба класса оружия и живёт в одной функции — `EnemyNearby` ([src/ai/firing_solver.h](../src/ai/firing_solver.h)): > пока в радиусе `propYieldRadius` есть хоть одна живая тварь, ящик целью > не становится **вообще**. И стрелок (`EvaluateAgentLane`), и клинок (`EvaluateMeleeTarget`) делают два прохода по целям: сначала твари, и только при полностью пустом первом проходе — обстановка. Даже если линию до твари перекрыл свой, а до ящика она чиста, боец переступает (решатель 6.5), а не разряжает магазин в мебель. Радиус **свой**, а не дальность ствола. С дальностью правило читалось бы как «не достаю — стреляю по ящику», и подошедшую тварь боец встречал бы на перезарядке. Стены его не отменяют намеренно: тварь за углом в пяти шагах — всё ещё повод не заниматься мебелью. Линия к хламу никогда не `priorityLane`: приоритет запрещает решателю искать позицию получше, а ради ящика боец переступать обязан. Назначенная игроком цель (6.8) правило обходит — это осознанно: явный приказ сильнее автоматики. ## Снаряжение [src/sim/items.h](../src/sim/items.h). Предмет даёт бойцу ровно четыре числа: броня, скорость, запас здоровья, дальность обзора. **Ни один предмет не может ослабить правило «не бить своих»** — у него просто нет такой ручки, и это не самоограничение, а устройство: `FireWeapons` и `UpdateMelee` про инвентарь не знают вообще, они читают уже готовые статы агента. Броня складывается, но упирается в `ARMOR_CAP = 0.72`: неуязвимых нет. | Броня | Поглощение | Скорость | Прочее | |---|---|---|---| | JACKET | 10% | 100% | — | | VEST | 28% | 95% | +10 HP | | PLATE | 48% | 82% | +25 HP | | SHROUD | 4% | 112% | обзор 92% | В карман: LANTERN (обзор 135%), CHARM (+30 HP), SPURS (скорость 114%). Расходники: BANDAGE, STIM. Комплекты раздаются по СЛОТУ СТРОЯ, а не «персонажу» (`Game::EquipKits`): остриё клина берёт плиту и тесак, хвост — лёгкую броню и дальний ствол. Состав отряда читается прямо по строю. ## Экраны ![меню](menu.png) Меню, пауза, экран отряда и поражение живут в [src/ui/menu.cpp](../src/ui/menu.cpp). Это второе и последнее место в игре, где разрешены примитивы raylib (первое — `debug/overlay`): здесь нужны слова, а собственный шрифт на 480x270 стоил бы дороже всего остального UI вместе взятого. Мир по-прежнему рисуется только в софтверный фреймбуфер, меню ложится поверх готового кадра. ![отряд](squad.png) Экран отряда (`I` в бою) показывает те же числа, которые читает симуляция: HP, броню, скорость, обзор. `ENTER` меняет местами оружие в руках и выбранную ячейку сумки — отряд не носит воздух, у него всегда что-то в руках. ## HUD [src/render/hud.cpp](../src/render/hud.cpp) рисуется в тот же софтверный фреймбуфер, что и мир, — значит попадает под ту же сетку пикселей. Текста в нём нет ни одной буквы: - цвет полосы — номер бойца, тот же, что у его тела на карте; - длина полосы — здоровье, тонкая полоска под ней — броня; - квадратики — патроны, гаснут по одному; сплошная полоса — перезарядка; - у клинка вместо патронов фаза удара: замах, удар, восстановление; - уголок над карточкой — боец при цели; - карточка перечёркнута — боец упал. ## Смерть бойца Мёртвый агент остаётся в массиве: `NUM_AGENTS` — величина времени сборки, по ней ходят все циклы, слоты строя, метрики и оверлей. Убирать его из пула значило бы переиндексировать полмира на каждой смерти. Поэтому смерть — это флаг `Agent::alive`, и его проверяют все системы: движение, расталкивание, реестр линий, решатель, проверка «свой на линии», сектор клинка, пули. Павший не перекрывает линию огня и не попадает под лезвие. Тело остаётся на полу до конца вылазки. ## Сложность Всё крутится на ходу через оверлей или `--set`: ```bash squad_proto.exe --set enemyDamage=1.6 # во сколько раз больнее бьют squad_proto.exe --set agentHP=60 # запас бойца до брони squad_proto.exe --set enemyRespawn=8 # как быстро твари возвращаются ``` Числа видов правятся в `EnemyDefOf`, числа предметов — в `ItemDefOf`. Баланс на 24 тварей и 5 бойцов подобран грубо и ждёт живых прогонов: отряд, стоящий на месте, держится, но постепенно теряет здоровье.