From d6ea08bba66db961f859935a5b9e8ef9e43774c1 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Thu, 14 May 2026 15:18:29 -0700 Subject: [PATCH] D&D: class passives + subclasses for the five caster classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/plugin/dnd_passives.go | 39 +++ internal/plugin/dnd_subclass.go | 86 ++++++- internal/plugin/dnd_subclass_combat.go | 325 +++++++++++++++++++++++++ internal/plugin/dnd_subclass_test.go | 11 +- internal/plugin/dnd_xp_test.go | 33 ++- 5 files changed, 480 insertions(+), 14 deletions(-) diff --git a/internal/plugin/dnd_passives.go b/internal/plugin/dnd_passives.go index 4079c08..091df9c 100644 --- a/internal/plugin/dnd_passives.go +++ b/internal/plugin/dnd_passives.go @@ -39,6 +39,28 @@ var dndClassAbilities = map[DnDClass]DnDClassAbility{ Name: "Hunter's Mark", Description: "You read your prey's weak points: +5% damage and +1 to attack rolls.", }, + // Open5e caster scaffold — one signature passive each, riding existing + // CombatModifiers channels the same way the original five do. + ClassDruid: { + Name: "Wild Resilience", + Description: "The wild lends you its toughness: incoming damage is reduced 5%.", + }, + ClassBard: { + Name: "Bardic Inspiration", + Description: "Quick wit keeps you a step ahead: +1 to your initiative each round.", + }, + ClassSorcerer: { + Name: "Innate Sorcery", + Description: "Raw magic spills out as the fight begins, dealing immediate damage scaled by your Charisma.", + }, + ClassWarlock: { + Name: "Agonizing Blast", + Description: "Your pact-fueled eldritch power adds +10% to all damage you deal.", + }, + ClassPaladin: { + Name: "Divine Smite", + Description: "You channel a burst of radiant power on engagement, dealing flat damage that grows with your level.", + }, } // applyRacePassives sets the combat-impacting flags from the player's race. @@ -85,5 +107,22 @@ func applyClassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharact case ClassRanger: mods.DamageBonus += 0.05 stats.AttackBonus++ + case ClassDruid: + // Wild Resilience — multiplicative, so it stacks cleanly with the + // subclass DamageReduct riders. DamageReduct is initialized to 1.0 + // by DerivePlayerStats before passives run. + mods.DamageReduct *= 0.95 + case ClassBard: + mods.InitiativeBias += 1 + 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) + case ClassWarlock: + mods.DamageBonus += 0.10 + case ClassPaladin: + // Divine Smite — radiant burst on engage, scaling with level so it + // stays relevant against tougher foes. + mods.FlatDmgStart += 4 + c.Level/2 } } diff --git a/internal/plugin/dnd_subclass.go b/internal/plugin/dnd_subclass.go index 4822f3c..a4dee56 100644 --- a/internal/plugin/dnd_subclass.go +++ b/internal/plugin/dnd_subclass.go @@ -39,6 +39,26 @@ const ( SubclassHunter DnDSubclass = "hunter" SubclassBeastMaster DnDSubclass = "beast_master" SubclassGloomStalker DnDSubclass = "gloom_stalker" + // Druid — Open5e caster scaffold + SubclassCircleLand DnDSubclass = "circle_land" + SubclassCircleMoon DnDSubclass = "circle_moon" + SubclassCircleShepherd DnDSubclass = "circle_shepherd" + // Bard + SubclassCollegeLore DnDSubclass = "college_lore" + SubclassCollegeValor DnDSubclass = "college_valor" + SubclassCollegeGlamour DnDSubclass = "college_glamour" + // Sorcerer + SubclassDraconicBloodline DnDSubclass = "draconic_bloodline" + SubclassWildMagic DnDSubclass = "wild_magic" + SubclassStormSorcery DnDSubclass = "storm_sorcery" + // Warlock + SubclassFiendPatron DnDSubclass = "fiend_patron" + SubclassArchfeyPatron DnDSubclass = "archfey_patron" + SubclassGreatOldOne DnDSubclass = "great_old_one" + // Paladin + SubclassOathDevotion DnDSubclass = "oath_devotion" + SubclassOathVengeance DnDSubclass = "oath_vengeance" + SubclassOathAncients DnDSubclass = "oath_ancients" ) // DnDSubclassInfo — registry row. Tagline summarizes identity in one line @@ -52,9 +72,14 @@ type DnDSubclassInfo struct { Flavor string } -// dndSubclassRegistry — 15 entries, ordered first by class (Fighter, -// Rogue, Mage, Cleric, Ranger) then by the design doc's listed order -// within each class. The selection prompt presents them in this order. +// dndSubclassRegistry — 30 entries (3 per class × 10 classes), ordered +// first by class (Fighter, Rogue, Mage, Cleric, Ranger, then the Open5e +// caster scaffold: Druid, Bard, Sorcerer, Warlock, Paladin) then by the +// design doc's listed order within each class. The selection prompt +// presents them in this order. The caster subclasses use canonical 5e +// archetype names, consistent with the original fifteen — see the +// avoid-dnd-naming note: the naming concern is code identifiers and +// marketing, not in-game content. var dndSubclassRegistry = []DnDSubclassInfo{ // Fighter {SubclassChampion, ClassFighter, "Champion", @@ -110,6 +135,61 @@ var dndSubclassRegistry = []DnDSubclassInfo{ {SubclassGloomStalker, ClassRanger, "Gloom Stalker", "Darkness specialist. Exceptional underground.", "You were already good in the dark. Now you're better. TwinBee notes: the Underdark is waiting."}, + + // Druid — Open5e caster scaffold + {SubclassCircleLand, ClassDruid, "Circle of the Land", + "Versatile caster; recovery and warding.", + "The land remembers you, and answers when you call. A measured path — and a durable one."}, + {SubclassCircleMoon, ClassDruid, "Circle of the Moon", + "Combat shapeshifter; tanky beast forms.", + "You don't cast at the fight so much as become it. Tooth, claw, and a great deal of hide."}, + {SubclassCircleShepherd, ClassDruid, "Circle of the Shepherd", + "Summoner; spirit totems and conjured allies.", + "You never walk into a fight alone — the wild walks in with you."}, + + // Bard + {SubclassCollegeLore, ClassBard, "College of Lore", + "Knowledge and control; turn the enemy's edge dull.", + "You've read the room, the foe, and three books about both. The fight is half-won before it starts."}, + {SubclassCollegeValor, ClassBard, "College of Valor", + "Combat bard; songs that sharpen sword and shield.", + "A song with a blade in it. The old kind of bard — the kind that came back from the war."}, + {SubclassCollegeGlamour, ClassBard, "College of Glamour", + "Charm and majesty; enthrall and disarm the foe.", + "You wear the Feywild's glamour like a borrowed coat. Few can bear to strike at something so dazzling."}, + + // Sorcerer + {SubclassDraconicBloodline, ClassSorcerer, "Draconic Bloodline", + "Durable blaster; draconic scales and elemental punch.", + "There's a dragon somewhere back down your line, and it left the door unlocked."}, + {SubclassWildMagic, ClassSorcerer, "Wild Magic", + "Chaotic burst; luck bent hard in your favor.", + "Your magic doesn't take orders so much as suggestions. Loud, unpredictable, occasionally glorious."}, + {SubclassStormSorcery, ClassSorcerer, "Storm Sorcery", + "Mobile elemental caster; wind, speed, and thunder.", + "You move first because the storm always does. Everything after is just weather."}, + + // Warlock + {SubclassFiendPatron, ClassWarlock, "The Fiend", + "Damage and grit; power borrowed from the lower planes.", + "The bargain was steep, the firepower is steeper. Try not to read the fine print mid-fight."}, + {SubclassArchfeyPatron, ClassWarlock, "The Archfey", + "Charm and escape; fey tricks that unmake the attack.", + "Your patron deals in glamour and misdirection — and so, now, do you. Hard to hit what won't hold still."}, + {SubclassGreatOldOne, ClassWarlock, "The Great Old One", + "Psychic control; minds bent, foes unmade from within.", + "Something vast and distant is half-listening through you. Best not to think too hard about which half."}, + + // Paladin + {SubclassOathDevotion, ClassPaladin, "Oath of Devotion", + "Holy warrior; sacred weapon and protective auras.", + "You took the oath and meant every word of it. The light leans your way for it."}, + {SubclassOathVengeance, ClassPaladin, "Oath of Vengeance", + "Relentless attacker; mark a foe and bring it down.", + "Someone, somewhere, has earned this. You've simply volunteered to deliver it."}, + {SubclassOathAncients, ClassPaladin, "Oath of the Ancients", + "Resilient nature-paladin; warding light, hard to fell.", + "You guard the green and growing things, and the old light guards you back. Stubbornly."}, } // subclassInfo returns the registry row for id, or (zero, false). diff --git a/internal/plugin/dnd_subclass_combat.go b/internal/plugin/dnd_subclass_combat.go index 7876465..f99e3c3 100644 --- a/internal/plugin/dnd_subclass_combat.go +++ b/internal/plugin/dnd_subclass_combat.go @@ -340,6 +340,331 @@ func applySubclassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDChar if c.Level >= 15 { mods.DamageReduct *= 0.90 } + + // ── Open5e caster scaffold — Druid / Bard / Sorcerer / Warlock / + // Paladin subclasses. Passive-only, consistent with the majority of the + // original fifteen (Champion, Thief, the Mage and Ranger subclasses): + // no new resource pools or active abilities. Each L5/7/10/15 tier rides + // an existing CombatModifiers channel — per-case notes name the 5e + // ability each proxy stands in for. DamageReduct is initialized to 1.0 + // by DerivePlayerStats, so the `*=` reductions compose cleanly. + case SubclassCircleLand: + // L5 Natural Recovery: a measured self-heal — rides HealItem (fires + // once at <50% HP). L7 Land's Stride: +2 Speed (initiative). L10 + // Nature's Ward: resistance to a slice of elemental/poison damage, + // collapsed to a flat 8% incoming reduction. L15 Nature's Sanctuary: + // foes hesitate to close — 1 round of SporeCloud — plus a further 10%. + if c.Level >= 5 { + mods.HealItem += 5 + } + if c.Level >= 7 { + stats.Speed += 2 + } + if c.Level >= 10 { + mods.DamageReduct *= 0.92 + } + if c.Level >= 15 { + mods.SporeCloud++ + mods.DamageReduct *= 0.90 + } + case SubclassCircleMoon: + // L5 Combat Wild Shape: a bestial form's HP pool — an ArcaneWardHP + // buffer scaling with level — plus the form's heavier hits (+10% + // damage). L7 Primal Strike: beast attacks bite harder (+5%). L10 + // Elemental Wild Shape: an elemental form's opening slam — a + // FlatDmgStart burst. L15 Archdruid: the form barely tires — a flat + // 15% incoming reduction. + if c.Level >= 5 { + if buf := 3 * c.Level; mods.ArcaneWardHP < buf { + mods.ArcaneWardHP = buf + } + mods.DamageBonus += 0.10 + } + if c.Level >= 7 { + mods.DamageBonus += 0.05 + } + if c.Level >= 10 { + mods.FlatDmgStart += 8 + } + if c.Level >= 15 { + mods.DamageReduct *= 0.85 + } + case SubclassCircleShepherd: + // L5 Spirit Totem (Bear): a warding spirit grants a temp-HP buffer. + // L7 Mighty Summoner: a conjured beast joins the fight — rides the + // pet channel. L10 Guardian Spirit: the spirits mend you — HealItem. + // L15 Faithful Summons: the conjured ally is tougher and strikes + // more often. + if c.Level >= 5 { + if buf := 2 * c.Level; mods.ArcaneWardHP < buf { + mods.ArcaneWardHP = buf + } + } + if c.Level >= 7 { + if mods.PetAttackProc < 0.25 { + mods.PetAttackProc = 0.25 + } + if dmg := 3 + c.Level/2; mods.PetAttackDmg < dmg { + mods.PetAttackDmg = dmg + } + } + if c.Level >= 10 { + mods.HealItem += c.Level / 2 + } + if c.Level >= 15 { + if mods.PetAttackProc < 0.45 { + mods.PetAttackProc = 0.45 + } + if dmg := 8 + c.Level/2; mods.PetAttackDmg < dmg { + mods.PetAttackDmg = dmg + } + } + case SubclassCollegeLore: + // L5 Cutting Words: a barbed quip throws the foe off — 2 rounds of + // SporeCloud miss chance. L7 Additional Magical Secrets: borrowed + // versatility, a flat +1 to attack rolls. L10 Peerless Skill: a + // self-aimed Bardic Inspiration die on the opener (FirstAttackBonus). + // L15: deeper Magical Secrets keep paying out — +10% damage. + if c.Level >= 5 { + mods.SporeCloud += 2 + } + if c.Level >= 7 { + stats.AttackBonus++ + } + if c.Level >= 10 { + mods.FirstAttackBonus += 5 + } + if c.Level >= 15 { + mods.DamageBonus += 0.10 + } + case SubclassCollegeValor: + // L5 Combat Inspiration: songs that sharpen the blade (+10% damage). + // L7 Extra Attack: throughput — +1 attack and a further +10% damage + // proxy for the second swing. L10 Battle Magic: a bonus-action + // strike after a spell — a FlatDmgStart burst. L15: +1 AC, the + // late-game shield half of Combat Inspiration. + if c.Level >= 5 { + mods.DamageBonus += 0.10 + } + if c.Level >= 7 { + stats.AttackBonus++ + mods.DamageBonus += 0.10 + } + if c.Level >= 10 { + mods.FlatDmgStart += 6 + } + if c.Level >= 15 { + stats.AC++ + } + case SubclassCollegeGlamour: + // L5 Mantle of Inspiration: a fey-glamour temp-HP buffer. L7 Mantle + // of Majesty: the foe is briefly enthralled — skips its first + // attack. L10 Enthralling Performance: lingering fascination — 2 + // rounds of SporeCloud miss chance. L15 Unbreakable Majesty: foes + // struggle to bring themselves to strike you — a flat 15% reduction. + if c.Level >= 5 { + if buf := 2 * c.Level; mods.ArcaneWardHP < buf { + mods.ArcaneWardHP = buf + } + } + if c.Level >= 7 { + mods.SpellEnemySkipFirst = true + } + if c.Level >= 10 { + mods.SporeCloud += 2 + } + if c.Level >= 15 { + mods.DamageReduct *= 0.85 + } + case SubclassDraconicBloodline: + // L5 Draconic Resilience: scaled hide — +1 AC and an HP buffer. L7 + // Elemental Affinity: CHA folded into spell damage — +10%. L10 + // Dragon Wings: mobility and a tougher frame — an 8% reduction and a + // small damage bump. L15 Draconic Presence: a frightful aura — 2 + // rounds of SporeCloud miss chance. + if c.Level >= 5 { + stats.AC++ + if mods.ArcaneWardHP < c.Level { + mods.ArcaneWardHP = c.Level + } + } + if c.Level >= 7 { + mods.DamageBonus += 0.10 + } + if c.Level >= 10 { + mods.DamageReduct *= 0.92 + mods.DamageBonus += 0.05 + } + if c.Level >= 15 { + mods.SporeCloud += 2 + } + case SubclassWildMagic: + // L5 Tides of Chaos: fortune favors the opener — advantage on the + // first attack. L7 Bend Luck: nudge a roll your way — + // FirstAttackBonus. L10 Controlled Chaos: a surge you actually aimed + // — a FlatDmgStart burst. L15 Spell Bombardment: dice that keep + // exploding — +15% damage. + if c.Level >= 5 { + mods.AssassinateAdvantage = true + } + if c.Level >= 7 { + mods.FirstAttackBonus += 3 + } + if c.Level >= 10 { + mods.FlatDmgStart += 10 + } + if c.Level >= 15 { + mods.DamageBonus += 0.15 + } + case SubclassStormSorcery: + // L5 Tempestuous Magic: the storm moves first — +1 initiative and + // +2 Speed. L7 Heart of the Storm: a thunderclap as you cast — a + // level-scaled FlatDmgStart burst. L10 Storm Guide: you ride the + // weather — a small 5% reduction and 1 round of SporeCloud. L15 + // Storm's Fury: incoming blows are answered with lightning — + // ReflectNext on the first hit. + if c.Level >= 5 { + mods.InitiativeBias += 1 + stats.Speed += 2 + } + if c.Level >= 7 { + mods.FlatDmgStart += 5 + c.Level/2 + } + if c.Level >= 10 { + mods.DamageReduct *= 0.95 + mods.SporeCloud++ + } + if c.Level >= 15 { + mods.ReflectNext += 0.30 + } + case SubclassFiendPatron: + // L5 Dark One's Blessing: temp HP as foes fall — modeled as an + // up-front CHA-scaled buffer. L7 Dark One's Own Luck: a nudged roll + // — FirstAttackBonus. L10 Fiendish Resilience: chosen resistance — a + // flat 10% incoming reduction. L15 Hurl Through Hell: a foe sent + // briefly to the lower planes — a heavy FlatDmgStart burst. + if c.Level >= 5 { + chaMod := abilityModifier(c.CHA) + if chaMod < 0 { + chaMod = 0 + } + if buf := c.Level + chaMod; mods.ArcaneWardHP < buf { + mods.ArcaneWardHP = buf + } + } + if c.Level >= 7 { + mods.FirstAttackBonus += 3 + } + if c.Level >= 10 { + mods.DamageReduct *= 0.90 + } + if c.Level >= 15 { + mods.FlatDmgStart += 12 + } + case SubclassArchfeyPatron: + // L5 Fey Presence: the foe is briefly charmed/frightened — skips its + // first attack. L7 Misty Escape: you blink out of harm's way — 2 + // rounds of SporeCloud miss chance. L10 Beguiling Defenses: hostile + // magic rebounds — ReflectNext on the first hit. L15 Dark Delirium: + // the foe is lost in illusion — more SporeCloud and a small edge. + if c.Level >= 5 { + mods.SpellEnemySkipFirst = true + } + if c.Level >= 7 { + mods.SporeCloud += 2 + } + if c.Level >= 10 { + mods.ReflectNext += 0.25 + } + if c.Level >= 15 { + mods.SporeCloud += 2 + mods.DamageBonus += 0.05 + } + case SubclassGreatOldOne: + // L5 Entropic Ward: a missed attack tilts the next exchange — a + // small reduction and 1 round of SporeCloud. L7 Thought Shield: + // psychic resilience — a flat 8% incoming reduction. L10 Create + // Thrall: a dominated mind fights beside you — the pet channel. L15: + // deeper psychic dominion keeps the thrall in the fight and sharpens + // your own assault (+10% damage). + if c.Level >= 5 { + mods.DamageReduct *= 0.95 + mods.SporeCloud++ + } + if c.Level >= 7 { + mods.DamageReduct *= 0.92 + } + if c.Level >= 10 { + if mods.PetAttackProc < 0.30 { + mods.PetAttackProc = 0.30 + } + if dmg := 4 + c.Level/2; mods.PetAttackDmg < dmg { + mods.PetAttackDmg = dmg + } + } + if c.Level >= 15 { + mods.DamageBonus += 0.10 + } + case SubclassOathDevotion: + // L5 Sacred Weapon: a blade lit with the oath — +1 attack and + // radiant damage on every hit (DivineStrikePerHit, the channel the + // Cleric domains use). L7 Aura of Devotion: a small warding aura — + // 5% reduction. L10 Aura of Courage/Purity: the aura hardens — a + // further 8% reduction. L15 Holy Nimbus: a radiant aura that sears + // the foe on engagement — a FlatDmgStart burst. + if c.Level >= 5 { + stats.AttackBonus++ + mods.DivineStrikePerHit += 4 + } + if c.Level >= 7 { + mods.DamageReduct *= 0.95 + } + if c.Level >= 10 { + mods.DamageReduct *= 0.92 + } + if c.Level >= 15 { + mods.FlatDmgStart += 10 + } + case SubclassOathVengeance: + // L5 Vow of Enmity: advantage against the marked foe plus the focus + // to press it — advantage on the opener and +10% damage. L7 + // Relentless Avenger: you keep pace with a fleeing target — +2 Speed + // and a small damage bump. L10 Soul of Vengeance: an extra strike at + // the marked foe — +10% damage. L15: the hunt never lets up — + // a FlatDmgStart opener. + if c.Level >= 5 { + mods.AssassinateAdvantage = true + mods.DamageBonus += 0.10 + } + if c.Level >= 7 { + stats.Speed += 2 + mods.DamageBonus += 0.05 + } + if c.Level >= 10 { + mods.DamageBonus += 0.10 + } + if c.Level >= 15 { + mods.FlatDmgStart += 6 + } + case SubclassOathAncients: + // L5 Nature's Wrath: the green and growing things slow the foe — a + // small reduction. L7 Aura of Warding: resistance to spell damage — + // a further 8% reduction. L10 Undying Sentinel: you refuse to fall — + // survive one otherwise-lethal hit (DeathSave). L15 Elder Champion: + // the old light blazes up — +1 attack and +10% damage. + if c.Level >= 5 { + mods.DamageReduct *= 0.95 + } + if c.Level >= 7 { + mods.DamageReduct *= 0.92 + } + if c.Level >= 10 { + mods.DeathSave = true + } + if c.Level >= 15 { + stats.AttackBonus++ + mods.DamageBonus += 0.10 + } } } diff --git a/internal/plugin/dnd_subclass_test.go b/internal/plugin/dnd_subclass_test.go index 400ace3..c9c532c 100644 --- a/internal/plugin/dnd_subclass_test.go +++ b/internal/plugin/dnd_subclass_test.go @@ -12,9 +12,9 @@ import ( // ── Registry sanity ────────────────────────────────────────────────────── -func TestSubclassRegistry_FifteenEntriesThreePerClass(t *testing.T) { - if got := len(dndSubclassRegistry); got != 15 { - t.Fatalf("registry size: got %d, want 15", got) +func TestSubclassRegistry_ThirtyEntriesThreePerClass(t *testing.T) { + if got := len(dndSubclassRegistry); got != 30 { + t.Fatalf("registry size: got %d, want 30", got) } perClass := map[DnDClass]int{} for _, s := range dndSubclassRegistry { @@ -23,7 +23,10 @@ func TestSubclassRegistry_FifteenEntriesThreePerClass(t *testing.T) { t.Errorf("incomplete entry: %+v", s) } } - for _, cls := range []DnDClass{ClassFighter, ClassRogue, ClassMage, ClassCleric, ClassRanger} { + for _, cls := range []DnDClass{ + ClassFighter, ClassRogue, ClassMage, ClassCleric, ClassRanger, + ClassDruid, ClassBard, ClassSorcerer, ClassWarlock, ClassPaladin, + } { if perClass[cls] != 3 { t.Errorf("%s: %d subclasses, want 3", cls, perClass[cls]) } diff --git a/internal/plugin/dnd_xp_test.go b/internal/plugin/dnd_xp_test.go index 109bac6..1bad9a2 100644 --- a/internal/plugin/dnd_xp_test.go +++ b/internal/plugin/dnd_xp_test.go @@ -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) + } } }