squad-proto/docs/06-debug-and-tuning.md
z.kirill e3294cffd4 Личинки, парирование и инструмент --wall (M30)
--WALL. Новый прогон отвечает на вопрос, появившийся вместе с чекпоинтами
через пятьдесят этажей: растёт ли достижимая глубина от вылазки к вылазке.
Отряд из одного, трёх и пяти, одетый по глубине, ставится в скопление
тварей и живёт полторы минуты без управления. Первая стена: одиночка D7,
трое D30, пятеро D30 - петля работает.

И он же нашёл настоящую ошибку: PlaceSquad воскрешал ВСЕ пять слотов, в
том числе незанятые. Строка писалась, когда отряд всегда был впятером;
после появления ростера одиночка выходил на этаж с четырьмя невидимыми
телохранителями. Из-за них прежний замер "одиночка выжил на первом этаже,
100 -> 41" был боем впятером.

ЛИЧИНКИ. С исправленным замером выяснилось, что одиночка с ножом
проигрывает двум бредущим: 210 здоровья против ножа это девять секунд
работы под ответным уроном. Первые этажи населены личинками (55 здоровья,
7 урона, медленные), бредущий уехал на третий этаж, бегун на четвёртый.
Новый ВИД, а не ослабленный бредущий с множителем: глубина в этой игре
меняет, кто выходит, а не числа у одного и того же.

ПАРИРОВАНИЕ. Отдельная характеристика клинка, и чем оружие легче, тем она
выше: нож 34%, сабля 20%, тесак 8%. Не выведена из веса намеренно - её
двигает аффикс PARRY, и лёгкий тесак обязан быть возможен как находка.

Отбить можно, только если удары СОПРИКОСНУЛИСЬ: боец сам должен быть в
замахе или в ударе. Иначе клинок стал бы щитом, работающим, пока стоишь.

Удавшийся отбив снимает урон, бьёт тварь ударом бойца целиком и запускает
повторную атаку с начала фазы, НЕ сбрасывая откат. Последнее и есть суть
размена: сбрасывай отбив откат, выгоднее было бы подставляться под удары.

Плевок отбивается тоже: летит назад и несёт урон бойца, а не твари. Это
его удар, просто по летящей цели.

Проверка меряет размен целиком, включая обратную половину правила:
"стоя столбом не отбивает". Без неё условие про соприкосновение
существовало бы только в комментарии.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 05:04:35 +03:00

