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 {
|
switch c.Subclass {
|
||||||
case SubclassChampion:
|
case SubclassChampion:
|
||||||
// L5 Improved Critical: crit on nat 19+. SUB3 Superior Critical
|
// L5 Improved Critical: crit on nat 19+. L15 Superior Critical
|
||||||
// (L15) will lower this to 18.
|
// lowers this further to 18.
|
||||||
if c.Level >= 5 {
|
if c.Level >= 5 {
|
||||||
mods.CritThreshold = 19
|
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:
|
case SubclassBattleMaster:
|
||||||
// L7 Know Your Enemy: 5e gives factual info about the target after
|
// L7 Know Your Enemy: 5e gives factual info about the target after
|
||||||
// 1 min observation. We don't model "observation time" — proxy the
|
// 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