Adv 2.0 D&D Phase 10 SUB2a-i: Champion + Berserker + Thief L5/L7

First mechanical slice of the subclass system. Each subclass gets its L5
and L7 abilities wired through the existing combat engine + skill check
substrate; no new resource pools introduced.

Champion (L5/L7):
- Improved Critical: new Mods.CritThreshold lowers crit floor to nat 19+
  in resolvePlayerAttack. Default 20 preserved for non-Champions.
- Remarkable Athlete: +½ proficiency bonus to STR/DEX/CON skill checks
  via subclassSkillBonus, layered on raceSkillBonus.

Berserker (L5/L7):
- Rage: new !arm-able active ability gated to Berserker subclass via
  DnDAbility.Subclass field. Reuses the Fighter stamina pool (3/long-rest
  matches 5e's rage uses). On fire: +2 flat damage per hit, halve incoming
  weapon damage, +50% damage approximation for Frenzy's bonus-attack-per-
  turn (one-shot combat can't model that literally).
- Frenzy exhaustion: new exhaustion column on dnd_character; incremented
  by persistDnDPostCombatSubclass after any rage'd combat. Decremented by
  one per long rest.
- Mindless Rage: documented; charm/frighten conditions don't exist in the
  combat engine yet, so no functional effect until P11 zone bosses land.

Thief (L5/L7):
- Sleight of Hand added to dndSkillTable.
- Fast Hands: +5 to Sleight of Hand checks via subclassSkillBonus.
- Supreme Sneak: advantage on Stealth (best of two d20s) via new
  subclassSkillAdvantage hook in performSkillCheck. 5e's half-speed gate
  dropped — we don't track movement speed.

Plumbing:
- applySubclassPassives layered after applyClassPassives in both arena
  and dungeon combat bridges.
- DnDAbility.Subclass enables per-subclass gating; characterActiveAbilities
  filters !arm and !abilities lists by both class and subclass.
- Existing !respec full-wipe also clears Exhaustion.

Tests: Champion CritThreshold gating across L4/L5/no-subclass; rage Apply
flag-set; rage probabilistic win-rate lift vs tougher enemy; exhaustion
increment only on raged combats; long-rest decrement; Champion bonus on
STR/DEX/CON only and only at L7+; Thief Fast Hands SoH-only; Thief
Supreme Sneak L7+ Stealth-only; statistical advantage roll lifts Thief
stealth average by ≥2 vs plain Rogue; !arm rage subclass gating; schema
roundtrip; characterActiveAbilities visibility.
This commit is contained in:
prosolis
2026-05-08 10:02:40 -07:00
parent 1ee4276bcd
commit 437460c9b2
12 changed files with 653 additions and 26 deletions

View File

@@ -21,16 +21,17 @@ import (
type DnDSkill string
const (
SkillAthletics DnDSkill = "athletics"
SkillAcrobatics DnDSkill = "acrobatics"
SkillStealth DnDSkill = "stealth"
SkillArcana DnDSkill = "arcana"
SkillInvestigation DnDSkill = "investigation"
SkillPerception DnDSkill = "perception"
SkillInsight DnDSkill = "insight"
SkillPersuasion DnDSkill = "persuasion"
SkillIntimidation DnDSkill = "intimidation"
SkillDeception DnDSkill = "deception"
SkillAthletics DnDSkill = "athletics"
SkillAcrobatics DnDSkill = "acrobatics"
SkillStealth DnDSkill = "stealth"
SkillSleightOfHand DnDSkill = "sleight_of_hand"
SkillArcana DnDSkill = "arcana"
SkillInvestigation DnDSkill = "investigation"
SkillPerception DnDSkill = "perception"
SkillInsight DnDSkill = "insight"
SkillPersuasion DnDSkill = "persuasion"
SkillIntimidation DnDSkill = "intimidation"
SkillDeception DnDSkill = "deception"
)
type dndSkillInfo struct {
@@ -43,6 +44,7 @@ var dndSkillTable = []dndSkillInfo{
{SkillAthletics, "Athletics", "str"},
{SkillAcrobatics, "Acrobatics", "dex"},
{SkillStealth, "Stealth", "dex"},
{SkillSleightOfHand, "Sleight of Hand", "dex"},
{SkillArcana, "Arcana", "int"},
{SkillInvestigation, "Investigation", "int"},
{SkillPerception, "Perception", "wis"},
@@ -136,6 +138,54 @@ func statValue(c *DnDCharacter, stat string) int {
return 10
}
// subclassSkillBonus returns the additive bonus from a subclass ability
// for a given skill. Phase 10 SUB2a:
//
// Champion (L7+) Remarkable Athlete: +½ proficiency bonus (rounded down)
// to STR/DEX/CON skill checks. Approximation of 5e's "+½ prof to
// checks not already proficient"; we don't track per-skill prof yet.
// Thief (L5+) Fast Hands: +5 to Sleight of Hand. Stand-in for 5e's
// "bonus action: SoH check" — out-of-combat utility flattened into a
// raw skill bonus.
func subclassSkillBonus(c *DnDCharacter, info dndSkillInfo) int {
if c == nil || c.Subclass == "" {
return 0
}
switch c.Subclass {
case SubclassChampion:
if c.Level >= 7 {
switch info.Stat {
case "str", "dex", "con":
return proficiencyBonus(c.Level) / 2
}
}
case SubclassThief:
if c.Level >= 5 && info.Key == SkillSleightOfHand {
return 5
}
}
return 0
}
// subclassSkillAdvantage reports whether the player rolls with advantage
// (best of two d20s) on this skill check. Phase 10 SUB2a:
//
// Thief (L7+) Supreme Sneak: advantage on Stealth (5e gates this on
// half-speed movement; we don't track movement speed, so we apply
// unconditionally — approachability cut).
func subclassSkillAdvantage(c *DnDCharacter, info dndSkillInfo) bool {
if c == nil || c.Subclass == "" {
return false
}
switch c.Subclass {
case SubclassThief:
if c.Level >= 7 && info.Key == SkillStealth {
return true
}
}
return false
}
// raceSkillBonus returns the per-skill bonus from a player's race.
// Half-Elf gets +1 to every skill (rough mapping of "two bonus skill profs").
// Tiefling gets +2 to CHA-based skills (matches the doc's "+bonus on CHA checks").
@@ -151,11 +201,20 @@ func raceSkillBonus(c *DnDCharacter, info dndSkillInfo) int {
return 0
}
// performSkillCheck rolls the d20 and computes the result.
// performSkillCheck rolls the d20 and computes the result. Subclass
// effects (Phase 10 SUB2a) apply additive bonuses and may grant advantage
// (best of two d20s).
func performSkillCheck(c *DnDCharacter, skill DnDSkill, dc int) SkillCheckResult {
info, _ := skillInfo(skill)
mod := abilityModifier(statValue(c, info.Stat)) + raceSkillBonus(c, info)
mod := abilityModifier(statValue(c, info.Stat)) +
raceSkillBonus(c, info) +
subclassSkillBonus(c, info)
roll := 1 + rand.IntN(20)
if subclassSkillAdvantage(c, info) {
if alt := 1 + rand.IntN(20); alt > roll {
roll = alt
}
}
res := SkillCheckResult{Skill: skill, DC: dc, Roll: roll, Mod: mod}
switch roll {