# balanceall — CLI баланса монстров Утилита `backend/cmd/balanceall` прогоняет **архетипы монстров** из **PostgreSQL** (таблица `enemies`) через то же ядро боя, что онлайн/оффлайн (`game.ResolveCombatToEndWithDuration`), на **референсном герое** (`game.NewReferenceHeroForBalance`). ## Режим метрик без подбора врагов Если нужно **оценивать силу предметов**, не меняя таблицу `enemies`, используйте флаг: - `-adjust-enemies=false` В этом режиме утилита **не подбирает** `hp/attack` у монстров и **не печатает** `UPDATE` SQL. Она только прогоняет сетку и печатает метрики (медианы длительности, HP героя, win rate), что удобно для подгонки формулы `ScalePrimary`, `refGearBase` и параметров лута. Пример: ```bash cd backend set DATABASE_URL=postgres://... go run ./cmd/balanceall -adjust-enemies=false ``` ## Проверка баланса шмота (`-gear-check`) Сравнивает **референсного героя в common** (`ReferenceGearBaseline`: меч + средняя броня) и **в legendary** (`ReferenceGearMax`) на каждом уровне в полосе `min_level..max_level` выбранных шаблонов монстров. Для каждого уровня: - **Скорость убийства:** медиана длительности победы в лучшем шмоте не должна быть короче медианы в базовом более чем на `-gear-check-max-speedup-pct` процентов (по умолчанию **20%** — то есть `dur_max >= dur_baseline × 0.80`). - **HP после боя:** медиана доли HP героя после побед в лучшем шмоте не выше `-gear-check-max-hero-hp-pct` (по умолчанию **75%**). При нарушении печатаются строки `FAIL`, процесс завершаётся с кодом **1**. Подбор врагов и SQL не выполняются; сетка `-gear-variants` в этом режиме не используется. Дополнительно: - **`-gear-check-level-min` / `-gear-check-level-max`** — пересечь полосу уровней шаблона с нужным диапазоном (по умолчанию `0` = взять `min_level` / `max_level` шаблона). Удобно для калибровки на «типичном» уровне или mid/late без прогона всей полосы. - **`-gear-check-strict`** — если у baseline нет медианы длительности победы (нет побед), считать **FAIL** вместо `SKIP`, и пустое пересечение уровней тоже **FAIL**. Нужен, чтобы не получать ложный `PASSED` без реальных сравнений. Пример — один шаблон, уровни 5–15, строгий режим: ```bash go run ./cmd/balanceall -gear-check -enemy-type wolf_l5_5_meadow -gear-check-level-min 5 -gear-check-level-max 15 -gear-check-strict -iterations 200 ``` Пример — все строки БД с семейством `wolf` (колонка `enemies.archetype`): ```bash go run ./cmd/balanceall -gear-check -enemy-archetype wolf -iterations 200 ``` Список значений `archetype` из БД: **`go run ./cmd/balanceall -list-archetypes`**. ### Локальный оверлей каталога (`-gear-overlay`) Каталог предметов сначала загружается из БД (`weapons`, `armor`, `equipment_items`) или из **встроенного** набора (`-gear-base=code`), затем в памяти накладывается JSON с частичными полями `GearFamily` — по ключу **имя предмета** или **`slot:name`** (например `main_hand:Soul Reaver`, `chest:Chainmail`). Поля: `basePrimary`, `speedModifier`, `baseCrit`, `agilityBonus`, `statType`. Так можно крутить баланс **без** записи в БД, по аналогии с `-config` для монстров. Пример: ```bash go run ./cmd/balanceall -gear-check -enemy-type wolf_l2_2_forest -gear-check-level-min 2 -gear-check-level-max 2 ^ -gear-overlay ./cmd/balanceall/testdata/gear_overlay_balanced.json -iterations 200 ``` ### SQL после подбора (`-gear-print-sql`) Когда значения в оверлее устраивают (`-gear-check` проходит), сгенерируйте `UPDATE` для `gear`, `weapons`, `armor` (и при необходимости `equipment_items` для не-слотов main_hand/chest): ```bash go run ./cmd/balanceall -gear-overlay ./cmd/balanceall/testdata/gear_overlay_balanced.json -gear-print-sql ``` Команда завершает работу после вывода SQL. Перенесите результат в миграцию (см. пример `backend/migrations/000012_gear_balance_overlay.sql`). После миграций, меняющих `gear` или множители встреч в `runtime_config` (`enemyEncounterStatMultiplier`, `enemyStatMultiplierVsUnequippedHero`), имеет смысл прогнать `-gear-check` и при необходимости сетку с `-adjust-enemies=false`, чтобы увидеть метрики; `BuildEnemyInstanceForLevel` уже учитывает эти множители в симуляциях. ### Калибровка без полного перебора Выберите типичных монстров (`-enemy-type`, `-gear-check-level-*`) и итерируйте **JSON оверлей**, пока `-gear-check` не проходит; затем зафиксируйте изменения миграцией. Глобальные множители редкости остаются в `runtime_config` / `ScalePrimary`. ## Режим по умолчанию: сетка (`-grid`, по умолчанию `true`) Для каждого архетипа строится сетка сценариев: - **Уровни:** для каждого `L` от `min_level` до `max_level` включительно — бой **герой L** против **экземпляра монстра L** (`heroLv == enemyLv`), честный тир. - **Шмот:** `-gear-variants` профилей (по умолчанию **4**): один с **медианным** шмотом, остальные с **rolled** ilvl (как в дропе). Агрегированные цели (как «медиана медиан» по ячейкам): 1. **Длительность** — медиана по ячейкам от **медиан длительности победных** боёв попадает в полосу `[targetSec × (1 − tolerancePct/100), targetSec × (1 + tolerancePct/100)]` (по умолчанию при 330 с и **10%** — примерно **297–363 с**). 2. **HP героя** — медиана по ячейкам от **медиан доли HP после победы** попадает в полосу **`hero-hp-mid` ± `hero-hp-pp` процентных пунктов** (по умолчанию **60% ± 7 п.п.** → **53–67%**). После подбора выполняется смягчение атаки, пока в худшей ячейке есть хотя бы одна победа в выборке (как в прежнем отдельном прототипе сетки). Логика вынесена в `backend/cmd/balanceall/grid.go`. ## Режим legacy (`-grid=false`) Один сценарий на архетип: - Герой и монстр: уровень = середина полосы `(min_level + max_level) / 2`, только **медианный** шмот. - **Длительность** — медиана длительности побед в полосе по `-tolerance-pct`. - **Давление** — медиана оставшегося HP **не выше** `-max-hero-hp-pct-on-win` (при необходимости ослабление до 65/70/75%). ## Подключение к БД Нужен DSN (без БД утилита не запускается): - переменная окружения **`DATABASE_URL`**, или - флаг **`-dsn`** (перекрывает env). Шаблоны подгружаются через `storage.ContentStore.LoadEnemyTemplates` (как в `balancesim`). ## Запуск Из каталога модуля Go: ```bash cd backend set DATABASE_URL=postgres://... go run ./cmd/balanceall [флаги] ``` Сборка: ```bash go build -o balanceall ./cmd/balanceall ./balanceall -iterations 200 ``` ## Область прогона - **Все шаблоны из БД** — по умолчанию: строки из `enemies` в порядке `ORDER BY min_level, archetype, type` (как `ListEnemyRows`). - **Один шаблон по `enemies.id`** — `-enemy-id ` (удобно после `-list-enemies`). - **Один шаблон по уникальному slug `type`** — `-enemy-type ` (например `wolf_l5_5_meadow`, не короткое имя `wolf`). - **Все шаблоны с семейством `archetype`** — `-enemy-archetype ` (колонка `enemies.archetype`, напр. `wolf` — все волки из БД). Нельзя одновременно задавать `-enemy-id` и `-enemy-type`. Если заданы **`-enemy-type` и `-enemy-archetype`**, slug должен принадлежать этому семейству (иначе ошибка). С **`-enemy-id`** можно указать **`-enemy-archetype`** для проверки согласованности строки в БД. ## JSON-оверлей (`-config`) Флаг **`-config path.json`** задаёт файл с объектом верхнего уровня: **ключи — строки `type`**, как в таблице `enemies`. Значение — объект с **любым подмножеством** полей шаблона монстра (имена полей как в JSON у `model.Enemy`: `maxHp`, `attack`, `hpPerLevel`, `specialAbilities`, …). После загрузки из БД данные из файла **накладываются в памяти**: указанное в JSON поле заменяет значение из БД; отсутствующие в JSON поля не трогаются. Неизвестный ключ верхнего уровня (тип, которого нет среди загруженных шаблонов) пропускается с предупреждением в stderr. Пример: ```json { "wolf": { "attack": 12, "attackPerLevel": 1.1 }, "demon_fire": { "maxHp": 800, "hpPerLevel": 45 } } ``` Если в оверлее задан только один из пары `hp` / `maxHp`, второй выравнивается под него для согласованности шаблона. ## Флаги | Флаг | По умолчанию | Смысл | |------|----------------|--------| | `-dsn` | `""` | Postgres DSN; если пусто — берётся `DATABASE_URL`. | | `-enemy-id` | 0 | Только строка с этим `enemies.id`. | | `-enemy-type` | `""` | Только шаблон с этим `enemies.type` (slug). | | `-enemy-archetype` | `""` | Все шаблоны с этим `enemies.archetype`, если не заданы `-enemy-id`/`-enemy-type`. | | `-list-archetypes` | false | Список уникальных `enemies.archetype` и выход. | | `-config` | `""` | Путь к JSON: частичные шаблоны по ключу `type`, поверх БД. | | `-grid` | `true` | Сетка уровней × шмот; `false` — legacy (один уровень, медианный шмот). | | `-gear-variants` | 4 | Режим сетки: число профилей шмота на уровень (1 median + N−1 rolled). | | `-hero-hp-mid` | 60 | Режим сетки: центр полосы HP героя на победах (%). | | `-hero-hp-pp` | 7 | Режим сетки: ±п.п. вокруг `-hero-hp-mid`. | | `-refine` | 2 | Режим сетки: проходы подгонки длительности после атаки. | | `-iterations` | 120 | Число боёв **на ячейку сетки** (grid) или на архетип (legacy). Рекомендуется ≥ 120–200. | | `-seed` | `20260331` | База RNG; на архетип добавляется хеш `type`. | | `-target-sec` | 330 | Центр полосы медианы длительности побед (секунды). | | `-tolerance-pct` | 10 | Полоса вокруг центра; при 330 и 10% → **297–363 с**. | | `-max-hero-hp-pct-on-win` | 60 | Только **legacy**: верхняя граница медианы HP героя после побед (%). | | `-min-win-rate` | 0.35 | Legacy: планка винрейта при накрутке атаки. Сетка: планка **медианного** винрейта по ячейкам. | | `-sql` | `true` | Печатать предлагаемые `UPDATE enemies ...`. | | `-list-enemies` | false | Список архетипов из БД (с колонкой `id`) и выход. | | `-filter` | `""` | Подстрока для `-list-enemies`. | | `-limit` | 50 | Максимум строк для `-list-enemies` (1–500). | | `-adjust-enemies` | `true` | Если `false` — только метрики, без подбора и без SQL. | | `-gear-check` | `false` | Сравнение baseline vs legendary reference gear по уровням; `exit 1` при нарушении порогов. | | `-gear-check-max-speedup-pct` | `20` | Макс. ускорение убийства с лучшим шмотом относительно baseline (%). | | `-gear-check-max-hero-hp-pct` | `75` | Верхняя граница медианы HP героя на победах с лучшим шмотом (%). | | `-gear-check-level-min` | `0` | Нижняя граница уровня для gear-check (`0` = `min_level` шаблона). | | `-gear-check-level-max` | `0` | Верхняя граница уровня для gear-check (`0` = `max_level` шаблона). | | `-gear-check-strict` | `false` | Нет побед у baseline / пустой диапазон уровней → `FAIL`. | | `-gear-base` | `db` | `db` — каталог из БД; `code` — только встроенный каталог до оверлея. | | `-gear-overlay` | `""` | JSON с патчами полей предметов поверх каталога (только память). | | `-gear-print-sql` | `false` | Напечатать `UPDATE` для таблиц каталога и выйти (нужен `-gear-overlay`). | Примеры: ```bash go run ./cmd/balanceall -iterations 200 go run ./cmd/balanceall -enemy-type wolf -iterations 200 # Старый алгоритм (один уровень — середина полосы) go run ./cmd/balanceall -grid=false -enemy-type wolf go run ./cmd/balanceall -list-enemies go run ./cmd/balanceall -sql=false ``` ## Вывод - **Сетка:** для каждого типа — baseline по текущему шаблону, затем `hpScale`/`atkScale`, агрегаты `medOfMed(duration)`, `medOfMed(heroHp%)`, диапазоны по ячейкам, при `-sql` — `UPDATE`. - **Legacy:** как раньше — одна строка метрик и `UPDATE`. ## Связь с репозиторием - Загрузка из БД: `internal/storage/content_store.go` (`LoadEnemyTemplates`, `ListEnemyRows`). - Сетка: `cmd/balanceall/grid.go`. - Проверка шмота: `cmd/balanceall/gear_check.go`. - Одиночная симуляция: `cmd/balancesim` + `DATABASE_URL`. - Краткий снимок для контента: `docs/monster-catalog-balanced-v1.md`. ## Ограничения - Сетка не моделирует все пары (герой L5 vs монстр L1): для одной кривой в БД агрегаты по «честному» тиру (`hero == enemy`) устойчивее. - Элиты с сильным DoT могут требовать ослабления целей или точечной настройки. - Вывод SQL — предложение; источник правды в продакшене — таблица `enemies` после миграций и reload.