You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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` без реальных сравнений.
Пример — один шаблон, уровни 515, строгий режим:
```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%** — примерно **297363 с**).
2. **HP героя** — медиана по ячейкам от **медиан доли HP после победы** попадает в полосу
**`hero-hp-mid` ± `hero-hp-pp` процентных пунктов** (по умолчанию **60% ± 7 п.п.****5367%**).
После подбора выполняется смягчение атаки, пока в худшей ячейке есть хотя бы одна победа в выборке (как в прежнем отдельном прототипе сетки).
Логика вынесена в `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 + N1 rolled). |
| `-hero-hp-mid` | 60 | Режим сетки: центр полосы HP героя на победах (%). |
| `-hero-hp-pp` | 7 | Режим сетки: ±п.п. вокруг `-hero-hp-mid`. |
| `-refine` | 2 | Режим сетки: проходы подгонки длительности после атаки. |
| `-iterations` | 120 | Число боёв **на ячейку сетки** (grid) или на архетип (legacy). Рекомендуется ≥ 120200. |
| `-seed` | `20260331` | База RNG; на архетип добавляется хеш `type`. |
| `-target-sec` | 330 | Центр полосы медианы длительности побед (секунды). |
| `-tolerance-pct` | 10 | Полоса вокруг центра; при 330 и 10% → **297363 с**. |
| `-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` (1500). |
| `-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.