|
|
|
|
@ -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 := 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
|
|
|
|
|
}
|
|
|
|
|
if atk2 := findAtkScaleForAggHeroHpGrid(tmpl, et, scenarios, hpScale, n, seedBase, hpLow, hpHigh, minWinRate); atk2 > 0 {
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|