mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
D&D: class-balance Phase 3 — off-tier caster lift (druid + sorcerer)
Design steer from user — "relatively easy but not too easy" — narrowed the target: lift the embarrassing caster trailers on off-tier cells (a casual player walking into a slightly too-hard dungeon underleveled) without pushing the already-saturated in-tier ceiling. Levers: - Druid passive: was the only chassis with a purely defensive passive (5% DR, no offense), and it read it — L1/T1 mean 0.77 (lowest at the entry tier), L1/T2 0.04. Added a level + WIS-scaled FlatDmgStart burst, same shape as the Phase-2 Bard/Mage/Warlock pass. Kept the DR; no DamageBonus rider so high-tier ceilings stay flat. - Sorcerer passive: burst base 3→5. Sorcerer was second-worst caster off-tier (L1/T2 0.10 vs Mage 0.27 pre-tune) despite a comparable stat line; the bump pulls it toward arcane-chassis parity. Observed lifts: - Druid L1/T1: 0.77 → 0.86 (+9pp) — chassis now functional at its intended tier - L2/T2 cross-class spread: 77pp → 63pp; druid trailer 0.23 → 0.35 - L1/T1 spread: 23pp → 14pp Off-tier diagnostic: added a focused log to TestClassBalance_Phase1_FullMatrix that names the trailing class at each off-tier (lvl, tier) cell. Not asserted — L1 in T2 is *supposed* to be hard, so the diagnostic is for watching the gap, not the absolute number. In-tier parity assertion (35pp band on the diagonal) still passes; TestApplyClassPassives updated for the new druid/sorcerer FlatDmgStart values; full plugin -short suite clean.
This commit is contained in:
@@ -277,6 +277,67 @@ func TestClassBalance_Phase1_FullMatrix(t *testing.T) {
|
||||
t.Logf("L%-4d %s", lvl, line)
|
||||
}
|
||||
|
||||
// Phase-3 diagnostic — off-tier trailer per (level, tier). For each cell
|
||||
// where the level is *below* the in-tier band (e.g. L1 at T2-T5), print the
|
||||
// trailing class and its win rate. This is what Phase 3 lifts: the casual
|
||||
// player who walks into a slightly-too-hard dungeon underleveled. Not
|
||||
// asserted — Phase 3 tunes against the diagnostic numbers and the next
|
||||
// phase decides whether to lock a soft off-tier band.
|
||||
offTierLevels := map[int][]int{
|
||||
2: {1, 2},
|
||||
3: {1, 2, 3, 4},
|
||||
4: {1, 2, 3, 4, 5},
|
||||
5: {1, 2, 3, 4, 5, 7},
|
||||
}
|
||||
t.Logf("")
|
||||
t.Logf("off-tier trailer per (level, tier) — class winrate is mean over subclasses (or single cell pre-L5):")
|
||||
t.Logf("%-5s T2 T3 T4 T5", "lvl")
|
||||
for _, lvl := range allLevels {
|
||||
var line string
|
||||
for tier := 2; tier <= 5; tier++ {
|
||||
levels := offTierLevels[tier]
|
||||
matched := false
|
||||
for _, l := range levels {
|
||||
if l == lvl {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
line += " " + " — "
|
||||
continue
|
||||
}
|
||||
var trailerClass DnDClass
|
||||
minV := 1.01
|
||||
for _, ci := range dndClasses {
|
||||
var sum float64
|
||||
var n int
|
||||
if lvl < 5 {
|
||||
if r, ok := rows[rowKey{ci.Key, "", lvl}]; ok {
|
||||
sum = r[tier].WinRate()
|
||||
n = 1
|
||||
}
|
||||
} else {
|
||||
for _, si := range subclassesForClass(ci.Key) {
|
||||
if r, ok := rows[rowKey{ci.Key, si.ID, lvl}]; ok {
|
||||
sum += r[tier].WinRate()
|
||||
n++
|
||||
}
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
wr := sum / float64(n)
|
||||
if wr < minV {
|
||||
minV, trailerClass = wr, ci.Key
|
||||
}
|
||||
}
|
||||
line += fmt.Sprintf(" %-10s %.2f ", trailerClass, minV)
|
||||
}
|
||||
t.Logf("L%-4d %s", lvl, line)
|
||||
}
|
||||
|
||||
// Harness-broken gates first.
|
||||
for _, r := range results {
|
||||
if r.Tier == 1 && r.WinRate() == 0 {
|
||||
|
||||
Reference in New Issue
Block a user