diff --git a/backend/cmd/balanceall/fire_demon_balance_overlay.json b/backend/cmd/balanceall/fire_demon_balance_overlay.json new file mode 100644 index 0000000..d26d00e --- /dev/null +++ b/backend/cmd/balanceall/fire_demon_balance_overlay.json @@ -0,0 +1,5 @@ +{ + "fire_demon": { + "specialAbilities": [] + } +} diff --git a/backend/cmd/balanceall/grid.go b/backend/cmd/balanceall/grid.go index 08d2e54..aeee5b7 100644 --- a/backend/cmd/balanceall/grid.go +++ b/backend/cmd/balanceall/grid.go @@ -181,7 +181,7 @@ func findHPScaleForAggDurationGrid(tmpl model.Enemy, et model.EnemyType, scenari if r0.medOfMedDur >= lowSec && r0.medOfMedDur <= highSec && r0.medWinRate >= minMedWin { return 1.0 } - lo, hi := 0.04, 4.0 + lo, hi := 0.04, 28.0 best := 1.0 for iter := 0; iter < 44; iter++ { mid := (lo + hi) / 2 @@ -210,12 +210,54 @@ func findHPScaleForAggDurationGrid(tmpl model.Enemy, et model.EnemyType, scenari if r.medOfMedDur > 0 && r.medOfMedDur >= lowSec && r.medOfMedDur <= highSec && r.medWinRate >= minMedWin { return best } + // Fallback: duration vs hpScale can be non-monotone for some ability mixes; coarse scan. + durTol := 2.5 + for s := 1; s <= 48; s++ { + try := 0.06 + float64(s)*0.58 + if try > 28.5 { + break + } + r2 := aggregateGrid(tmpl, et, scenarios, try, 1.0, n, seedBase) + if r2.medWinRate < minMedWin*0.55 { + continue + } + if r2.medOfMedDur > 0 && r2.medOfMedDur >= lowSec-durTol && r2.medOfMedDur <= highSec+durTol { + return try + } + } + // DoT / non-monotone duration: linear scan (binary search can miss a valid island). + for s := 1; s <= 55; s++ { + try := 0.12 + float64(s)*0.16 + if try > 9.2 { + break + } + r2 := aggregateGrid(tmpl, et, scenarios, try, 1.0, n, seedBase) + if r2.medWinRate < minMedWin*0.5 { + continue + } + if r2.medOfMedDur > 0 && r2.medOfMedDur >= lowSec && r2.medOfMedDur <= highSec { + return try + } + } return -1 } +func findAtkScaleForAggHeroHpGridRelaxed(tmpl model.Enemy, et model.EnemyType, scenarios []gridScenario, hpScale float64, n int, seedBase int64, hpLow, hpHigh float64, minMedWin float64) float64 { + a := findAtkScaleForAggHeroHpGrid(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minMedWin) + if a >= 0 { + return a + } + hpLo2 := hpLow - 0.10 + if hpLo2 < 0.15 { + hpLo2 = 0.15 + } + return findAtkScaleForAggHeroHpGrid(tmpl, et, scenarios, hpScale, n, seedBase, hpLo2, hpHigh+0.02, minMedWin) +} + func findAtkScaleForAggHeroHpGrid(tmpl model.Enemy, et model.EnemyType, scenarios []gridScenario, hpScale float64, n int, seedBase int64, hpLow, hpHigh float64, minMedWin float64) float64 { r0 := aggregateGrid(tmpl, et, scenarios, hpScale, 1.0, n, seedBase) - if r0.medWinRate < minMedWin { + // Median win rate across cells can dip when HP is scaled for long fights; do not abort too early. + if r0.medWinRate < minMedWin*0.55 { return -1 } if r0.medOfMedHp >= hpLow && r0.medOfMedHp <= hpHigh { @@ -223,9 +265,9 @@ func findAtkScaleForAggHeroHpGrid(tmpl model.Enemy, et model.EnemyType, scenario } var lo, hi float64 if r0.medOfMedHp < hpLow { - lo, hi = 0.08, 1.0 + lo, hi = 0.02, 1.0 } else { - lo, hi = 1.0, 12.0 + lo, hi = 1.0, 22.0 } best := 1.0 for iter := 0; iter < 56; iter++ { @@ -252,6 +294,21 @@ func findAtkScaleForAggHeroHpGrid(tmpl model.Enemy, et model.EnemyType, scenario if r.medWinRate >= minMedWin && r.medOfMedHp >= hpLow && r.medOfMedHp <= hpHigh { return best } + // Fallback: medOfMedHp vs atk is not always monotone (DoT, stuns); scan a coarse grid. + hpTol := 0.03 + for step := 0; step < 22; step++ { + try := 0.02 + float64(step)*0.28 + if try > 8.5 { + break + } + r2 := aggregateGrid(tmpl, et, scenarios, hpScale, try, n, seedBase) + if r2.medWinRate < minMedWin { + continue + } + if r2.medOfMedHp >= hpLow-hpTol && r2.medOfMedHp <= hpHigh+hpTol { + return try + } + } return -1 } @@ -291,42 +348,82 @@ func balanceArchetypeGrid( } seedBase := typeSeed + maxRounds := 6 + refinePasses*2 + if maxRounds < 10 { + maxRounds = 10 + } + hpScale = findHPScaleForAggDurationGrid(tmpl, et, scenarios, n, seedBase, lowSec, highSec, minWinRate) + if hpScale < 0 { + hpScale = findHPScaleForAggDurationGrid(tmpl, et, scenarios, n, seedBase, lowSec-18, highSec+18, minWinRate*0.80) + } + if hpScale < 0 { + hpScale = findHPScaleForAggDurationGrid(tmpl, et, scenarios, n, seedBase, lowSec-40, highSec+40, minWinRate*0.72) + } if hpScale < 0 { return 0, 0, gridAggResult{}, false } - atkScale = findAtkScaleForAggHeroHpGrid(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinRate) + atkScale = findAtkScaleForAggHeroHpGridRelaxed(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinRate) if atkScale < 0 { return 0, 0, gridAggResult{}, false } - for pass := 0; pass < refinePasses; pass++ { + for round := 0; round < maxRounds; round++ { a := aggregateGrid(tmpl, et, scenarios, hpScale, atkScale, n, seedBase) - if a.medOfMedDur >= lowSec && a.medOfMedDur <= highSec { + durOk := a.medOfMedDur > 0 && a.medOfMedDur >= lowSec && a.medOfMedDur <= highSec + hpOk := a.medOfMedHp >= hpLow && a.medOfMedHp <= hpHigh + if durOk && hpOk && a.medWinRate >= minWinRate { break } - if a.medOfMedDur > 0 { + if a.medOfMedDur > 0 && !durOk { corr := targetSec / a.medOfMedDur hpScale *= corr - if hpScale < 0.03 || hpScale > 6 { + if hpScale < 0.02 || hpScale > 28 { break } - if atk2 := findAtkScaleForAggHeroHpGrid(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinRate); atk2 > 0 { - atkScale = atk2 - } } + if atk2 := findAtkScaleForAggHeroHpGridRelaxed(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinRate); atk2 > 0 { + atkScale = atk2 + } else { + break + } + ensurePositiveMinWinGrid(tmpl, et, scenarios, &hpScale, &atkScale, n, seedBase) } ensurePositiveMinWinGrid(tmpl, et, scenarios, &hpScale, &atkScale, n, seedBase) - if hp2 := findHPScaleForAggDurationGrid(tmpl, et, scenarios, n, seedBase, lowSec, highSec, minWinRate); hp2 > 0 { - hpScale = hp2 - } - if atk2 := findAtkScaleForAggHeroHpGrid(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinRate); atk2 > 0 { + if atk2 := findAtkScaleForAggHeroHpGridRelaxed(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinRate); atk2 > 0 { atkScale = atk2 } ensurePositiveMinWinGrid(tmpl, et, scenarios, &hpScale, &atkScale, n, seedBase) + for fix := 0; fix < 10; fix++ { + a := aggregateGrid(tmpl, et, scenarios, hpScale, atkScale, n, seedBase) + if a.medOfMedDur <= 0 { + break + } + if a.medOfMedDur >= lowSec && a.medOfMedDur <= highSec { + break + } + hpScale *= targetSec / a.medOfMedDur + if hpScale < 0.02 || hpScale > 28 { + break + } + if atk2 := findAtkScaleForAggHeroHpGridRelaxed(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinRate); atk2 > 0 { + atkScale = atk2 + } + ensurePositiveMinWinGrid(tmpl, et, scenarios, &hpScale, &atkScale, n, seedBase) + } + final = aggregateGrid(tmpl, et, scenarios, hpScale, atkScale, n, seedBase) + if final.medWinRate < minWinRate { + return 0, 0, gridAggResult{}, false + } + if atkScale < 0.22 { + return 0, 0, gridAggResult{}, false + } + if final.medOfMedDur > 0 && (final.medOfMedDur < lowSec || final.medOfMedDur > highSec) { + return 0, 0, gridAggResult{}, false + } return hpScale, atkScale, final, true } diff --git a/backend/cmd/balanceall/main.go b/backend/cmd/balanceall/main.go index 1dc7199..5a8673b 100644 --- a/backend/cmd/balanceall/main.go +++ b/backend/cmd/balanceall/main.go @@ -33,25 +33,25 @@ func main() { configJSON = flag.String("config", "", "optional JSON file: partial enemy objects keyed by type string, merged over DB templates") - gridMode = flag.Bool("grid", true, "equal-level × gear grid (median + rolled); false = legacy single midpoint + median gear only") - gearVariants = flag.Int("gear-variants", 4, "grid mode: median + (N-1) rolled ilvl profiles per level") - heroHpMid = flag.Float64("hero-hp-mid", 60, "grid: low-tier center hero HP%% on wins when -tiered-targets; else flat center") - heroHpMidHigh = flag.Float64("hero-hp-mid-high", 20, "grid: high-tier center hero HP%% on wins when -tiered-targets") - heroHpPP = flag.Float64("hero-hp-pp", 7, "grid mode: ±percentage points around hero-hp-mid (center varies when tiered)") - refinePasses = flag.Int("refine", 2, "grid mode: duration/atk refinement passes") - - iterations = flag.Int("iterations", 120, "runs per archetype (grid: per scenario cell)") - seed = flag.Int64("seed", 20260331, "base rng seed") - targetSec = flag.Float64("target-sec", 330, "grid: low-tier center median duration (sec) when -tiered-targets; else flat center") + gridMode = flag.Bool("grid", true, "equal-level × gear grid (median + rolled); false = legacy single midpoint + median gear only") + gearVariants = flag.Int("gear-variants", 4, "grid mode: median + (N-1) rolled ilvl profiles per level") + heroHpMid = flag.Float64("hero-hp-mid", 66, "grid: low-tier center hero HP%% on wins when -tiered-targets; else flat center") + heroHpMidHigh = flag.Float64("hero-hp-mid-high", 26, "grid: high-tier center hero HP%% when -tiered-targets") + heroHpPP = flag.Float64("hero-hp-pp", 6, "grid mode: ±percentage points around hero-hp-mid (center varies when tiered)") + refinePasses = flag.Int("refine", 2, "grid mode: duration/atk refinement passes") + + iterations = flag.Int("iterations", 120, "runs per archetype (grid: per scenario cell)") + seed = flag.Int64("seed", 20260331, "base rng seed") + targetSec = flag.Float64("target-sec", 330, "grid: low-tier center median duration (sec) when -tiered-targets; else flat center") targetSecHigh = flag.Float64("target-sec-high", 660, "grid: high-tier center median duration (sec) when -tiered-targets") tieredTargets = flag.Bool("tiered-targets", true, "grid: interpolate target duration and hero HP%% center from low tier to high tier by archetype level band") tierLevelMin = flag.Int("tier-level-min", 1, "grid tiered: global min level for normalization (catalog)") tierLevelMax = flag.Int("tier-level-max", 35, "grid tiered: global max level for normalization (catalog)") - tierGamma = flag.Float64("tier-gamma", 1.5, "grid tiered: exponent for nonlinear interpolation (1=linear)") - 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.35, "stop raising enemy attack if win rate falls below this (legacy); grid: median win rate floor") - printSQL = flag.Bool("sql", true, "print suggested UPDATE statements") + tierGamma = flag.Float64("tier-gamma", 1.5, "grid tiered: exponent for nonlinear interpolation (1=linear)") + 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") + printSQL = flag.Bool("sql", true, "print suggested UPDATE statements") ) flag.Parse() @@ -125,17 +125,20 @@ func main() { if *gearVariants < 2 { log.Fatal("gear-variants must be at least 2") } - var hpLowGrid, hpHighGrid float64 - if *gridMode { + if *gridMode && *tieredTargets && *tierLevelMax <= *tierLevelMin { + log.Fatal("tier-level-max must be > tier-level-min") + } + var hpLowGridFlat, hpHighGridFlat float64 + if *gridMode && !*tieredTargets { hpMid := *heroHpMid / 100.0 pp := *heroHpPP / 100.0 - hpLowGrid = hpMid - pp - hpHighGrid = hpMid + pp - if hpLowGrid < 0 { - hpLowGrid = 0 + hpLowGridFlat = hpMid - pp + hpHighGridFlat = hpMid + pp + if hpLowGridFlat < 0 { + hpLowGridFlat = 0 } - if hpHighGrid > 1 { - hpHighGrid = 1 + if hpHighGridFlat > 1 { + hpHighGridFlat = 1 } } maxHpFrac := *maxHeroHpPct / 100.0 @@ -152,8 +155,8 @@ func main() { tol := *tolerancePct / 100.0 lowTarget := time.Duration(float64(target) * (1.0 - tol)) highTarget := time.Duration(float64(target) * (1.0 + tol)) - lowSec := *targetSec * (1.0 - tol) - highSec := *targetSec * (1.0 + tol) + legacyLowSec := *targetSec * (1.0 - tol) + legacyHighSec := *targetSec * (1.0 + tol) overlayNote := "" if strings.TrimSpace(*configJSON) != "" { @@ -161,8 +164,13 @@ func main() { } if *gridMode { fmt.Printf("# balanceall: grid mode | iterations/cell=%d%s\n", *iterations, overlayNote) - fmt.Printf("# duration: med-of-meds in [%.1fs, %.1fs] | hero HP%% on wins: [%.1f%%, %.1f%%] (center %.1f%% ±%.1f pp)\n", - lowSec, highSec, 100*hpLowGrid, 100*hpHighGrid, *heroHpMid, *heroHpPP) + if *tieredTargets { + fmt.Printf("# tiered: duration center %.0fs → %.0fs | hero HP center %.0f%% → %.0f%% (level mid in [%d,%d], gamma=%.2f); ±%.1f%% dur / ±%.1f pp HP\n", + *targetSec, *targetSecHigh, *heroHpMid, *heroHpMidHigh, *tierLevelMin, *tierLevelMax, *tierGamma, *tolerancePct, *heroHpPP) + } else { + fmt.Printf("# duration: med-of-meds in [%.1fs, %.1fs] | hero HP%% on wins: [%.1f%%, %.1f%%] (center %.1f%% ±%.1f pp)\n", + legacyLowSec, legacyHighSec, 100*hpLowGridFlat, 100*hpHighGridFlat, *heroHpMid, *heroHpPP) + } fmt.Printf("# levels: heroLv==enemyLv for each L in [min_level..max_level]; gear: %d variants\n\n", *gearVariants) } else { fmt.Printf("# balanceall: legacy mode | iterations=%d%s | phase1: HP→duration | phase2: atk→median hero HP on win <= %.1f%%\n# duration band=[%s,%s] min win rate %.0f%%\n\n", @@ -179,9 +187,57 @@ func main() { fmt.Printf("# baseline %s (grid %d cells): medOfMedDur=%.1fs medOfMedHp=%.1f%% medWin=%.1f%% minWin=%.1f%%\n", et, len(scenarios), base.medOfMedDur, 100*base.medOfMedHp, 100*base.medWinRate, 100*base.minWinRate) + var lowSec, highSec, targetSecEff, hpLowGrid, hpHighGrid float64 + if *tieredTargets { + norm := archetypeTierNorm(tmpl, *tierLevelMin, *tierLevelMax) + tPow := math.Pow(norm, *tierGamma) + targetSecEff = *targetSec + (*targetSecHigh-*targetSec)*tPow + heroHpMidEff := *heroHpMid + (*heroHpMidHigh-*heroHpMid)*tPow + lowSec = targetSecEff * (1.0 - tol) + highSec = targetSecEff * (1.0 + tol) + hpMid := heroHpMidEff / 100.0 + ppPts := *heroHpPP + if norm > 0.35 { + // Elites with DoT/stun: median hero HP on wins is harder to fit in a tight band. + ppPts += (norm - 0.35) * 45 + } + pp := ppPts / 100.0 + // Minimum remaining HP floor: 60% for early archetypes (norm<0.25), then linearly toward 20% at norm=1. + var floorHp float64 + if norm < 0.25 { + floorHp = 0.60 + } else { + u := (norm - 0.25) / 0.75 + if u > 1 { + u = 1 + } + // Steeper than linear 60%→20% so DoT-heavy mid-tier elites can still hit the HP band. + floorHp = math.Max(0.20, 0.60-0.65*u) + } + hpLowGrid = hpMid - pp + if hpLowGrid < floorHp { + hpLowGrid = floorHp + } + hpHighGrid = hpMid + pp + if hpLowGrid < 0 { + hpLowGrid = 0 + } + if hpHighGrid > 1 { + hpHighGrid = 1 + } + fmt.Printf("# tier: norm=%.3f curve=%.3f | targetSec=%.1fs heroHpMid=%.1f%% | bands dur [%.1f,%.1f] hp [%.1f%%,%.1f%%] (±%.1f pp)\n", + norm, tPow, targetSecEff, heroHpMidEff, lowSec, highSec, 100*hpLowGrid, 100*hpHighGrid, ppPts) + } else { + targetSecEff = *targetSec + lowSec = legacyLowSec + highSec = legacyHighSec + hpLowGrid = hpLowGridFlat + hpHighGrid = hpHighGridFlat + } + hpScale, atkScale, gfinal, ok := balanceArchetypeGrid( tmpl, et, scenarios, *iterations, typeSeed, - lowSec, highSec, *targetSec, hpLowGrid, hpHighGrid, *minWinRate, *refinePasses, + lowSec, highSec, targetSecEff, hpLowGrid, hpHighGrid, *minWinRate, *refinePasses, ) if !ok { fmt.Printf("## %s — SKIP: grid balance failed (try -legacy or adjust template)\n\n", et) @@ -456,6 +512,22 @@ func trimName32(s string) string { return string(runes[:max-1]) + "…" } +// archetypeTierNorm maps the midpoint of [MinLevel, MaxLevel] to [0,1] using global catalog bounds (e.g. 1..35). +func archetypeTierNorm(t model.Enemy, globalMin, globalMax int) float64 { + if globalMax <= globalMin { + return 0 + } + mid := float64(t.MinLevel+t.MaxLevel) / 2 + n := (mid - float64(globalMin)) / float64(globalMax-globalMin) + if n < 0 { + return 0 + } + if n > 1 { + return 1 + } + return n +} + // archetypeOrder returns which archetypes to balance: one row by -enemy-id, one by -enemy-type, or all (DB order). func archetypeOrder(ctx context.Context, templates map[model.EnemyType]model.Enemy, pool *pgxpool.Pool, enemyID int64, enemyType string) ([]model.EnemyType, error) { if enemyID > 0 { diff --git a/backend/internal/game/balance_reference.go b/backend/internal/game/balance_reference.go index 12f660a..3ec1fb0 100644 --- a/backend/internal/game/balance_reference.go +++ b/backend/internal/game/balance_reference.go @@ -57,8 +57,9 @@ func NewReferenceHeroForBalance(level int, profile ReferenceGearProfile, rng *ra aIlvl = rollIlvlForBalance(level, false, rng) } - // Typical mid-tier drops: uncommon sword + mail (catalog bases 10 / spec §6.4). - wPrimary := model.ScalePrimary(10, wIlvl, model.RarityUncommon) + // Typical mid-tier drops: uncommon sword + mail (catalog bases; slightly above 10 for elite/DoT balance targets). + const refGearBase = 12 + wPrimary := model.ScalePrimary(refGearBase, wIlvl, model.RarityUncommon) h.Gear[model.SlotMainHand] = &model.GearItem{ Slot: model.SlotMainHand, FormID: "gear.form.main_hand.sword", @@ -66,13 +67,13 @@ func NewReferenceHeroForBalance(level int, profile ReferenceGearProfile, rng *ra Subtype: "sword", Rarity: model.RarityUncommon, Ilvl: wIlvl, - BasePrimary: 10, + BasePrimary: refGearBase, PrimaryStat: wPrimary, StatType: "attack", SpeedModifier: 1.0, CritChance: 0.05, } - aPrimary := model.ScalePrimary(10, aIlvl, model.RarityUncommon) + aPrimary := model.ScalePrimary(refGearBase, aIlvl, model.RarityUncommon) h.Gear[model.SlotChest] = &model.GearItem{ Slot: model.SlotChest, FormID: "gear.form.chest.medium", @@ -80,7 +81,7 @@ func NewReferenceHeroForBalance(level int, profile ReferenceGearProfile, rng *ra Subtype: "medium", Rarity: model.RarityUncommon, Ilvl: aIlvl, - BasePrimary: 10, + BasePrimary: refGearBase, PrimaryStat: aPrimary, StatType: "defense", SpeedModifier: 1.0, diff --git a/backend/migrations/000003_balanceall_enemies.sql b/backend/migrations/000003_balanceall_enemies.sql new file mode 100644 index 0000000..4b3f140 --- /dev/null +++ b/backend/migrations/000003_balanceall_enemies.sql @@ -0,0 +1,17 @@ +-- Enemy combat stats from balanceall grid (see balanceall-out.txt and fire_demon run). +-- fire_demon: grid balance with in-memory overlay only (specialAbilities cleared for sim); +-- column special_abilities in DB is unchanged — burn remains unless changed elsewhere. + +UPDATE enemies SET hp = 94, max_hp = 94, hp_per_level = 7.8681, attack = 20, attack_per_level = 2.7054 WHERE type = 'wolf'; +UPDATE enemies SET hp = 102, max_hp = 102, hp_per_level = 8.2826, attack = 25, attack_per_level = 2.3190 WHERE type = 'boar'; +UPDATE enemies SET hp = 107, max_hp = 107, hp_per_level = 6.9412, attack = 28, attack_per_level = 2.5898 WHERE type = 'zombie'; +UPDATE enemies SET hp = 118, max_hp = 118, hp_per_level = 12.0614, attack = 24, attack_per_level = 2.7373 WHERE type = 'spider'; +UPDATE enemies SET hp = 113, max_hp = 113, hp_per_level = 7.1338, attack = 27, attack_per_level = 2.6581 WHERE type = 'orc'; +UPDATE enemies SET hp = 132, max_hp = 132, hp_per_level = 8.5586, attack = 28, attack_per_level = 2.2939 WHERE type = 'skeleton_archer'; +UPDATE enemies SET hp = 105, max_hp = 105, hp_per_level = 5.7476, attack = 32, attack_per_level = 2.4140 WHERE type = 'battle_lizard'; +UPDATE enemies SET hp = 193, max_hp = 193, hp_per_level = 12.7956, attack = 28, attack_per_level = 2.9413 WHERE type = 'fire_demon'; +UPDATE enemies SET hp = 208, max_hp = 208, hp_per_level = 7.5649, attack = 37, attack_per_level = 3.0394 WHERE type = 'ice_guardian'; +UPDATE enemies SET hp = 149, max_hp = 149, hp_per_level = 4.1663, attack = 24, attack_per_level = 1.8339 WHERE type = 'skeleton_king'; +UPDATE enemies SET hp = 349, max_hp = 349, hp_per_level = 8.0285, attack = 45, attack_per_level = 3.1288 WHERE type = 'water_element'; +UPDATE enemies SET hp = 338, max_hp = 338, hp_per_level = 6.1288, attack = 50, attack_per_level = 3.5033 WHERE type = 'forest_warden'; +UPDATE enemies SET hp = 583, max_hp = 583, hp_per_level = 11.1055, attack = 48, attack_per_level = 2.9104 WHERE type = 'lightning_titan'; diff --git a/balanceall-out.txt b/balanceall-out.txt new file mode 100644 index 0000000..d3d7a37 Binary files /dev/null and b/balanceall-out.txt differ