Class identity audit close-out: Fighter/Ranger/Paladin/Druid + Bard-Valor

Phase 2 Monte Carlo tuning had papered over four classes whose 5e-defining
mechanics never actually existed in the engine — only flat DamageBonus or
FlatDmgStart compensation riders that hit the right aggregate win rate.
This pass replaces the proxies with the real primitives.

New CombatModifiers fields: ExtraAttacks (additional swings/round, looped
in a new resolvePlayerSwings helper); HuntersMarkDie (per-hit Nd6 bonus,
same path as Sneak Attack); ThornLashDmg (flat counter on landed enemy
hits, fires in resolveEnemyAttack after ward/block).

Fighter L5/11/20 Extra Attack, Ranger L5 Extra Attack + Hunter's Mark
(1→4 d6 by level), Paladin L5 Extra Attack + per-hit Divine Smite
(replaces one-shot opener), Druid thorn lash. Bard College of Valor L7
"Extra Attack" subclass tier converted from +1 attack/+10% damage proxy
to the real primitive.

Post-fix T5 class-balance: martials cluster at top (Fighter/Rogue 0.90,
Ranger 0.89, Paladin 0.88), casters keep their relative ordering, spread
14pp top-to-bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-16 12:53:00 -07:00
parent 41adfce9f1
commit 98ba416359
5 changed files with 144 additions and 14 deletions

View File

@@ -236,12 +236,18 @@ func TestApplyClassPassives(t *testing.T) {
{ClassRogue, 0.05, 0, true, 0, 1.0, 0, 0},
{ClassMage, 0.05, 1, false, 0, 1.0, 1, 0},
{ClassCleric, 0, 0, false, 5, 1.0, 0, 0},
{ClassRanger, 0.05, 1, false, 0, 1.0, 0, 0},
// Class-identity audit (2026-05-16): Ranger Hunter's Mark is now
// per-hit HuntersMarkDie (not asserted here — same shape as
// SneakAttackDie); Paladin Divine Smite is now per-hit
// DivineStrikePerHit (not asserted here). The +5% damage and
// FlatDmgStart compensation riders are gone; +1 to-hit stays on
// Ranger as the "read prey tells" half.
{ClassRanger, 0, 1, false, 0, 1.0, 0, 0},
{ClassDruid, 0, 0, false, 0, 0.95, 1, 0},
{ClassBard, 0.05, 1, false, 0, 1.0, 1, 1},
{ClassSorcerer, 0.05, 0, false, 0, 1.0, 6, 0},
{ClassWarlock, 0.12, 1, false, 0, 1.0, 1, 0},
{ClassPaladin, 0, 0, false, 0, 1.0, 4, 0},
{ClassPaladin, 0, 0, false, 0, 1.0, 0, 0},
}
for _, tc := range cases {
stats := CombatStats{AttackBonus: 5}