|
|
# 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.
|