squad-proto/src/tuning.h
z.kirill c1f79a1fb8 Поле путей: отставший возвращается в строй (M37)
Отряд из пяти не проходил первый этаж ни на одном сиде, а одиночка проходил.
Дело было не в ширине коридоров: у бойцов не было поиска пути вообще. Боец,
оказавшийся за стеной от своего слота, seek'ом тянулся В СТЕНУ, обход стены
толкал его обратно, и он замирал в углу навсегда.

Теперь у каждого бойца своё поле путей (BFS от клетки его цели) с натягиванием
верёвки: он берёт самую дальнюю точку пути, которую видит. Включается обход по
двум условиям сразу — боец упёрся (хочет идти и не едет) И потерял строй. Счётчик
упора сбрасывается по факту «цель видно», а не «поехал»: сброс по скорости давал
дребезг, и боец полз шаг за две секунды.

По дороге вскрылись две неверные МЕРКИ:

* критерий 11.6 считал колонной «разброс вдоль больше, чем поперёк» — и потому
  награждал отряд, у которого двое навсегда остались по ту сторону стены. Теперь
  колонна меряется самим проёмом: разом в горловине не больше двух, и прошли ВСЕ.
* зонд --wall читал «стены не нашлось» (ноль) как «стена на нулевом этаже» и
  объявлял пятёрку, не сломавшуюся нигде, худшей из трёх.

Обе переписаны и проверены на сломанной сборке.

Замеры: --play 6 5 на восьми сидах 47/48 (было 0/48), --accept PASS, шаг 6.1 мкс,
--wall 30 одиночка D20 / трое D20 / пятеро D31 (не найдена), --descend 8/30,
hollow --descend 6, --ui-check, оба проекта --check — без ошибок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:31:34 +03:00

570 lines
41 KiB
C++
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.

