Adv 2.0 D&D Phase 10 SUB3a-i: Champion + Berserker L10/L15

Champion L10 Additional Fighting Style → +1 AC + 10% damage (Defense +
Dueling collapse, since one-shot combat can't surface a style picker).
Champion L15 Superior Critical → CritThreshold drops 19 → 18.
Berserker L10 Intimidating Presence → 2 rounds of SporeCloud miss
chance (Action-based fright proxied via the existing fog-cloud channel).
Berserker L15 Retaliation → +15% damage (reaction-strike average uplift,
since the engine has no reaction primitive).

6 new tests; full plugin suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-08 11:03:07 -07:00
parent b56e53300e
commit 97c2c37396
2 changed files with 108 additions and 2 deletions

View File

@@ -17,11 +17,44 @@ func applySubclassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDChar
}
switch c.Subclass {
case SubclassChampion:
// L5 Improved Critical: crit on nat 19+. SUB3 Superior Critical
// (L15) will lower this to 18.
// L5 Improved Critical: crit on nat 19+. L15 Superior Critical
// lowers this further to 18.
if c.Level >= 5 {
mods.CritThreshold = 19
}
// Phase 10 SUB3a-i — L10 Additional Fighting Style. 5e lets the
// player pick a second style; in 1v1 we can't surface a UI for
// that, so we collapse to the two most generally useful
// approximations stacked: Defense (+1 AC) and Dueling (+10%
// damage when one-handed — we don't track grip state, so this is
// a flat damage bump regardless).
if c.Level >= 10 {
stats.AC++
mods.DamageBonus += 0.10
}
// Phase 10 SUB3a-i — L15 Superior Critical: crit floor drops to 18.
if c.Level >= 15 {
mods.CritThreshold = 18
}
case SubclassBerserker:
// L5/L7 Berserker is rage-driven (active ability — see init() below)
// plus L7 Mindless Rage which is condition immunity, irrelevant in
// our 1v1 model. Phase 10 SUB3a-i adds the always-on L10/L15 layer.
//
// L10 Intimidating Presence: 5e is an Action that frightens one
// target on a failed WIS save. One-shot combat has no action
// economy to spend on a non-damage Action, so we proxy as 2 rounds
// of 15% enemy miss chance via SporeCloud (the same channel
// fog-cloud / Cloak of Shadows uses).
// L15 Retaliation: 5e Reaction — when struck in melee, one melee
// attack back. We don't model reactions, so the average per-fight
// damage uplift is approximated as a +15% damage multiplier.
if c.Level >= 10 {
mods.SporeCloud += 2
}
if c.Level >= 15 {
mods.DamageBonus += 0.15
}
case SubclassBattleMaster:
// L7 Know Your Enemy: 5e gives factual info about the target after
// 1 min observation. We don't model "observation time" — proxy the

View File

@@ -868,3 +868,76 @@ func TestCharacterActiveAbilities_RageVisibility(t *testing.T) {
}
}
// ── SUB3a-i — Champion + Berserker L10/L15 ──────────────────────────────
func TestApplySubclassPassives_ChampionL10AdditionalFightingStyle(t *testing.T) {
c := &DnDCharacter{Class: ClassFighter, Subclass: SubclassChampion, Level: 10}
stats := &CombatStats{AC: 16}
mods := &CombatModifiers{DamageReduct: 1.0}
applySubclassPassives(stats, mods, c)
if stats.AC != 17 {
t.Errorf("L10 Champion AC = %d, want 17 (+1 Defense style)", stats.AC)
}
if mods.DamageBonus < 0.099 || mods.DamageBonus > 0.101 {
t.Errorf("L10 Champion DamageBonus = %v, want ~0.10 (Dueling style)", mods.DamageBonus)
}
// L5 Improved Critical still in effect; L15 hasn't yet kicked in.
if mods.CritThreshold != 19 {
t.Errorf("L10 Champion CritThreshold = %d, want 19 (L15 not yet)", mods.CritThreshold)
}
}
func TestApplySubclassPassives_ChampionL9NoFightingStyle(t *testing.T) {
c := &DnDCharacter{Class: ClassFighter, Subclass: SubclassChampion, Level: 9}
stats := &CombatStats{AC: 16}
mods := &CombatModifiers{DamageReduct: 1.0}
applySubclassPassives(stats, mods, c)
if stats.AC != 16 {
t.Errorf("L9 Champion AC = %d, want 16 (Fighting Style gates at L10)", stats.AC)
}
if mods.DamageBonus != 0 {
t.Errorf("L9 Champion DamageBonus = %v, want 0", mods.DamageBonus)
}
}
func TestApplySubclassPassives_ChampionL15SuperiorCritical(t *testing.T) {
c := &DnDCharacter{Class: ClassFighter, Subclass: SubclassChampion, Level: 15}
mods := &CombatModifiers{DamageReduct: 1.0}
applySubclassPassives(&CombatStats{AC: 16}, mods, c)
if mods.CritThreshold != 18 {
t.Errorf("L15 Champion CritThreshold = %d, want 18 (Superior Critical)", mods.CritThreshold)
}
}
func TestApplySubclassPassives_BerserkerL10IntimidatingPresence(t *testing.T) {
c := &DnDCharacter{Class: ClassFighter, Subclass: SubclassBerserker, Level: 10}
mods := &CombatModifiers{DamageReduct: 1.0}
applySubclassPassives(&CombatStats{}, mods, c)
if mods.SporeCloud != 2 {
t.Errorf("L10 Berserker SporeCloud = %d, want 2 (Intimidating Presence)", mods.SporeCloud)
}
if mods.DamageBonus != 0 {
t.Errorf("L10 Berserker DamageBonus = %v, want 0 (Retaliation gates at L15)", mods.DamageBonus)
}
}
func TestApplySubclassPassives_BerserkerL9NoIntimidatingPresence(t *testing.T) {
c := &DnDCharacter{Class: ClassFighter, Subclass: SubclassBerserker, Level: 9}
mods := &CombatModifiers{DamageReduct: 1.0}
applySubclassPassives(&CombatStats{}, mods, c)
if mods.SporeCloud != 0 {
t.Errorf("L9 Berserker SporeCloud = %d, want 0", mods.SporeCloud)
}
}
func TestApplySubclassPassives_BerserkerL15Retaliation(t *testing.T) {
c := &DnDCharacter{Class: ClassFighter, Subclass: SubclassBerserker, Level: 15}
mods := &CombatModifiers{DamageReduct: 1.0}
applySubclassPassives(&CombatStats{}, mods, c)
if mods.DamageBonus < 0.149 || mods.DamageBonus > 0.151 {
t.Errorf("L15 Berserker DamageBonus = %v, want ~0.15 (Retaliation)", mods.DamageBonus)
}
if mods.SporeCloud != 2 {
t.Errorf("L15 Berserker SporeCloud = %d, want 2 (L10 still active)", mods.SporeCloud)
}
}