mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user