listEnemies=flag.Bool("list-enemies",false,"print enemy archetypes from DB and exit")
filter=flag.String("filter","","optional substring filter for -list-enemies (id/type/name)")
listLimit=flag.Int("limit",50,"max rows for -list-enemies")
listEnemies=flag.Bool("list-enemies",false,"print enemy rows from DB and exit")
listArchetypes=flag.Bool("list-archetypes",false,"print distinct enemies.archetype values from DB and exit")
filter=flag.String("filter","","optional substring filter for -list-enemies (id/type/name/archetype)")
listLimit=flag.Int("limit",50,"max rows for -list-enemies")
enemyIDFlag=flag.Int64("enemy-id",0,"if set, balance only this enemies.id row")
enemyTypeFlag=flag.String("enemy-type","","if set, balance only this archetype (type string); ignored if -enemy-id is set")
enemyIDFlag=flag.Int64("enemy-id",0,"if set, balance only this enemies.id row")
enemyTypeFlag=flag.String("enemy-type","","if set, balance only this template slug (enemies.type); ignored if -enemy-id is set")
enemyArchetypeFlag=flag.String("enemy-archetype","","if set without -enemy-id/-enemy-type, include all DB rows with this enemies.archetype; with -enemy-type, must match template archetype")
configJSON=flag.String("config","","optional JSON file: partial enemy objects keyed by type string, merged over DB templates")
@ -51,7 +55,19 @@ func main() {
tolerancePct=flag.Float64("tolerance-pct",10,"deviation from target (percent); 10%% with target 330 → band [297s,363s]")
maxHeroHpPct=flag.Float64("max-hero-hp-pct-on-win",60,"legacy mode: median hero HP%% after victory must be <= this (0-100)")
minWinRate=flag.Float64("min-win-rate",0.28,"stop raising enemy attack if win rate falls below this (legacy); grid: median win rate floor")
adjustEnemies=flag.Bool("adjust-enemies",true,"when false, only simulate and report metrics; do not tune or print UPDATE SQL")
gearCheck=flag.Bool("gear-check",false,"compare baseline (common) vs max (legendary) reference gear per level; exits non-zero on violations")
gearCheckMaxSpeedupPct=flag.Float64("gear-check-max-speedup-pct",20,"max allowed kill speedup with max gear vs baseline (percent); duration max >= baseline * (1 - pct/100)")
gearCheckMaxHeroHpPct=flag.Float64("gear-check-max-hero-hp-pct",75,"max median hero HP%% on wins with max gear (0-100)")
gearCheckLevelMin=flag.Int("gear-check-level-min",0,"gear-check: min hero/enemy level inclusive (0 = use template min_level)")
gearCheckLevelMax=flag.Int("gear-check-level-max",0,"gear-check: max hero/enemy level inclusive (0 = use template max_level)")
gearCheckStrict=flag.Bool("gear-check-strict",false,"gear-check: baseline must get wins (SKIP → FAIL) so vacuous passes are impossible")
gearBase=flag.String("gear-base","db","gear catalog source before overlay: db (weapons/armor/equipment_items) or code (embedded defaults only)")
gearOverlay=flag.String("gear-overlay","","JSON file: partial GearFamily patches keyed by item name or \"slot:name\"; merged over catalog for simulation only")
gearPrintSQL=flag.Bool("gear-print-sql",false,"print SQL UPDATEs for gear/weapons/armor/equipment_items from patched catalog; requires -gear-overlay; then exit")
{Name:"Whisper of the Void",Type:"light",Rarity:RarityLegendary,Defense:16,SpeedModifier:1.15,AgilityBonus:18,SetName:"Assassin's Set",SpecialEffect:"crit_bonus"},
// Light armor — same base Defense per class; rarity scaling via M(rarity) in ScalePrimary.
{Name:"Whisper of the Void",Type:"light",Rarity:RarityLegendary,Defense:2,SpeedModifier:1.15,AgilityBonus:18,SetName:"Assassin's Set",SpecialEffect:"crit_bonus"},
Утилита `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-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`).
### Калибровка без полного перебора
Выберите типичных монстров (`-enemy-type`, `-gear-check-level-*`) и итерируйте **JSON оверлей**, пока `-gear-check` не проходит; затем зафиксируйте изменения миграцией. Глобальные множители редкости остаются в `runtime_config` / `ScalePrimary`.
## Режим по умолчанию: сетка (`-grid`, по умолчанию `true`)
Для каждого архетипа строится сетка сценариев:
@ -56,11 +125,12 @@ go build -o balanceall ./cmd/balanceall
## Область прогона
- **Все архетипы из БД** — по умолчанию: строки из `enemies` в порядке `ORDER BY min_level, type` (как `ListEnemyRows`).
- **Один монстр по `enemies.id`** — `-enemy-id <id>` (удобно после `go run ./cmd/balanceall -list-enemies`).
- **Один архетип по строке `type`** — `-enemy-type <type>` (в таблице `enemies` это строка вроде `wolf`, не catalog id `enemy.wolf_forest`).
- **Все шаблоны из БД** — по умолчанию: строки из `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-id` и `-enemy-type`. Если заданы **`-enemy-type` и `-enemy-archetype`**, slug должен принадлежать этому семейству (иначе ошибка). С**`-enemy-id`** можно указать **`-enemy-archetype`** для проверки согласованности строки в БД.
## JSON-оверлей (`-config`)
@ -93,7 +163,9 @@ go build -o balanceall ./cmd/balanceall
Одинаков для всех типов предметов, для воспроизводимости:
Одинаков для всех типов предметов, для воспроизводимости. Якорь:**Legendary = +40% к Common** при том же `ilvl`. Базовые числа семейства в каталоге (`weapons.damage`, `armor.defense`, …) задаются **одинаковыми** для линейки типа; разброс редкости даёт только `M(rarity)` (без двойного масштабирования в БД).
| Редкость | `M(rarity)` |
|----------|----------------|
| Common | `1.00` |
| Uncommon | `1.12` |
| Rare | `1.30` |
| Epic | `1.52` |
| Legendary | `1.78` |
| Uncommon | `1.0878` |
| Rare | `1.1832` |
| Epic | `1.2871` |
| Legendary | `1.40` |
#### 6.4.3 Первичные статы предмета (урон / защита от предмета)
`basePrimary` — целое из каталога для семейства предмета на «эталоне» `ilvl = 1`, `Common`.
**Свойство баланса:** при тех же `basePrimary` возможны ситуации, когда **редкий предмет с меньшим `ilvl` даёт больше или столько же**, чем **обычный с большим `ilvl`**, потому что `M(rarity)` растёт быстрее, чем `L(ilvl)` в типичных диапазонах дропа. Примеры (без привязки к слоту, `basePrimary = 10`):
**Свойство баланса:** при данной кривой `ilvl`**уровень предмета даёт более сильный вклад**, чем раньше, а редкость заметно усиливает предмет **внутри одного уровня**. Пример (без привязки к слоту, `basePrimary = 10`):