328 lines
25 KiB
Markdown
328 lines
25 KiB
Markdown
|
|
# 08 — Решения и отклонения от спека
|
|||
|
|
|
|||
|
|
Спек в разделе 12 требует: раздел 6 реализуется как написано, а если считаешь,
|
|||
|
|
что где-то ошибка — сначала скажи, потом делай. Здесь перечислено **всё**, что
|
|||
|
|
отличается от буквы спека, с причиной и замерами.
|
|||
|
|
|
|||
|
|
Отклонения разделены на две группы: содержательные (меняют поведение) и
|
|||
|
|
структурные (не меняют).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## A. Содержательные
|
|||
|
|
|
|||
|
|
### A1. Пуля не летит дальше проверенного отрезка
|
|||
|
|
|
|||
|
|
**Спек**: 6.9 — снаряд со скоростью 22 юнита/с.
|
|||
|
|
**Сделано**: время жизни пули = длина отрезка «дуло → цель» / скорость,
|
|||
|
|
а не `дальность оружия / скорость`.
|
|||
|
|
|
|||
|
|
**Почему**. `LaneClear` проверяет отрезок до цели. Если цель успевает умереть от
|
|||
|
|
чужой пули, снаряд летит **дальше** — в пространство, которое никто не проверял.
|
|||
|
|
Ровно так возникало последнее упорное попадание по своим: агент стоял внутри
|
|||
|
|
кучки мишеней, пуля прошла место умершей мишени насквозь и нашла его.
|
|||
|
|
|
|||
|
|
**Замер**: до фикса — 1 попадание за 300 с, стабильно воспроизводимое
|
|||
|
|
(`t=94.49, #1 -> #2 @ (30.6,21.7)`); после — 0 за 300 с.
|
|||
|
|
|
|||
|
|
### A2. Спуск учитывает время полёта пули (`AllyInterceptsShot`)
|
|||
|
|
|
|||
|
|
**Спек**: 6.3 проверяет линию в момент выстрела.
|
|||
|
|
**Сделано**: дополнительно проверяется сближение пули и союзника как двух
|
|||
|
|
движущихся точек на всём времени полёта, с радиусом безопасности, растущим со
|
|||
|
|
временем (`shotPredictSlack` — запас на то, что союзник может и повернуть).
|
|||
|
|
|
|||
|
|
**Почему**. 6.3 даёт мгновенный снимок, а 6.9 делает пулю реальным снарядом:
|
|||
|
|
до 0.4 с полёта, за которые союзник проходит больше метра. Союзник, честно
|
|||
|
|
чистый в момент нажатия, спокойно входит в коридор уже после выстрела.
|
|||
|
|
Реактивное расступание (6.6) этот случай не закрывает по определению — оно
|
|||
|
|
срабатывает, только когда тело уже в линии.
|
|||
|
|
|
|||
|
|
Само правило 6.3 при этом **не ослаблено**: это дополнительное условие спуска,
|
|||
|
|
а не замена. `LaneClear` продолжает использоваться и в целеуказании, и в оценке
|
|||
|
|
кандидатов, и в реестре линий — в том виде, как написано в спеке.
|
|||
|
|
|
|||
|
|
**Замер**: журнал попаданий показывал, что жертва **всегда** была `ADVANCING` —
|
|||
|
|
то есть именно вбегала под летящую пулю.
|
|||
|
|
|
|||
|
|
### A3. В реестр линий попадают линии, перекрытые своим
|
|||
|
|
|
|||
|
|
**Спек**: 6.6 — «список активных линий огня — сегментов от **стреляющих** агентов».
|
|||
|
|
**Сделано**: линия регистрируется и тогда, когда у агента есть цель в радиусе,
|
|||
|
|
стена не мешает, а мешает **только свой** (`Agent::laneIntent`).
|
|||
|
|
|
|||
|
|
**Почему**. При буквальном прочтении механика самоотменяется: стоит кому-то
|
|||
|
|
влезть в линию — стрелок теряет линию, перестаёт быть «стреляющим», отрезок
|
|||
|
|
исчезает из реестра, и перекрывшего **ничто не выталкивает**. Сектор обстрела не
|
|||
|
|
освобождается никогда, а обещание 6.6 («отряд сам расплывается, освобождая
|
|||
|
|
сектора обстрела») и критерий 11.4 становятся недостижимыми.
|
|||
|
|
|
|||
|
|
**Замер**: `FIRE UPTIME` на маршруте вырос с 33 % до 49 %, худший uptime на
|
|||
|
|
стоянке — с 38 % до 51 %.
|
|||
|
|
|
|||
|
|
### A4. Резерв линии шире порога блокировки (`laneReserveScale = 1.8`)
|
|||
|
|
|
|||
|
|
**Спек**: 6.6 — импульс, если `dist < bodyRadius + laneHalfWidth + SAFETY_MARGIN`.
|
|||
|
|
**Сделано**: импульс расступания начинается на `laneReserveScale` шире этого
|
|||
|
|
порога. Сам порог блокировки в 6.3 не тронут.
|
|||
|
|
|
|||
|
|
**Почему**. Раздел называется «**Резервирование** линий». Если отжимать агента
|
|||
|
|
ровно с той границы, за которой он уже блокирует выстрел, — это не резерв, а
|
|||
|
|
реакция постфактум: агент успевает войти в сектор, где летит пуля.
|
|||
|
|
|
|||
|
|
**Замер** (300 с, с A1 и A2):
|
|||
|
|
|
|||
|
|
| `laneReserve` | FRIENDLY_HITS | худший uptime на стоянке |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 1.0 (буква спека) | 1 | 42.5 % |
|
|||
|
|
| 1.4 | 0 | 50.0 % |
|
|||
|
|
| **1.8** (принято) | 0 | 50.4 % |
|
|||
|
|
|
|||
|
|
### A5. Касательная составляющая обхода стен (`wallSlide`)
|
|||
|
|
|
|||
|
|
**Спек**: 6.7, слой 4 — «wallAvoidance + скольжение вдоль стен при коллизии».
|
|||
|
|
**Сделано**: помимо скольжения при коллизии, `WallAvoidForce` добавляет
|
|||
|
|
касательную силу в сторону цели, когда стена стоит между агентом и целью.
|
|||
|
|
|
|||
|
|
**Почему**. Чисто отталкивающее избегание даёт классический локальный минимум:
|
|||
|
|
seek тянет вперёд, отталкивание держит — агент залипает в «тени» препятствия.
|
|||
|
|
В прогонах это ловилось буквально: агент оставался в северной комнате
|
|||
|
|
(`y=12`), когда отряд уже стоял на арене (`y=26`).
|
|||
|
|
|
|||
|
|
### A6. Агент без целей в радиусе возвращается в строй
|
|||
|
|
|
|||
|
|
**Спек**: случай не описан. 6.5 запускает решатель, когда линии нет, но не
|
|||
|
|
говорит, что делать, если стрелять вообще не по чему.
|
|||
|
|
**Сделано**: если решатель не нашёл ни одной цели в пределах
|
|||
|
|
`weaponRange + ring3`, `postPos` = слот строя.
|
|||
|
|
|
|||
|
|
**Почему**. Иначе отставший агент стоит на месте вечно: в `HOLDING` он идёт
|
|||
|
|
только к `postPos`, а `postPos` равен его же позиции.
|
|||
|
|
|
|||
|
|
### A7. Дуло считается в сторону цели, а не по `facing`
|
|||
|
|
|
|||
|
|
**Спек**: 6.3 — «`muzzle()` — точка в 0.3 юнита от центра агента по `facing`».
|
|||
|
|
**Сделано**: для проверки линии и для спавна пули дуло считается по направлению
|
|||
|
|
на цель; выстрел разрешён только когда ствол доведён до цели с точностью ±5°
|
|||
|
|
(`aimToleranceDeg`).
|
|||
|
|
|
|||
|
|
**Почему**. `facing` доворачивается плавно и на шаг отстаёт от направления на
|
|||
|
|
цель. Если проверять линию по одному направлению, а стрелять по другому, пуля
|
|||
|
|
летит не по проверенному отрезку — то есть гарантия «не сквозь своих» перестаёт
|
|||
|
|
быть гарантией. Сейчас проверенный отрезок и траектория пули — буквально один и
|
|||
|
|
тот же отрезок.
|
|||
|
|
|
|||
|
|
### A8. Пуля не бьёт собственного стрелка
|
|||
|
|
|
|||
|
|
**Спек**: 6.9 — «коллизия со **всеми** капсулами, включая союзников».
|
|||
|
|
**Сделано**: стрелок исключён из коллизий своей пули.
|
|||
|
|
|
|||
|
|
**Почему**. Дуло вынесено на 0.3 юнита при радиусе тела 0.28 — пуля стартует
|
|||
|
|
почти вплотную к своему же телу. Самопопадание было бы вырожденным шумом в
|
|||
|
|
`FRIENDLY_HITS`, главной метрике корректности. Все **остальные** капсулы,
|
|||
|
|
включая союзников, проверяются как написано.
|
|||
|
|
|
|||
|
|
### A9. Северная кучка мишеней стоит напротив прохода
|
|||
|
|
|
|||
|
|
**Спек**: 4 — 12 мишеней кучками, конкретные места на усмотрение реализации.
|
|||
|
|
**Сделано**: кучка из 3 мишеней в северной комнате поставлена напротив прохода
|
|||
|
|
`x=30..31`, так что из коридора до неё есть чистая линия.
|
|||
|
|
|
|||
|
|
**Почему**. Спек называет узкий проход важным тест-кейсом, а критерий 11.6
|
|||
|
|
требует, чтобы в проходе отряд вытягивался в колонну и стрелял только передний.
|
|||
|
|
Без цели, простреливаемой сквозь проход, проверять нечего.
|
|||
|
|
|
|||
|
|
### A10. Вторичные цели в `HOLDING` при ручном прицеливании
|
|||
|
|
|
|||
|
|
**Спек**: 6.8 — флаг `freeFireSecondary` описан только для `ADVANCING`.
|
|||
|
|
**Сделано**: буквально — в `HOLDING` агент без линии до назначенной цели молчит
|
|||
|
|
и перестраивается решателем, по вторичным целям не стреляет.
|
|||
|
|
|
|||
|
|
**Почему**. Иначе агент нашёл бы вторичную цель, получил бы `hasLane`, и решатель
|
|||
|
|
для него никогда бы не запустился — то есть требование «в `HOLDING`
|
|||
|
|
перестраивается через решатель» не выполнялось бы.
|
|||
|
|
|
|||
|
|
### A11. Туман войны и освещение вопреки разделу 12
|
|||
|
|
|
|||
|
|
**Спек**: 12 — «не добавлять освещение, туман войны».
|
|||
|
|
**Сделано**: добавлено и то, и другое.
|
|||
|
|
|
|||
|
|
**Почему**. Заказчик изменил требования: прототип разворачивается в хоррор, где
|
|||
|
|
ограниченный обзор и темнота — не украшение, а основа подачи. Правила стрельбы
|
|||
|
|
(раздел 6) при этом не тронуты ни одной строкой, критерии приёмки остались
|
|||
|
|
зелёными. Устройство слоя — [10-atmosphere.md](10-atmosphere.md).
|
|||
|
|
|
|||
|
|
### A12. Конус зрения не влияет на ИИ
|
|||
|
|
|
|||
|
|
**Сделано**: у агента есть конус обзора для КАРТИНКИ, но выбор целей и стрельба
|
|||
|
|
по-прежнему работают по LOS и дальности оружия.
|
|||
|
|
|
|||
|
|
**Почему**. Если завязать целеуказание на видимость, меняются все критерии
|
|||
|
|
приёмки, а отряд начинает «слепнуть» от поворота ствола — при том что `LaneClear`
|
|||
|
|
и так требует чистой линии, то есть отсутствия стены. Чтобы стрельба в темноту
|
|||
|
|
не выглядела абсурдом, мишень показывается, если освещена **или** если по ней уже
|
|||
|
|
работает кто-то из отряда.
|
|||
|
|
|
|||
|
|
### A13. Shift — рывок, а не медленный шаг
|
|||
|
|
|
|||
|
|
**Спек**: 5 — «Shift (удерж.) — медленный шаг».
|
|||
|
|
**Сделано**: базовая скорость снижена с 3.2 до 2.0, а Shift даёт рывок 3.2.
|
|||
|
|
|
|||
|
|
**Почему**. Заказчик просил «менее казуальное управление, помедленнее». Медленный
|
|||
|
|
шаг при уже медленной базе — мёртвая клавиша. Инверсия сохраняет обе скорости из
|
|||
|
|
спека (2.0 и 3.2), но делает осторожность режимом по умолчанию, а спешку —
|
|||
|
|
осознанным решением игрока. Побочный эффект: проверка спуска на время полёта пули
|
|||
|
|
обязана считать по МАКСИМАЛЬНОЙ скорости (`MaxAgentSpeed()`), иначе рывок союзника
|
|||
|
|
не учитывается и возвращаются попадания по своим.
|
|||
|
|
|
|||
|
|
### A14. Дальность оружия 9.0 -> 7.2
|
|||
|
|
|
|||
|
|
**Сделано**: минус 20 % по требованию заказчика; вместе с ней отмасштабированы
|
|||
|
|
`minEngageDist` (2.0 → 1.6) и кольца решателя (0.8/1.6/2.4 → 0.7/1.4/2.1),
|
|||
|
|
а старт отряда сдвинут в (26.5, 26.5) ближе к кучке мишеней.
|
|||
|
|
|
|||
|
|
**Замер**: без сдвига старта и масштабирования колец сокращение дальности само по
|
|||
|
|
себе давало 1 попадание по своим за 300 с и просадку uptime — агенты сбивались в
|
|||
|
|
кучу у цели. С пакетом целиком: 0 попаданий, uptime в стоячем бою 100 %.
|
|||
|
|
|
|||
|
|
### A15. `candMinAlly` 0.55 -> 0.60
|
|||
|
|
|
|||
|
|
Порог «кандидат слишком близко к союзнику» был **меньше диаметра тела**
|
|||
|
|
(2 × 0.28 = 0.56), то есть допускал позиции, где агенты физически пересекаются.
|
|||
|
|
|
|||
|
|
### A16. Клинок союзникам урона не наносит вообще
|
|||
|
|
|
|||
|
|
**Спек**: ближнего боя не описывает.
|
|||
|
|
**Сделано**: сектор удара повторяет правило 6.3 в угловой форме — свой в
|
|||
|
|
секторе — не бить. Но урон применяется **только к мишеням**: даже влезший под
|
|||
|
|
лезвие союзник не получает ничего, взмах просто обрывается.
|
|||
|
|
|
|||
|
|
**Почему**. Соблазн был сделать «сильную» версию: клинок бьёт всех, а от
|
|||
|
|
попаданий по своим спасает дисциплина сектора. Но у клинка нет времени полёта,
|
|||
|
|
за которое можно передумать: союзник входит в дугу за 1/60 с уже после того, как
|
|||
|
|
взмах начат, и в этот момент отменять нечего. «Сильная» версия означала бы
|
|||
|
|
`FRIENDLY_HITS > 0` в любой плотной свалке, то есть провал главного критерия
|
|||
|
|
прототипа ради механики, которой в спеке нет.
|
|||
|
|
|
|||
|
|
Обрыв взмаха даёт то же самое ощущение (боец отдёргивает клинок, серая вспышка,
|
|||
|
|
`MELEE_ABORT` в метриках) и при этом не может сломать критерий 11.2.
|
|||
|
|
|
|||
|
|
**Замер**: 81 взмах за 30 с в плотной свалке, `FRIENDLY_HITS = 0`,
|
|||
|
|
16 взмахов оборвано.
|
|||
|
|
|
|||
|
|
### A17. Старт замаха смотрит на союзника вперёд
|
|||
|
|
|
|||
|
|
**Сделано**: `MeleeArcClear` проверяет союзника в двух точках — где он сейчас и
|
|||
|
|
где будет через `windup + strike`. Ровно та же поправка, что A2 для пули.
|
|||
|
|
|
|||
|
|
**Почему**. Взмах живёт 0.30 с, за которые союзник проходит полметра. Проверять
|
|||
|
|
только текущее положение — значит начинать удары, которые всё равно придётся
|
|||
|
|
обрывать.
|
|||
|
|
|
|||
|
|
**Замер** (сценарий M7, 30 с): без поправки и без резерва дуги — 21 оборванный
|
|||
|
|
взмах из 77; с поправкой и резервом — 16 из 81, попаданий 81 против 81, убийств
|
|||
|
|
23 против 23. То есть взмахов стало больше при том же результате.
|
|||
|
|
|
|||
|
|
### A18. Резерв в реестре линий получил явную полуширину
|
|||
|
|
|
|||
|
|
**Сделано**: у `FireLane` появилось поле `reserve`. Линия огня по-прежнему берёт
|
|||
|
|
ширину из правила 6.3, клинок задаёт свою — долю бокового размаха сектора
|
|||
|
|
(`meleeReserveFrac` 0.55).
|
|||
|
|
|
|||
|
|
**Почему**. Клинок резервирует отрезок вдоль оси удара, а поражает сектор ±50°.
|
|||
|
|
Союзник, отжатый строго перпендикулярно оси, попадал ровно во фланг дуги — то
|
|||
|
|
есть механизм расступания работал против себя.
|
|||
|
|
|
|||
|
|
**Замер**: `meleeReserve` 0 → 0.55 дало 16 оборванных взмахов вместо 21 при
|
|||
|
|
большем числе взмахов, а худший `FIRE UPTIME` на стоянке маршрута — 23.3 %
|
|||
|
|
вместо 0 % (стрелки перестали стоять в дугах и молчать).
|
|||
|
|
|
|||
|
|
### A19. Жёсткая граница огневой позиции (`postLeash`)
|
|||
|
|
|
|||
|
|
**Спек**: 6.5 — цена за удаление от якоря.
|
|||
|
|
**Сделано**: дополнительно кандидат дальше `cohesionRadius + postLeash` от якоря
|
|||
|
|
отбраковывается совсем.
|
|||
|
|
|
|||
|
|
**Почему**. Цена — мягкая: линия огня стоит 100 очков, а уход на 1.5 юнита за
|
|||
|
|
круг — 36. Решатель всегда выбирал линию, и отряд расползался.
|
|||
|
|
|
|||
|
|
**Замер**: максимум удаления стрелка от диска 3.65 → 2.81 юнита. При этом
|
|||
|
|
`FIRE UPTIME` на 180-секундном маршруте не упал, а вырос: на стоянках у мишеней
|
|||
|
|
82.9 / 93.2 / 90.7 / 86.0 % — плотный отряд перекрывает друг другу линии реже,
|
|||
|
|
чем расползшийся.
|
|||
|
|
|
|||
|
|
### A20. Мишени — тела, и они получают отдачу
|
|||
|
|
|
|||
|
|
**Сделано**: слой движения `TargetPushForce` (мишень расталкивает агентов) и
|
|||
|
|
импульс от удара с возвратом на исходное место (`Target::home`).
|
|||
|
|
|
|||
|
|
**Почему**. Без расталкивания боец стоит **внутри** того, кого рубит, и удар не
|
|||
|
|
читается вообще. Без отдачи попадание в упор визуально не отличается от промаха.
|
|||
|
|
Возврат на `home` обязателен: расстановка уровня захардкожена, и уползание мишеней
|
|||
|
|
поломало бы все замеры разом. Респавн дополнительно ставит `pos = home`.
|
|||
|
|
|
|||
|
|
### A21. Критерий 11.6 считает только выстрелы вперёд
|
|||
|
|
|
|||
|
|
**Было**: тест требовал, чтобы в коридоре передний агент стрелял минимум вдвое
|
|||
|
|
чаще хвостовых, и считал ВСЕ выстрелы внутри прохода.
|
|||
|
|
|
|||
|
|
**Стало**: считаются только выстрелы **по ходу движения**. Выстрелы назад
|
|||
|
|
выводятся в отчёт отдельно.
|
|||
|
|
|
|||
|
|
**Почему**. Диагностика `DIAG` показала, что почти все «нарушения» — это
|
|||
|
|
хвостовой агент, стреляющий назад, в оставленную позади комнату, по совершенно
|
|||
|
|
другой цели. Впереди по такой линии никого из отряда нет по построению, и к
|
|||
|
|
правилу «не стрелять сквозь своего» это отношения не имеет. Порог ловил не
|
|||
|
|
поведение, а геометрию расстановки мишеней.
|
|||
|
|
|
|||
|
|
**Замер**: на прежней базе тюнинга новый счёт даёт 4/0 (было 12/4), на текущей —
|
|||
|
|
5/0. То есть критерий стал строже, а не мягче: вперёд хвост не стреляет вообще.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## B. Структурные (поведение не меняют)
|
|||
|
|
|
|||
|
|
### B1. Дополнительные файлы
|
|||
|
|
|
|||
|
|
Спек перечисляет структуру проекта; добавлены четыре файла:
|
|||
|
|
|
|||
|
|
| Файл | Зачем |
|
|||
|
|
|---|---|
|
|||
|
|
| `game.h/.cpp` | склейка подсистем и порядок шага. Без него это переехало бы в `main.cpp`, а спек требует, чтобы там были только окно и цикл |
|
|||
|
|
| `render/scene.h/.cpp` | проход по полу и список сортировки; отдельно от `iso.h`, который остаётся чистой математикой |
|
|||
|
|
| `core/input.h` | `InputState` нужен и `main.cpp`, и симуляции, и headless-прогонам |
|
|||
|
|
| `debug/harness.h/.cpp` | прогоны `--headless` / `--accept` |
|
|||
|
|
| `sim/loadout.h` | класс оружия и его параметры; сюда приедут разные пушки и апгрейды |
|
|||
|
|
| `ai/melee.h/.cpp` | ближний бой; зеркало `firing_solver` для угловой геометрии |
|
|||
|
|
|
|||
|
|
### B2. У `Sprite` есть точка привязки
|
|||
|
|
|
|||
|
|
Спек: `Sprite { int w, h; std::vector<uint32_t> px; }`. Добавлены поля `ax, ay` —
|
|||
|
|
точка внутри спрайта, совмещаемая с проекцией мировой позиции. Иначе смещения
|
|||
|
|
пришлось бы размазывать по коду отрисовки для каждого типа спрайта.
|
|||
|
|
|
|||
|
|
### B3. Параметры оружия синхронизируются из `Tuning` каждый шаг
|
|||
|
|
|
|||
|
|
`Agent::weapon` существует как в спеке, но перед движением поля копируются из
|
|||
|
|
`g_tune` (`SyncLoadoutFromTuning`). Без этого runtime-подкрутка (раздел 8 спека)
|
|||
|
|
не влияла бы на уже созданных агентов. Состояние ближнего боя — фаза, таймеры,
|
|||
|
|
цель — при этом не трогается: копируются только параметры.
|
|||
|
|
|
|||
|
|
### B4. Dev-инструменты
|
|||
|
|
|
|||
|
|
`--headless`, `--accept`, `--probe`, `--set`, `--shot`, тумблеры `--no-solver` /
|
|||
|
|
`--no-laneavoid`. В спеке их нет; они не влияют на игровую сборку и нужны, чтобы
|
|||
|
|
критерии приёмки из раздела 11 проверялись цифрами, а не на глаз.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Что осталось честно неидеальным
|
|||
|
|
|
|||
|
|
- `FIRE UPTIME` считает «нет живой цели в радиусе» простоем. На маршруте между
|
|||
|
|
комнатами это даёт низкие значения, которые к качеству ИИ отношения не имеют.
|
|||
|
|
- `SETTLE TIME` по той же причине растёт, если отряд остановился там, где стрелять
|
|||
|
|
не по чему; в общем прогоне попадаются значения до 5.7 с. В обстановке, к
|
|||
|
|
которой относится критерий 11.1 (мишени в радиусе), — 0.02 с.
|
|||
|
|
- Пути агентов чисто локальные, без поиска пути. Отряд проходит узкие коридоры,
|
|||
|
|
но лабиринт с тупиками не осилит — это вне рамок прототипа.
|