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