mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
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:
73
internal/plugin/dnd_subclass_combat.go
Normal file
73
internal/plugin/dnd_subclass_combat.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package plugin
|
||||
|
||||
// Phase 10 SUB2a — subclass combat hooks.
|
||||
//
|
||||
// applySubclassPassives layers subclass-driven flags onto CombatModifiers
|
||||
// the same way applyClassPassives does. Called from combat_bridge after
|
||||
// applyClassPassives so subclass effects can stack on top of (or override)
|
||||
// class baseline.
|
||||
//
|
||||
// Subclass active abilities (currently just Berserker's rage) are
|
||||
// registered via init() into dndActiveAbilities below, gated by
|
||||
// DnDAbility.Subclass.
|
||||
|
||||
func applySubclassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharacter) {
|
||||
if c == nil || c.Subclass == "" {
|
||||
return
|
||||
}
|
||||
switch c.Subclass {
|
||||
case SubclassChampion:
|
||||
// L5 Improved Critical: crit on nat 19+. SUB3 Superior Critical
|
||||
// (L15) will lower this to 18.
|
||||
if c.Level >= 5 {
|
||||
mods.CritThreshold = 19
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Berserker rage — active ability that piggybacks on the Fighter
|
||||
// stamina pool. 5e specs 3 uses/long-rest as a separate pool, but
|
||||
// stamina is already sized at 3 for Fighter and the design's
|
||||
// "approachability" constraint preferred reusing the existing pool
|
||||
// over introducing per-subclass resource types in SUB2a.
|
||||
//
|
||||
// While armed and fired:
|
||||
// - +2 flat damage per hit (RageMeleeDmg)
|
||||
// - incoming weapon damage halved (PhysicalResistRage)
|
||||
// - +50% damage multiplier for Frenzy approximation
|
||||
// (one-shot combat can't model "1 bonus attack per turn" literally)
|
||||
//
|
||||
// Post-combat exhaustion increment lives in
|
||||
// persistDnDPostCombatSubclass — Frenzy specs +1 exhaustion after rage
|
||||
// ends, and in our model rage spans the whole one-shot combat so we
|
||||
// always tick exhaustion when rage fired.
|
||||
dndActiveAbilities["rage"] = DnDAbility{
|
||||
ID: "rage",
|
||||
Name: "Rage",
|
||||
Class: ClassFighter,
|
||||
Subclass: SubclassBerserker,
|
||||
Resource: "stamina",
|
||||
Description: "Enter rage for the next combat: +2 damage per hit, halve incoming weapon damage, Frenzy bonus attack approximation. +1 exhaustion after.",
|
||||
Apply: func(c *DnDCharacter, mods *CombatModifiers) {
|
||||
mods.BerserkerRage = true
|
||||
mods.RageMeleeDmg = 2
|
||||
mods.PhysicalResistRage = true
|
||||
mods.FrenzyDmgBonus = 0.5
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// persistDnDPostCombatSubclass handles end-of-combat subclass bookkeeping.
|
||||
// Currently: increment Exhaustion if Berserker rage fired. Called from
|
||||
// combat_bridge after the combat result is computed.
|
||||
//
|
||||
// raged is computed as (mods.BerserkerRage at combat-start time) — pass it
|
||||
// through from the caller where the mods value is still in scope.
|
||||
func persistDnDPostCombatSubclass(c *DnDCharacter, raged bool) error {
|
||||
if c == nil || !raged {
|
||||
return nil
|
||||
}
|
||||
c.Exhaustion++
|
||||
return SaveDnDCharacter(c)
|
||||
}
|
||||
Reference in New Issue
Block a user