#pragma once
#include <algorithm>
#include "engine/config.h"
// -----------------------------------------------------------------------------
// Единственное место, где живут числа ИГРЫ.
// В коде не должно быть магических констант — всё берётся отсюда.
//
// * блок COMPILE-TIME — размеры буферов игры, менять только пересборкой;
// * struct Tuning — всё, что подкручивается в рантайме ([ ] выбор, - = шаг).
//
// Размеры кадра, тайла и карты сюда БОЛЬШЕ НЕ ВХОДЯТ: это хозяйство движка, оно
// в engine/config.h. Имена те же, файл включён выше — по коду игры ничего не
// меняется, но движок теперь собирается без единого файла игры.
// -----------------------------------------------------------------------------
// --- compile-time константы --------------------------------------------------
// Точка старта отряда (центральная арена).
constexpr float SQUAD_START_X = 26.5f; // ближе к кучке: дальность стала 7.2
constexpr float SQUAD_START_Y = 26.5f;
constexpr int NUM_AGENTS = 5;
constexpr int NUM_TARGETS = 12; // мишени приёмки: расстановка захардкожена
constexpr int MAX_BULLETS = 512;
// Твари, которых боевой режим доставляет СВЕРХ мишеней. В песочнице приёмки их
// нет: расстановка мишеней и все замеры на ней остаются прежними.
constexpr int NUM_EXTRA_ENEMIES = 12;
// Разрушаемые объекты (ящики, бочки, шкафчики). Спавнятся ТОЛЬКО в боевом режиме;
// приёмка их не видит. Приоритет — ниже всех: ИИ переключается на них только
// если ни одного живого врага в поле зрения нет.
constexpr int NUM_PROPS = 8;
// Потолок на число целей в мире. Он не «на всякий случай»: MeleeWeapon::hitMask —
// это маска «кого уже задел ЭТИМ взмахом», индексируемая номером цели, и цель
// за её пределами молча получала бы удары дважды за взмах.
//
// Было 32. Игра про бесконечный спуск упёрлась в этот потолок примерно на
// шестнадцатом этаже: бюджет опасности перестал влезать в число тел, и глубже
// этажи переставали становиться страшнее. Маска расширена до 64 бит — это не
// «запас на будущее», а ровно та цифра, которой сейчас не хватало.
constexpr int MAX_TARGETS = 64;
static_assert(NUM_TARGETS + NUM_EXTRA_ENEMIES + NUM_PROPS <= MAX_TARGETS,
"целей больше, чем битов в MeleeWeapon::hitMask");
// У движка потолок населения свой (MAX_LEVEL_SPAWNS в engine/level_gen.h):
// генератор уровней не обязан знать про маску удара клинком. Что одно влезает
// в другое, проверяется там, где видны оба, — в src/game.h.
// Сколько слотов строя занимают бойцы ближнего боя (слоты 0..N-1).
// В клине это остриё и левое крыло — те, кто и так идёт первым.
constexpr int DEFAULT_MELEE_COUNT = 2;
// Сколько ПОСЛЕДНИХ слотов носят пистолет вместо карабина.
// Короткий ствол уезжает в хвост клина: ему там и место.
constexpr int DEFAULT_PISTOL_COUNT = 1;
// Сколько тайлов пола могут нести запечённую кровь. Каждый — своя копия
// спрайта 32x16 (2 КБ), то есть потолок ~2.4 МБ. Дальше новые пятна ложатся
// только на уже запачканные тайлы: декали вечные, но не бесконечные.
constexpr int BLOOD_TILE_BUDGET = 1200;
// Решатель огневых позиций: 3 кольца x 12 направлений + текущая позиция.
constexpr int SOLVER_RINGS = 3;
constexpr int SOLVER_DIRS = 12;
constexpr int SOLVER_CANDIDATES = SOLVER_RINGS * SOLVER_DIRS + 1;
// --- runtime-тюнинг ----------------------------------------------------------
struct Tuning
{
// движение агента
// Скорость отряда. Была 1.75 при курсоре 3.3: отряд ВСЕГДА отставал, и
// управление ощущалось как «тяну за верёвку», а не «веду людей». Поднято до
// 2.6 — это по-прежнему медленнее курсора (иначе поводок теряет смысл), но
// разрыв закрывается за доли секунды, а не за метры.
float moveSpeed = 2.60f; // юнитов/сек: хоррор, а не шутер-беготня
float moveSpeedFast = 4.4f; // догоняют курсор быстро — иначе отряд отстаёт
float steerAccel = 26.0f; // тяжелее; НИЖЕ 16 нельзя — ломается критерий 11.4
float bodyRadius = 0.28f;
float separationRadius = 0.7f;
float wSeek = 1.0f;
// Как быстро строй доворачивается к направлению хода. Мгновенный доворот
// заставлял бойцов бегать вбок на каждом повороте (sim/squad.cpp).
float formTurnRate = 7.0f;
// Насколько боец может отстать от своего слота, прежде чем бросит всё и
// вернётся в строй. Ниже 2 отряд перестаёт разбредаться по огневым
// позициям вообще, выше 4 — снова застревает цепочкой (sim/squad.cpp).
float straggleDist = 2.8f;
// Когда включается поле путей. Не по расстоянию до цели, а по факту упора:
// боец хочет идти и не едет. Расстоянием этот порог не задать — на подходе
// к проходу цель не видна никому, и по «не видно» все пятеро получили бы
// одну путевую точку и вошли бы в горловину шеренгой (критерий 11.6).
float navStuckTime = 0.35f; // секунд упора до обхода
float navStuckSpeed = 0.25f; // «не едет» — это доля от скорости хода
float navGoalReached = 0.35f; // ближе этого цель считается достигнутой
float wSeparation = 1.0f;
float wLaneAvoid = 2.4f; // строго выше wSeparation (спек 6.7)
float wWallAvoid = 1.2f;
float wallLookahead = 0.8f;
float wallSlide = 1.4f; // касательная составляющая обхода стен
float arriveRadius = 0.18f;
float turnRateDeg = 300.0f; // ствол тяжёлый
float wTargetPush = 1.0f; // мишень — тело, а не пустое место
float targetPushGap = 0.10f; // зазор поверх суммы радиусов
// оружие (это ПАРАМЕТРЫ КАРАБИНА — оружия по умолчанию, на котором меряются
// критерии приёмки. Остальные стволы — записи каталога sim/weapon_defs.h)
float weaponRange = 7.2f; // -20% от исходных 9.0
float fireInterval = 0.35f;
float laneHalfWidth = 0.10f;
float damage = 12.0f;
float safetyMargin = 0.15f;
float muzzleOffset = 0.3f;
float bulletSpeed = 22.0f;
float bulletRadius = 0.04f;
float bulletKnock = 0.35f; // импульс мишени от попадания пули
float aimToleranceDeg = 5.0f; // ствол должен быть доведён до цели
// --- разброс -------------------------------------------------------------
// Стоя ствол почти не гуляет, на полном ходу гуляет сильно, очередь его
// раскрывает. Тот же конус проверяется на своих перед спуском: правило 6.3
// не ослаблено, оно просто считает по конусу вместо отрезка.
float spreadStandDeg = 0.9f;
float spreadMoveDeg = 4.2f;
float spreadRecoilDeg = 0.55f; // прибавка за выстрел
float spreadRecoilMax = 3.0f;
float spreadRecoilDecay = 3.2f; // градусов/сек оседания
float spreadScale = 1.0f; // общий множитель, ручка для замеров
// --- перезарядка ---------------------------------------------------------
float magazineRifle = 24.0f; // патронов в магазине карабина
float reloadRifle = 1.9f; // сек
// Запас на манёвр союзника за время полёта пули (доля от moveSpeed).
// 0 = верить экстраполяции скорости буквально.
float shotPredictSlack = 0.3f;
// --- ближний бой ---------------------------------------------------------
// Три фазы: замах видно, урон живёт в коротком окне, после удара боец открыт.
float meleeReach = 1.40f; // от центра тела до конца замаха
float meleeArcDeg = 100.0f; // ширина сектора удара
float meleeWindup = 0.17f; // замах — телеграф
float meleeStrike = 0.13f; // активное окно урона
float meleeRecover = 0.24f; // после удара
float meleeInterval = 0.60f; // между началами замахов
float meleeDamage = 130.0f; // 4 удара на мишень против 42 пуль
float meleeBladeDeg = 34.0f; // ширина полосы самого лезвия
float meleeAimToleranceDeg = 30.0f; // допуск разворота на старте замаха
float meleeAllyMargin = 0.14f; // запас к телу союзника в секторе
// На сколько взмахов вперёд смотреть на союзника при старте замаха
// (в долях windup+strike). 0 = верить только текущему положению.
float meleeStartPredict = 1.0f;
// Доля бокового размаха сектора, которую клинок резервирует в реестре линий.
// 1.0 = вся дуга; тогда своим нечем дышать. 0 = резерв шириной с отрезок.
float meleeReserveFrac = 0.55f;
float meleeGap = 0.12f; // зазор при подходе вплотную
float meleeLungeSpeed = 3.4f; // рывок в момент удара, юнитов/сек
float meleeLungeAccel = 120.0f; // рывок обязан успеть за 0.13 с
float meleeWindupScale = 0.20f; // множитель скорости на замахе
float meleeRecoverScale= 0.55f;
float meleeKnockback = 2.4f; // импульс мишени, юнитов/сек
// СВОЙ круг бойца: его отпускают от диска дальше, чем стрелка.
float meleeCohesionRadius = 5.6f;
float meleeSeekRadius = 7.0f; // дальше этого боец за целью не идёт вообще
// Штраф за уже занятую цель, в юнитах дистанции. Двое с клинками, вцепившиеся
// в одного, половину взмахов обрывают друг об друга — пусть расходятся.
float meleeClaimPenalty = 3.0f;
// Сколько держится связка после удара. Не успел продолжить — начинаешь
// серию заново с первого удара.
float meleeComboWindow = 1.10f;
// --- живучесть отряда и тварей -------------------------------------------
// У мишеней приёмки эти числа не работают: DUMMY не бьёт и не ходит,
// поэтому --accept от них не зависит вообще.
float agentHP = 100.0f; // запас бойца до брони и оберегов
float enemyDamageScale = 1.0f; // общая сложность: во сколько раз больно
float enemyRespawn = 16.0f; // через сколько тварь возвращается
float enemyLightPower = 0.14f; // как ярко тлеют глаза в темноте
// Сколько цель «светится» после попадания. Единственный признак, по которому
// в темноте видно, что отряд по кому-то реально работает, а не мажет.
float targetHurtTime = 0.22f;
// --- расход ------------------------------------------------------------------
// Патроны на вылазку. Не «на этаж»: запас едет вниз вместе с отрядом, и
// решение «ещё глубже или наверх» упирается именно в него.
float startAmmo = 120.0f;
// При какой доле здоровья боец сам возьмётся за бинт и на каком удалении
// от ближайшей твари это ему позволено. Лечиться в бою нельзя: перевязка
// посреди схватки обесценила бы урон.
float healAtFrac = 0.55f;
float healSafeDist = 9.0f;
// --- спуск, находки, опыт ---------------------------------------------------
// Ничего из этого не работает в песочнице приёмки: там нет ни лута, ни
// спуска, и `--accept` этих чисел не касается.
//
// Радиус подбора заметно больше тела бойца: подбор пассивный, и промазать
// мимо лежащего предмета, пройдя вплотную, игрок не должен — он ведь по
// нему не кликал.
float lootPickupRadius = 0.65f;
// На каком расстоянии от точки спуска отряд считается дошедшим. Меряется
// по ЯКОРЮ, а не по бойцам: спуск — это решение игрока, а не то, что
// случается от того, что один боец забрёл не туда.
float descendRadius = 1.10f;
// Сколько опыта даёт единица опасности убитого. Опыт этажа сверх этого
// множится на xpScale из кривой глубины (engine/progression.h).
float xpPerDanger = 10.0f;
// --- разрушаемая обстановка ------------------------------------------------
// Радиус, в котором ОДНО ПРИСУТСТВИЕ живой твари запрещает бойцу работать
// по хламу. Намеренно шире дальности любого ствола: привязать его к дальности
// значило бы «пока не достаю — стреляю по ящику», и боец встречал бы
// подошедшую тварь с пустым магазином. Ящик ломают, когда вокруг ЧИСТО.
float propYieldRadius = 15.0f;
// --- атаки тварей ---------------------------------------------------------
// Форма каждой атаки (сектор, радиус круга, длина рывка) — свойство ВИДА и
// живёт в EnemyDef (sim/target.cpp). Здесь только то, что общее для всех:
// именно этими двумя ручками правится «успеваю ли я среагировать».
//
// enemyWindupScale — множитель ВСЕХ телеграфов сразу. Опускать его ниже
// ~0.5 нечестно: замах становится короче времени реакции, и смерть бойца
// снова выглядит случайной.
float enemyWindupScale = 1.0f;
// Сколько захват держится без подтверждения от твари. Тварь каждый шаг
// продлевает его на эту величину; убили её — боец освобождается сам, и
// ни bullet.cpp, ни клинку не приходится знать про захват вообще.
float enemyGrabHold = 0.15f;
// мишени
float targetHP = 500.0f;
float targetRespawn = 3.0f;
float targetRadius = 0.34f;
float targetKnockDamp = 9.0f; // гашение импульса от удара
float targetReturnRate= 5.0f; // возврат на своё место
// Общий множитель сопротивления отбросу. Само сопротивление у каждой мишени
// своё (sim/target.cpp): тяжёлую клинком не сдвинуть, лёгкая улетает.
float knockResistScale = 1.0f;
// отряд
float holdDelay = 0.22f; // сек без ввода -> HOLDING
float cohesionRadius = 2.1f; // стрелки держатся кучнее
float minEngageDist = 1.6f; // отмасштабировано под дальность 7.2
float formationSpacing = 1.0f; // строй плотнее
float cameraSmooth = 9.0f;
float anchorRadius = 0.3f; // якорь тоже скользит вдоль стен
// Диск отзывчивее бойцов: команда читается мгновенно, а догоняют её ногами.
float anchorSpeed = 3.3f;
float anchorSpeedFast = 4.8f;
float anchorAccel = 20.0f; // разгон/торможение курсора, юнитов/сек^2
// Поводок УПРУГИЙ, а не жёсткий: без него быстрый диск просто отрывается от
// отряда и теряет смысл, но жёсткий упор намертво фиксирует дистанцию, и по
// ней уже не отличить «подправил направление» от «тяну изо всех сил».
// anchorLeash — мягкая граница: до неё курсор идёт свободно;
// anchorLeashMax — жёсткий предел: страховка, до неё обычно не доходит;
// leashDrag — насколько тяжелеет курсор между границами (0..1).
float anchorLeash = 2.5f;
float anchorLeashMax = 4.2f;
float leashDrag = 0.75f; // на жёсткой границе остаётся 25% скорости
// --- натяжение поводка (см. docs/05-squad-ai.md) --------------------------
// Курсор упёрся в поводок, а игрок продолжает давить -> он хочет идти
// быстрее, чем отряд шагает. Это и есть команда «бегом»: отряд ускоряется,
// строй растягивается, а на верхней трети шкалы огонь прекращается вовсе.
float urgeStartFrac = 0.50f; // с какой доли поводка начинается натяжение
// Натяжение набирается быстро, но НЕ мгновенно, и порог спринта поднят.
// При urgeAttack 5.2 отряд уходил в спринт от любого движения курсора, а
// спринт растягивает строй и выключает огонь: в коридорах это выглядело как
// «отряд застрял». Поймано автопилотом (--play): первый этаж переставал
// проходиться вообще.
float urgeAttack = 3.4f; // как быстро натяжение набирается
float urgeRelease = 3.4f; // и как быстро отпускает
float sprintAt = 0.95f; // выше — спринт: бежим, не атакуем.
// Порог высокий намеренно: при удержании направления натяжение садится
// примерно на 0.54, то есть отряд идёт быстрым шагом И ПРОДОЛЖАЕТ работать
// по целям. Спринт остаётся тем, чем должен быть, — отдельным решением
// (Shift) или следствием того, что курсор реально уехал.
// --- дыхание строя --------------------------------------------------------
float holdSpacing = 0.90f; // на стоянке отряд собирается
// Строй в тесноте: во сколько раз сжимается вбок и вытягивается вдоль хода.
// 0.28 — это колонна: пять тел проходят в дверь шириной в одну клетку.
float tightSpacing = 0.28f;
float tightStretch = 1.60f;
float sprintSpacing = 1.30f; // в беге расходится
float sprintStretch = 1.55f; // и вытягивается вдоль движения
float formMorphRate = 4.0f; // скорость перестроения, 1/сек
// решатель огневых позиций (6.5)
float solveInterval = 0.25f;
float commitTime = 0.6f;
float hysteresis = 15.0f;
float ring1 = 0.7f; // кольца решателя сжаты вслед за строем
float ring2 = 1.4f;
float ring3 = 2.1f;
float candMinAlly = 0.60f; // минимум до другого агента (было МЕНЬШЕ диаметра тела)
float sLaneClear = 100.0f;
float sCoverage = 25.0f;
float sBlockAlly = 35.0f;
float sDistCost = 9.0f;
float sCohesionCost = 24.0f; // отбегать от отряда дорого
float sTooClose = 60.0f;
// Жёсткая граница огневой позиции: дальше cohesionRadius + postLeash от диска
// кандидат просто отбраковывается. Одной цены мало — линия огня стоит 100
// очков, и за неё решатель готов уйти сколько угодно далеко. А отряд, из
// которого стрелок ушёл на пять метров, отрядом быть перестаёт.
float postLeash = 1.0f;
// расступание из чужих линий (6.6)
float laneAvoidStrength = 7.0f;
// Ширина РЕЗЕРВА линии относительно порога «тело в линии» из 6.3.
// >1 означает: агента отжимает ДО того, как он влез в чужой сектор.
// Правило стрельбы (6.3) от этого не меняется — см. docs/08-decisions.md.
float laneReserveScale = 2.0f;
// ручное прицеливание (6.8)
float aimConeDeg = 20.0f;
float aimTurnRateDeg = 110.0f; // прицел ведётся тяжелее
// метрики
float uptimeWindow = 10.0f; // окно FIRE UPTIME, сек
float settleQuorum = 4.0f; // сколько агентов нужно для SETTLE TIME
// --- свет и туман войны (render-only, на симуляцию не влияет) -----------
float sightRadius = 7.6f; // где обзор гаснет полностью
float sightFullRadius = 2.2f; // дальше сразу начинается спад: свет должен таять
float sightConeDeg = 100.0f; // конус взгляда агента
float sightConeSoftDeg = 22.0f; // мягкий край конуса
float sightPersonalRadius= 1.9f; // круговой «личный» ореол вокруг агента
float sightBrightness = 0.86f; // яркость видимого; остаток отдан вспышкам
float fogMemoryLevel = 0.13f; // как тускло помнится уже увиденное
float fogWallBleed = 0.92f; // затухание света вглубь блока стен
float fogFadeRate = 9.0f; // сглаживание во времени, чтобы не щёлкало
float fogEntityReveal = 0.30f; // ниже этого света сущность не показывается
float eyeRevealLight = 0.05f; // глаз мишени тлеет почти в полной темноте
float eyeRadiusPx = 3.0f;
float eyeLiftPx = 11.0f; // на уровне «головы» капсулы
float agentMinLight = 0.34f; // свои читаемы всегда: игрок обязан их различать
// вспышки выстрелов как источники света
float flashPower = 0.55f;
float flashRadius = 4.0f;
float flashTime = 0.11f;
// рампа освещения (меняет саму таблицу — требует перепечь)
float lightGamma = 1.45f;
float lightMaxGain = 1.25f;
// --- эффекты стрельбы ---------------------------------------------------
float fxIntensity = 1.0f;
float tracerLength = 1.30f; // длина светящегося штриха, юнитов мира
float bulletLiftPx = 8.0f; // пуля летит на уровне груди, не по земле
float muzzleTime = 0.07f;
float muzzleRadiusPx = 5.0f;
float sparkTime = 0.16f;
float sparkRadiusPx = 4.0f;
float bladeLiftPx = 9.0f; // дуга метёт на уровне груди, не по земле
float bladeTrailFrac = 0.80f; // какая доля пройденной дуги ещё светится
float meleeShake = 1.4f; // отдача камеры на попадание клинком
// --- напряжение и постобработка -----------------------------------------
float dreadRadius = 7.0f; // с какой дистанции мишень начинает пугать
float dreadAttack = 6.0f; // страх набегает быстро
float dreadRelease = 0.5f; // отпускает медленно
float vigStartCalm = 0.42f; // где начинается виньетка
float vigStartTense = 0.20f;
float vigDepthCalm = 0.55f; // насколько темнеет край
float vigDepthTense = 0.85f;
float grainCalm = 6.0f; // амплитуда зерна (макс 127)
float grainTense = 16.0f;
float heartBpmCalm = 58.0f;
float heartBpmTense = 132.0f;
float heartAmp = 0.10f; // насколько сердцебиение давит картинку
float shotFlash = 0.12f; // подсветка кадра на выстрел
float shakePerShot = 0.55f; // отдача камеры, пикселей на выстрел
float shakeMax = 3.0f;
float shakeDecay = 9.0f;
// --- кровь (render-only, декали запекаются в спрайты пола) ---------------
float bloodAmount = 1.0f; // общий множитель размера пятен, 0 = выключить
float bloodRadiusHit = 0.26f; // пуля
float bloodRadiusMelee = 0.50f; // клинок
float bloodRadiusKill = 0.78f; // смерть: лужа
float bloodDrops = 5.0f; // сколько брызг летит по направлению удара
float bloodOpacity = 0.92f; // насколько густо пятно перекрывает пол
// --- дымка и пепел в воздухе (render-only) --------------------------------
float hazeAmount = 0.34f; // сила дымки, 0 = выключить
float hazeScale = 1.0f; // крупность клубов
float hazeDrift = 0.05f; // скорость сноса, экранных ячеек/сек
float hazeGate = 24.0f; // ниже этой яркости дымки нет: тьма остаётся тьмой
float ashDensity = 0.75f; // пылинок на тайл
float ashSpeed = 0.30f; // скорость дрейфа пепла
float ashBrightness = 0.60f;
float ashLiftPx = 13.0f; // на какой высоте висит взвесь
// флаги
bool freeFireSecondary = true; // 6.8: стрелять по вторичным целям в движении
bool friendlyFire = true; // 6.9: всегда true, тумблер только для проверки
};
// --- связка ударов ближнего боя ----------------------------------------------
//
// Чередование «влево-вправо» — это не связка, а метроном: два удара, между
// которыми нет разницы ни на глаз, ни по последствиям. Связка обязана иметь
// ФОРМУ: быстрый вход, злой возврат, тяжёлое завершение.
//
// Множители, а не абсолютные числа: базовые характеристики клинка остаются
// одним набором в Tuning и продолжают крутиться на ходу, а связка их гнёт.
struct MeleeComboStep
{
const char* name;
float arcMul; // ширина сектора
float dmgMul;
float knockMul;
float windupMul; // замах — телеграф; у тяжёлого удара он длиннее
float strikeMul; // активное окно
float recoverMul; // и расплата после
float intervalMul; // пауза до следующего удара связки
bool flipSide; // менять сторону захода лезвия
};
inline const MeleeComboStep MELEE_COMBO[] = {
// 1. Косой заход. Обычный удар, с него всё начинается.
{"slash", 1.00f, 1.00f, 1.00f, 1.00f, 1.00f, 1.00f, 1.00f, true},
// 2. Возврат с обратной стороны. Быстрее и почти без паузы — именно это
// читается как «связка», а не как два отдельных удара.
{"backhand",0.85f, 1.00f, 0.85f, 0.55f, 0.80f, 0.70f, 0.50f, true},
// 3. Раскрутка. Широкая, тяжёлая, с долгим восстановлением: в тесноте её
// просто не дадут начать — сектор не будет чист, и это правильная цена.
{"spin", 1.70f, 1.75f, 2.30f, 1.50f, 1.40f, 1.70f, 1.35f, false},
};
inline constexpr int MELEE_COMBO_STEPS = int(sizeof(MELEE_COMBO) / sizeof(MELEE_COMBO[0]));
inline Tuning g_tune;
// Максимально возможная скорость агента НА ХОДУ. Проверка спуска на время полёта
// пули обязана считать по НЕЙ, иначе рывок союзника не учитывается.
//
// Рывок бойца ближнего боя сюда намеренно не входит: он не «манёвр», а прямой
// отрезок с уже известным направлением, и AllyInterceptsShot проверяет его
// точно — отдельной экстраполяцией (см. ai/firing_solver.cpp).
inline float MaxAgentSpeed() { return std::max(g_tune.moveSpeed, g_tune.moveSpeedFast); }
// --- таблица параметров для подкрутки в оверлее ------------------------------
struct TunableParam
{
const char* name;
float Tuning::* member;
float step;
float minV;
float maxV;
};
inline const TunableParam TUNABLES[] = {
{"safetyMargin", &Tuning::safetyMargin, 0.01f, 0.0f, 1.0f},
{"laneHalfWidth", &Tuning::laneHalfWidth, 0.01f, 0.0f, 0.5f},
{"bodyRadius", &Tuning::bodyRadius, 0.01f, 0.1f, 0.8f},
{"wLaneAvoid", &Tuning::wLaneAvoid, 0.1f, 0.0f, 10.0f},
{"laneAvoidStr", &Tuning::laneAvoidStrength,0.25f, 0.0f, 30.0f},
{"laneReserve", &Tuning::laneReserveScale, 0.05f, 1.0f, 4.0f},
{"predictSlack", &Tuning::shotPredictSlack, 0.05f, 0.0f, 2.0f},
{"wSeparation", &Tuning::wSeparation, 0.1f, 0.0f, 10.0f},
{"steerAccel", &Tuning::steerAccel, 1.0f, 2.0f, 60.0f},
{"formTurnRate", &Tuning::formTurnRate, 0.5f, 0.5f, 30.0f},
{"straggleDist", &Tuning::straggleDist, 0.2f, 1.0f, 12.0f},
{"navStuckTime", &Tuning::navStuckTime, 0.05f, 0.05f, 3.0f},
{"navStuckSpeed", &Tuning::navStuckSpeed, 0.05f, 0.02f, 1.0f},
{"navGoalReached", &Tuning::navGoalReached, 0.05f, 0.05f, 2.0f},
{"moveSpeed", &Tuning::moveSpeed, 0.1f, 0.5f, 10.0f},
{"moveSpeedFast", &Tuning::moveSpeedFast, 0.1f, 0.5f, 12.0f},
{"weaponRange", &Tuning::weaponRange, 0.25f, 2.0f, 20.0f},
{"fireInterval", &Tuning::fireInterval, 0.01f, 0.05f, 2.0f},
{"bulletSpeed", &Tuning::bulletSpeed, 1.0f, 5.0f, 60.0f},
{"solveInterval", &Tuning::solveInterval, 0.05f, 0.05f, 2.0f},
{"commitTime", &Tuning::commitTime, 0.05f, 0.0f, 3.0f},
{"hysteresis", &Tuning::hysteresis, 1.0f, 0.0f, 100.0f},
{"cohesionRadius", &Tuning::cohesionRadius, 0.1f, 1.0f, 12.0f},
{"minEngageDist", &Tuning::minEngageDist, 0.1f, 0.0f, 8.0f},
{"sLaneClear", &Tuning::sLaneClear, 5.0f, 0.0f, 400.0f},
{"sCoverage", &Tuning::sCoverage, 1.0f, 0.0f, 200.0f},
{"sBlockAlly", &Tuning::sBlockAlly, 1.0f, 0.0f, 200.0f},
{"sDistCost", &Tuning::sDistCost, 0.5f, 0.0f, 100.0f},
{"sCohesionCost", &Tuning::sCohesionCost, 0.5f, 0.0f, 100.0f},
{"sTooClose", &Tuning::sTooClose, 2.0f, 0.0f, 300.0f},
{"postLeash", &Tuning::postLeash, 0.1f, 0.0f, 10.0f},
{"formSpacing", &Tuning::formationSpacing, 0.05f, 0.4f, 4.0f},
{"tightSpacing", &Tuning::tightSpacing, 0.02f, 0.1f, 1.0f},
{"anchorSpeed", &Tuning::anchorSpeed, 0.1f, 0.5f, 12.0f},
{"anchorSpeedFast",&Tuning::anchorSpeedFast, 0.1f, 0.5f, 16.0f},
{"anchorLeash", &Tuning::anchorLeash, 0.1f, 0.2f, 12.0f},
{"anchorLeashMax", &Tuning::anchorLeashMax, 0.1f, 0.4f, 16.0f},
{"leashDrag", &Tuning::leashDrag, 0.05f, 0.0f, 1.0f},
{"anchorAccel", &Tuning::anchorAccel, 1.0f, 2.0f, 80.0f},
// --- натяжение и строй ---
{"urgeStart", &Tuning::urgeStartFrac, 0.05f, 0.0f, 0.95f},
{"urgeAttack", &Tuning::urgeAttack, 0.2f, 0.1f, 20.0f},
{"urgeRelease", &Tuning::urgeRelease, 0.2f, 0.1f, 20.0f},
{"sprintAt", &Tuning::sprintAt, 0.02f, 0.05f, 1.0f},
{"holdSpacing", &Tuning::holdSpacing, 0.02f, 0.4f, 2.0f},
{"sprintSpacing", &Tuning::sprintSpacing, 0.02f, 0.4f, 3.0f},
{"sprintStretch", &Tuning::sprintStretch, 0.05f, 0.5f, 4.0f},
{"formMorph", &Tuning::formMorphRate, 0.25f, 0.25f, 20.0f},
// --- разброс и перезарядка ---
{"spreadStand", &Tuning::spreadStandDeg, 0.1f, 0.0f, 20.0f},
{"spreadMove", &Tuning::spreadMoveDeg, 0.1f, 0.0f, 30.0f},
{"spreadRecoil", &Tuning::spreadRecoilDeg, 0.05f, 0.0f, 10.0f},
{"spreadRecMax", &Tuning::spreadRecoilMax, 0.1f, 0.0f, 20.0f},
{"spreadDecay", &Tuning::spreadRecoilDecay,0.2f, 0.1f, 40.0f},
{"spreadScale", &Tuning::spreadScale, 0.05f, 0.0f, 4.0f},
{"magazine", &Tuning::magazineRifle, 1.0f, 1.0f, 200.0f},
{"reloadTime", &Tuning::reloadRifle, 0.05f, 0.1f, 8.0f},
{"bulletKnock", &Tuning::bulletKnock, 0.05f, 0.0f, 6.0f},
{"knockResist", &Tuning::knockResistScale, 0.05f, 0.0f, 2.0f},
// --- живучесть и сложность ---
{"agentHP", &Tuning::agentHP, 5.0f, 10.0f, 500.0f},
{"enemyDamage", &Tuning::enemyDamageScale, 0.05f, 0.0f, 4.0f},
{"enemyRespawn", &Tuning::enemyRespawn, 1.0f, 1.0f, 300.0f},
{"enemyWindup", &Tuning::enemyWindupScale, 0.05f, 0.25f, 3.0f},
{"enemyGrabHold", &Tuning::enemyGrabHold, 0.01f, 0.02f, 1.0f},
{"propYield", &Tuning::propYieldRadius, 0.5f, 0.0f, 64.0f},
// --- ближний бой ---
{"meleeReach", &Tuning::meleeReach, 0.05f, 0.4f, 4.0f},
{"meleeArc", &Tuning::meleeArcDeg, 5.0f, 20.0f, 300.0f},
{"meleeWindup", &Tuning::meleeWindup, 0.01f, 0.02f, 1.0f},
{"meleeStrike", &Tuning::meleeStrike, 0.01f, 0.02f, 1.0f},
{"meleeRecover", &Tuning::meleeRecover, 0.01f, 0.0f, 1.5f},
{"meleeInterval", &Tuning::meleeInterval, 0.02f, 0.1f, 3.0f},
{"meleeDamage", &Tuning::meleeDamage, 5.0f, 1.0f, 999.0f},
{"meleeLunge", &Tuning::meleeLungeSpeed, 0.1f, 0.0f, 12.0f},
{"meleeKnock", &Tuning::meleeKnockback, 0.1f, 0.0f, 12.0f},
{"meleeCohesion", &Tuning::meleeCohesionRadius, 0.2f, 1.0f, 20.0f},
{"meleeAllyMargin",&Tuning::meleeAllyMargin, 0.02f, 0.0f, 1.0f},
{"meleePredict", &Tuning::meleeStartPredict,0.1f, 0.0f, 3.0f},
{"meleeReserve", &Tuning::meleeReserveFrac, 0.05f, 0.0f, 1.5f},
{"meleeShake", &Tuning::meleeShake, 0.1f, 0.0f, 6.0f},
// --- хоррор-часть ---
{"sightRadius", &Tuning::sightRadius, 0.25f, 2.0f, 24.0f},
{"sightCone", &Tuning::sightConeDeg, 5.0f, 20.0f, 360.0f},
{"sightPersonal", &Tuning::sightPersonalRadius, 0.1f, 0.5f, 10.0f},
{"fogMemory", &Tuning::fogMemoryLevel, 0.02f, 0.0f, 1.0f},
{"fogReveal", &Tuning::fogEntityReveal, 0.02f, 0.0f, 1.0f},
{"flashPower", &Tuning::flashPower, 0.05f, 0.0f, 2.0f},
{"lightGamma", &Tuning::lightGamma, 0.05f, 0.3f, 4.0f},
{"lightGain", &Tuning::lightMaxGain, 0.05f, 0.2f, 3.0f},
{"fxIntensity", &Tuning::fxIntensity, 0.1f, 0.0f, 4.0f},
{"tracerLength", &Tuning::tracerLength, 0.1f, 0.1f, 6.0f},
{"grainTense", &Tuning::grainTense, 1.0f, 0.0f, 100.0f},
{"vigDepthTense", &Tuning::vigDepthTense, 0.05f, 0.0f, 1.0f},
{"dreadRadius", &Tuning::dreadRadius, 0.5f, 1.0f, 20.0f},
{"shakePerShot", &Tuning::shakePerShot, 0.05f, 0.0f, 4.0f},
// --- кровь, дымка, пепел ---
{"bloodAmount", &Tuning::bloodAmount, 0.05f, 0.0f, 3.0f},
{"bloodOpacity", &Tuning::bloodOpacity, 0.02f, 0.0f, 1.0f},
{"bloodDrops", &Tuning::bloodDrops, 1.0f, 0.0f, 16.0f},
{"hazeAmount", &Tuning::hazeAmount, 0.02f, 0.0f, 2.0f},
{"hazeScale", &Tuning::hazeScale, 0.05f, 0.2f, 4.0f},
{"hazeDrift", &Tuning::hazeDrift, 0.01f, 0.0f, 1.0f},
{"hazeGate", &Tuning::hazeGate, 1.0f, 0.0f, 128.0f},
{"ashDensity", &Tuning::ashDensity, 0.05f, 0.0f, 4.0f},
{"ashSpeed", &Tuning::ashSpeed, 0.05f, 0.0f, 3.0f},
{"ashBright", &Tuning::ashBrightness, 0.05f, 0.0f, 2.0f},
{"ashLift", &Tuning::ashLiftPx, 1.0f, 0.0f, 40.0f},
};
inline constexpr int TUNABLE_COUNT = int(sizeof(TUNABLES) / sizeof(TUNABLES[0]));