180 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 06 — Оверлей, метрики, тюнинг
## Оверлей
[src/debug/overlay.cpp](../src/debug/overlay.cpp). Рисуется примитивами raylib **поверх**
уже отмасштабированного кадра. Это единственное исключение из «вся сцена софтверная»,
и оно dev-only: `F1` его убирает.
| Клавиша | Что показывает |
|---|---|
| `F1` | оверлей целиком |
| `F2` | линии огня |
| `F3` | кандидаты последнего прогона решателя |
| `F4` | метрики |
| `F5` | выключить хоррор-слой целиком (свет, туман, пост) |
| `F6` | показать диски обзора |
Всегда (при включённом `F1`): якорь отряда, **два** круга когезии (синий —
стрелков, оранжевый — клинков), слоты строя, подпись над каждым агентом
`индекс, класс (R/M), состояние (A/H), id цели` и у клинка фаза удара
(`wind` / `HIT` / `rec`), выбранная огневая позиция `postPos` кружком.
**Сектор клинка**: кольцо досягаемости и две границы дуги, цвет по фазе —
жёлто-оранжевый на замахе, белый на ударе, серый на восстановлении. В покое цвет
говорит, почему не бьём: жёлтый — свой в дуге, синий — просто ждёт.
**Цвета линий огня (`F2`)**:
| Цвет | Значение |
|---|---|
| зелёный | линия чиста, агент стреляет |
| жёлтый | цель есть, линию перекрыл свой |
| красный | линию перекрыла стена |
**Кандидаты (`F3`)**: точками, цвет по оценке (синий низкая → белый высокая),
отбракованные — тусклым красным, выбранный обведён белым кругом.
Показывается последний прогон решателя, поэтому у агентов, у которых линия есть,
там пусто — решатель для них не запускался, так и задумано.
В ручном режиме дополнительно рисуется вектор прицела, границы конуса ±20°
и обводка назначенной цели.
## Метрики (`F4`)
[src/debug/metrics.h](../src/debug/metrics.h).
| Метрика | Как считается | Смысл |
|---|---|---|
| **FRIENDLY_HITS** | накопительный счётчик попаданий пуль по своим | главная метрика корректности, должна быть 0 |
| **FIRE UPTIME** | доля шагов с валидной линией за окно 10 с; среднее — **только по стрелкам** | насколько отряд «в деле» |
| **MELEE reach** | доля шагов с целью в зоне удара, среднее по клинкам | то же самое для ближнего боя |
| `swings / hits / aborted` | взмахов начато, попаданий лезвием, взмахов оборвано своим в дуге | `aborted` — видимая цена скученности, не баг |
| **SETTLE TIME** | от перехода в `HOLDING` до момента, когда ≥4 агента «в бою» | как быстро расходится веером |
| fps, `sim ms/frame` | время симуляции за кадр | бюджет 16.6 мс |
«В бою» для стрелка — валидная линия, для клинка — выбранная цель. Смешивать
доли времени двух разных занятий в одно среднее бессмысленно, поэтому
`FIRE UPTIME` и `MELEE reach` считаются раздельно: у клинка линии огня не бывает
по определению, и его ноль занижал бы метрику стрельбы.
Реализация `FIRE UPTIME` — кольцевой буфер на 600 сэмплов на агента с
инкрементальными счётчиками, пересчёт O(1) за шаг.
**Журнал попаданий по своим**: каждое попадание пишется в `Metrics::hitLog`
(время, стрелок, жертва, координаты, состояние жертвы). Без него `FRIENDLY_HITS != 0`
нечем объяснить — именно журнал показал, что жертва всегда была `ADVANCING`,
и вывел на настоящую причину (см. [08-decisions.md](08-decisions.md)).
### Как читать цифры
- `FIRE UPTIME` считает «нет живой цели в радиусе» как простой. Поэтому на
переходах между комнатами и после зачистки кучки метрика честно падает — это не
дефект ИИ. Критерий приёмки 11.8 относится к **стоячему бою рядом с мишенями**.
- `SETTLE TIME` по той же причине растёт, если отряд остановился там, где стрелять
не по чему. Сценарий `--accept` меряет его в правильной обстановке.
## Runtime-тюнинг
Все числа живут в [src/tuning.h](../src/tuning.h) в структуре `Tuning`.
Магических констант по коду нет — если нужно новое число, оно добавляется туда.
Часть параметров вынесена в таблицу `TUNABLES` и крутится на ходу:
- `[` / `]` — выбрать параметр, текущее имя и значение видно в оверлее;
- `-` / `=` — изменить с шагом параметра (с зажатием работает автоповтор).
Те же параметры можно задать из командной строки без пересборки:
```bash
squad_proto.exe --headless 300 --set safetyMargin=0.25 --set laneReserve=2.0
```
Список параметров смотри прямо в `TUNABLES` — там имя, поле, шаг и границы.
Самые содержательные для ощущения:
| Параметр | На что влияет |
|---|---|
| `safetyMargin` | запас к «телу в линии»; шире → реже стреляют, но безопаснее |
| `laneReserve` | во сколько раз резерв линии шире порога блокировки; отвечает за «текучесть» |
| `wLaneAvoid` | насколько нагло агент выдавливается из чужого сектора |
| `hysteresis` | насколько лучше должна быть новая позиция, чтобы агент переехал |
| `commitTime` | сколько держится решение; меньше → живее и дёрганее |
| `sDistCost` / `sCohesionCost` | готовность отбегать далеко ради выстрела |
| `postLeash` | жёсткая граница огневой позиции от диска; ниже — отряд плотнее |
| `anchorSpeed` / `anchorLeash` | отзывчивость диска и насколько он может оторваться от отряда |
Ближний бой (подробности — [11-melee.md](11-melee.md)):
| Параметр | На что влияет |
|---|---|
| `meleeReach` / `meleeArc` | размер сектора удара; шире → чаще задевает нескольких, но и своих |
| `meleeWindup` / `meleeStrike` / `meleeRecover` | ритм удара: читаемость замаха против скорострельности |
| `meleeInterval` / `meleeDamage` | DPS клинка |
| `meleeLunge` / `meleeKnock` | вес удара: рывок бойца и отдача мишени |
| `meleeCohesion` | СВОЙ, больший круг клинка вокруг диска |
| `meleeAllyMargin` / `meleePredict` | строгость дисциплины сектора; выше → меньше оборванных взмахов и меньше взмахов вообще |
| `meleeReserve` | насколько широко клинок отжимает своих со своей дуги |
| `meleeShake` | отдача камеры на попадание лезвием |
Хоррор-часть (подробности — [10-atmosphere.md](10-atmosphere.md)):
| Параметр | На что влияет |
|---|---|
| `sightRadius` / `sightCone` | как далеко и как широко видит отряд |
| `sightPersonal` | круговой ореол вокруг агента, спасает от клаустрофобии |
| `fogMemory` | насколько тускло помнится уже увиденное |
| `fogReveal` | при каком свете показывается тело мишени |
| `lightGamma` / `lightGain` | форма затухания света; **перепекают таблицу рампы** |
| `flashPower` | насколько вспышка выстрела освещает окружение |
| `fxIntensity` / `tracerLength` | яркость и длина трассеров |
| `grainTense` / `vigDepthTense` | зерно и виньетка на пике напряжения |
| `dreadRadius` | с какой дистанции мишень начинает пугать |
| `shakePerShot` | отдача камеры |
`F5` — аварийный тумблер: мгновенно возвращает прежний кадр без света и тумана.
Полезен, когда непонятно, баг это или просто темно.
## Dev-тумблеры ядра
`--no-solver` и `--no-laneavoid` выключают решатель и расступание. Это A/B-проверка
ценности ядра, цифры за 60 с на одном маршруте:
| Конфигурация | FRIENDLY_HITS | FIRE UPTIME |
|---|---|---|
| ядро выключено | 1 | 13.6 % |
| ядро включено | 0 | 88 % (на стоянке у мишеней) |
`sim`/`render` в оверлее показывают время шага симуляции и время кадра рендера
раздельно: 2 мкс против 1.43.5 мс при бюджете 16.6 мс.
## `--wall`: где обрывается забег
```bash
squad_proto.exe --rooms --wall 50
```
Отвечает на вопрос, который появился вместе с чекпоинтами через пятьдесят
этажей: **растёт ли достижимая глубина от вылазки к вылазке**. Отряд из одного,
трёх и пяти бойцов, одетый по глубине, ставится в самое скопление тварей и живёт
там полторы минуты без управления.
```
первая стена: одиночка D7, трое D30, пятеро D30
```
Это НИЖНЯЯ оценка: игрок уводит бойцов, выбирает цели и отступает — проверка не
умеет ничего из этого. Поэтому «стена» здесь не конец игры, а глубина, с которой
игра начинает требовать умения.
Смотреть надо на первую стену, а не на «глубже всего дожили»: на глубоких этажах
твари разбросаны, и отряд иногда доживает просто потому, что до него дошли не все.
Этот же прогон нашёл настоящую ошибку: `PlaceSquad` воскрешал ВСЕ пять слотов, в
том числе незанятые. Строка писалась, когда отряд всегда был впятером, и после
появления ростера одиночка выходил на этаж с четырьмя невидимыми телохранителями.
Из-за них прежний замер «одиночка на первом этаже» показывал уверенную победу —
это был бой впятером.