D&D: class-balance Phase 2 — passive + L5 subclass tuning + in-tier parity assertion

The Phase 1 per-class-mean summary was hiding the truth — most cells are
floor/ceiling-saturated (L10+ pinned at 1.0, L1-4 caster cells at high
tier pinned at 0.0), so means barely budge when you tune passives. Added
a per-(level, tier) cross-class spread diagnostic to the matrix log,
then tuned with the levers from doc §6 in priority order:

1. Class passives (dnd_passives.go) — caster trailers (Bard, Mage,
   Warlock, Sorcerer) gained level + casting-stat-scaled FlatDmgStart
   bursts so the L1-4 chassis isn't a quarterstaff + one weak spell
   against a T2-T3 monster; small DamageBonus riders (Mage/Bard/
   Sorcerer/Rogue +5%, Warlock 10→12%) and +1 attack for Bard/Warlock
   close the steady-DPS gap. Added clampNonNeg so ability-mod-scaled
   additions never go negative on sub-10-stat sheets.

2. Subclass L5 tiers (dnd_subclass_combat.go) — the three Sorcerer L5
   picks (Wild/Storm/Draconic) and Warlock Great Old One were defense-
   only or near-inert pre-tune; each gained a small bite (DamageBonus
   +0.10, or a FlatDmgStart burst for Storm) so the L5 chassis can press
   through a T4 monster.

Parity band locked in TestClassBalance_Phase1_FullMatrix: cross-class
spread ≤ 35pp on the in-tier diagonal — (level, tier) cells where the
level is appropriate for the tier (T1: L1-4, T2: L3-7, T3: L5-10, T4:
L7-15, T5: L10-20). Off-tier cells (L1 mage at T3 dungeon etc.) are
still logged but not asserted: those are level-vs-tier mismatches and
casters at L1-4 can't muscle through a T3 monster on a single L1-slot
spell the way martials muscle through with weapon dice. Worst in-tier
cell after tuning: ~26pp at L3/T2. The 35pp band gives ~9pp Monte-Carlo
headroom over the worst signal at 200 trials/cell.

TestApplyClassPassives expectations updated to match the new passives.
Phase 0 spike still green, full plugin suite (-short) clean.
This commit is contained in:
prosolis
2026-05-14 20:15:16 -07:00
parent ddfa89e7a7
commit 76f814c0c9
5 changed files with 224 additions and 20 deletions

View File

