Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (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>
2026-08-03 17:00:49 +07:00
|
|
|
|
# 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.4–3.5 мс при бюджете 16.6 мс.
|
Личинки, парирование и инструмент --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 09:04:35 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## `--wall`: где обрывается забег
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
squad_proto.exe --rooms --wall 50
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Отвечает на вопрос, который появился вместе с чекпоинтами через пятьдесят
|
|
|
|
|
|
этажей: **растёт ли достижимая глубина от вылазки к вылазке**. Отряд из одного,
|
|
|
|
|
|
трёх и пяти бойцов, одетый по глубине, ставится в самое скопление тварей и живёт
|
|
|
|
|
|
там полторы минуты без управления.
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
первая стена: одиночка D7, трое D30, пятеро D30
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Это НИЖНЯЯ оценка: игрок уводит бойцов, выбирает цели и отступает — проверка не
|
|
|
|
|
|
умеет ничего из этого. Поэтому «стена» здесь не конец игры, а глубина, с которой
|
|
|
|
|
|
игра начинает требовать умения.
|
|
|
|
|
|
|
|
|
|
|
|
Смотреть надо на первую стену, а не на «глубже всего дожили»: на глубоких этажах
|
|
|
|
|
|
твари разбросаны, и отряд иногда доживает просто потому, что до него дошли не все.
|
|
|
|
|
|
|
|
|
|
|
|
Этот же прогон нашёл настоящую ошибку: `PlaceSquad` воскрешал ВСЕ пять слотов, в
|
|
|
|
|
|
том числе незанятые. Строка писалась, когда отряд всегда был впятером, и после
|
|
|
|
|
|
появления ростера одиночка выходил на этаж с четырьмя невидимыми телохранителями.
|
|
|
|
|
|
Из-за них прежний замер «одиночка на первом этаже» показывал уверенную победу —
|
|
|
|
|
|
это был бой впятером.
|