D&D: class passives + subclasses for the five caster classes

Adds signature class passives for Druid/Bard/Sorcerer/Warlock/Paladin in
dnd_passives.go, each riding an existing CombatModifiers channel so a
playable caster is no longer mechanically empty: Druid Wild Resilience
(damage reduction), Bard Bardic Inspiration (initiative), Sorcerer Innate
Sorcery (CHA-scaled pre-combat burst), Warlock Agonizing Blast (+10%
damage), Paladin Divine Smite (level-scaled burst).

Adds 15 subclasses (3 per caster, registry now 30) using canonical 5e
archetype names, with passive-only L5/7/10/15 effects in
applySubclassPassives — consistent with the majority of the original
fifteen, no new resource pools or active abilities. subclassesForClass is
now non-empty for casters, so the L5 !subclass prompt and sheet display
light up through the existing generic plumbing.

Tests: TestApplyClassPassives extended to all ten classes and the new
channels; TestSubclassRegistry_* updated to expect 30 entries across all
ten classes.
This commit is contained in:
prosolis
2026-05-14 15:18:29 -07:00
parent 6ef8b9fd0a
commit d6ea08bba6
5 changed files with 480 additions and 14 deletions

View File

@@ -219,17 +219,27 @@ func TestApplyClassPassives(t *testing.T) {
wantAtkBonusAdd int
wantAutoCrit bool
wantHealItem int
wantDmgReduct float64
wantFlatStart int
wantInitBias float64
}{
{ClassFighter, 0.05, 0, false, 0},
{ClassRogue, 0, 0, true, 0},
{ClassMage, 0, 1, false, 0},
{ClassCleric, 0, 0, false, 5},
{ClassRanger, 0.05, 1, false, 0},
{ClassFighter, 0.05, 0, false, 0, 1.0, 0, 0},
{ClassRogue, 0, 0, true, 0, 1.0, 0, 0},
{ClassMage, 0, 1, false, 0, 1.0, 0, 0},
{ClassCleric, 0, 0, false, 5, 1.0, 0, 0},
{ClassRanger, 0.05, 1, false, 0, 1.0, 0, 0},
// Open5e caster scaffold. CHA 10 → +0 mod, so Sorcerer's FlatDmgStart
// is the flat 3; Paladin at L1 is 4 + 0.
{ClassDruid, 0, 0, false, 0, 0.95, 0, 0},
{ClassBard, 0, 0, false, 0, 1.0, 0, 1},
{ClassSorcerer, 0, 0, false, 0, 1.0, 3, 0},
{ClassWarlock, 0.10, 0, false, 0, 1.0, 0, 0},
{ClassPaladin, 0, 0, false, 0, 1.0, 4, 0},
}
for _, tc := range cases {
stats := CombatStats{AttackBonus: 5}
mods := CombatModifiers{}
applyClassPassives(&stats, &mods, &DnDCharacter{Class: tc.class})
mods := CombatModifiers{DamageReduct: 1.0}
applyClassPassives(&stats, &mods, &DnDCharacter{Class: tc.class, Level: 1, CHA: 10})
if mods.DamageBonus != tc.wantDmgBonus {
t.Errorf("%s: DamageBonus=%v, want %v", tc.class, mods.DamageBonus, tc.wantDmgBonus)
}
@@ -242,5 +252,14 @@ func TestApplyClassPassives(t *testing.T) {
if mods.HealItem != tc.wantHealItem {
t.Errorf("%s: HealItem=%d, want %d", tc.class, mods.HealItem, tc.wantHealItem)
}
if mods.DamageReduct != tc.wantDmgReduct {
t.Errorf("%s: DamageReduct=%v, want %v", tc.class, mods.DamageReduct, tc.wantDmgReduct)
}
if mods.FlatDmgStart != tc.wantFlatStart {
t.Errorf("%s: FlatDmgStart=%d, want %d", tc.class, mods.FlatDmgStart, tc.wantFlatStart)
}
if mods.InitiativeBias != tc.wantInitBias {
t.Errorf("%s: InitiativeBias=%v, want %v", tc.class, mods.InitiativeBias, tc.wantInitBias)
}
}
}