@@ -25,11 +25,11 @@ var dndClassAbilities = map[DnDClass]DnDClassAbility{
},
ClassRogue: {
Name: "Sneak Attack",
Description: "Your first strike each combat lands as a critical hit, doubling its damage.",
Description: "Your first strike each combat lands as a critical hit, doubling its damage; precision drills add +5% to all damage you deal.",
},
ClassMage: {
Name: "Arcane Focus",
Description: "Practiced channeling adds +1 to your attack rolls.",
Description: "Practiced channeling adds +1 to your attack rolls and a small bump (+5%) to all damage you deal.",
},
ClassCleric: {
Name: "Divine Favor",
@@ -47,15 +47,15 @@ var dndClassAbilities = map[DnDClass]DnDClassAbility{
},
ClassBard: {
Name: "Bardic Inspiration",
Description: "Quick wit keeps you a step ahead: +1 to your initiative each round.",
Description: "Quick wit keeps you a step ahead: +1 initiative, +1 attack, and +5% to all damage you deal.",
},
ClassSorcerer: {
Name: "Innate Sorcery",
Description: "Raw magic spills out as the fight begins, dealing immediate damage scaled by your Charisma.",
Description: "Raw magic spills out as the fight begins, dealing immediate damage scaled by your Charisma, and +5% to all damage you deal.",
},
ClassWarlock: {
Name: "Agonizing Blast",
Description: "Your pact-fueled eldritch power adds +10% to all damage you deal.",
Description: "Your pact-fueled eldritch power adds +12% to all damage you deal and +1 to attack rolls.",
},
ClassPaladin: {
Name: "Divine Smite",
@@ -63,6 +63,17 @@ var dndClassAbilities = map[DnDClass]DnDClassAbility{
},
}
// clampNonNeg returns max(0, x). Used by Phase-2 class passives to keep
// ability-mod-scaled FlatDmgStart additions from going negative when a
// caster's casting stat happens to be below 10 (or when tests construct
// a DnDCharacter with zero-value stats).
func clampNonNeg(x int) int {
if x < 0 {
return 0
}
return x
}
// applyRacePassives sets the combat-impacting flags from the player's race.
// Races whose passives apply to skill checks or non-combat scenarios
// (Tiefling fire resist, Elf sleep immunity, Half-Elf bonus profs, Human
@@ -99,8 +110,22 @@ func applyClassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharact
mods.DamageBonus += 0.05
case ClassRogue:
mods.AutoCritFirst = true
// Phase 2 class-balance: rogue's once-per-fight auto-crit goes stale
// at high tiers (T5 mean trails leaders by ~10pp pre-tune). Add a
// modest steady-DPS rider so post-opener rounds aren't pure attrition.
mods.DamageBonus += 0.05
case ClassMage:
stats.AttackBonus++
// Phase 2 class-balance: +1 attack alone left Mage mid-pack on damage
// per round. A modest damage rider lifts weapon hits (DamageBonus does
// not multiply queued SpellPreDamage — that path is its own field).
mods.DamageBonus += 0.05
// Phase 2 class-balance: Arcane focus also produces a small pre-combat
// arcane burst, scaling with level and INT. Helps the L1-4 chassis,
// which would otherwise rely on a single weak L1 spell + quarterstaff
// against T2-T3 monsters. Saturates harmlessly at L10+ where every
// class wins anyway.
mods.FlatDmgStart += c.Level + clampNonNeg(abilityModifier(c.INT))
case ClassCleric:
// Passive heal at <50% HP. Stacks additively with consumable HealItem.
mods.HealItem += 5
@@ -113,13 +138,32 @@ func applyClassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharact
// by DerivePlayerStats before passives run.
mods.DamageReduct *= 0.95
case ClassBard:
// Phase 2 class-balance: bare +1 initiative left Bard the weakest
// class chassis (T5 mean 0.48 pre-tune). Add a Ranger-tier rider
// (+1 attack, +5% damage) so the chassis pulls weight before subclass
// kicks in at L5, plus a CHA-scaled opening flourish (FlatDmgStart) so
// the L1-4 chassis isn't dead at T3.
mods.InitiativeBias += 1
stats.AttackBonus++
mods.DamageBonus += 0.05
mods.FlatDmgStart += c.Level + clampNonNeg(abilityModifier(c.CHA))
case ClassSorcerer:
// Innate Sorcery — pre-combat burst, CHA-scaled like the Sorcerer's
// spellcasting stat. Floors at the flat 3 for low-CHA builds.
mods.FlatDmgStart += 3 + abilityModifier(c.CHA)
// Phase 2 class-balance: pure FlatDmgStart faded at higher tiers as
// monster HP grew. Adding a 5% damage rider plus level scaling on the
// burst keeps the chassis relevant past L1.
mods.FlatDmgStart += 3 + c.Level + clampNonNeg(abilityModifier(c.CHA))
mods.DamageBonus += 0.05
case ClassWarlock:
mods.DamageBonus += 0.10
// Phase 2 class-balance: bumped from 10% to 12% damage + 1 attack —
// the Warlock chassis read mid-pack at T5 (0.52) pre-tune. Eldritch
// blast as an opener (FlatDmgStart, level + CHA-scaled) covers the
// caster's L1-4 quarterstaff weakness, same shape as the other three
// arcane chassis.
mods.DamageBonus += 0.12
stats.AttackBonus++
mods.FlatDmgStart += c.Level + clampNonNeg(abilityModifier(c.CHA))
case ClassPaladin:
// Divine Smite — radiant burst on engage, scaling with level so it
// stays relevant against tougher foes.