# 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 <id>` (удобно после `-list-enemies`).
- **Один шаблон по уникальному slug `type`** — `-enemy-type <type>` (например `wolf_l5_5_meadow`, не короткое имя `wolf`).
- **Все шаблоны с семейством `archetype`** — `-enemy-archetype <name>` (колонка `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.
