mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +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:
@@ -53,6 +53,21 @@ type CombatModifiers struct {
|
||||
RageReady bool // Orc: when HP first drops <50%, next attack deals +50% damage
|
||||
PoisonResist bool // Dwarf: poison tick damage halved
|
||||
|
||||
// Phase 10 SUB2a — subclass combat hooks.
|
||||
// CritThreshold: lowest d20 roll that crits. 0 = use default (20).
|
||||
// Champion L5 Improved Critical sets this to 19; Champion L15
|
||||
// Superior Critical (SUB3) will set 18.
|
||||
CritThreshold int
|
||||
// BerserkerRage: while true, +RageMeleeDmg flat damage per hit and
|
||||
// incoming weapon damage halved (PhysicalResistRage). Frenzy also
|
||||
// adds FrenzyDmgBonus on top to model the bonus-attack-per-turn that
|
||||
// one-shot combat can't represent literally. Set by armed `rage`
|
||||
// ability for Berserker subclass.
|
||||
BerserkerRage bool
|
||||
RageMeleeDmg int // flat damage per hit while raging (5: +2)
|
||||
PhysicalResistRage bool // halve incoming physical damage while raging
|
||||
FrenzyDmgBonus float64 // multiplicative dmg bump while raging (Frenzy approximation)
|
||||
|
||||
// Phase 9 — pending spell resolution. Set by applyPendingCast in
|
||||
// dnd_spell_combat.go before SimulateCombat runs. SpellPreDamage is
|
||||
// dealt as a pre-combat event with SpellPreDamageDesc as the narrative
|
||||
@@ -469,7 +484,13 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
||||
roll = newRoll
|
||||
}
|
||||
isFumble := roll == 1
|
||||
isNat20 := roll == 20
|
||||
// Phase 10 SUB2a — Champion Improved Critical lowers the crit floor.
|
||||
// CritThreshold==0 means "use default" (nat 20). Champion sets 19.
|
||||
critFloor := 20
|
||||
if player.Mods.CritThreshold > 0 && player.Mods.CritThreshold < 20 {
|
||||
critFloor = player.Mods.CritThreshold
|
||||
}
|
||||
isCritRoll := roll >= critFloor
|
||||
// Class proficiency penalty (appendix §8): -4 attack with a non-proficient weapon.
|
||||
attackBonus := player.Stats.AttackBonus
|
||||
if player.Stats.Weapon != nil && !player.Stats.WeaponProficient {
|
||||
@@ -477,7 +498,7 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
||||
}
|
||||
total := roll + attackBonus
|
||||
|
||||
if isFumble || (!isNat20 && total < enemy.Stats.AC) {
|
||||
if isFumble || (!isCritRoll && total < enemy.Stats.AC) {
|
||||
desc := ""
|
||||
if isFumble {
|
||||
desc = "fumble"
|
||||
@@ -518,7 +539,7 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
||||
dmg = max(1, dmg/2)
|
||||
}
|
||||
|
||||
isCrit := isNat20
|
||||
isCrit := isCritRoll
|
||||
if st.autoCrit {
|
||||
isCrit = true
|
||||
st.autoCrit = false
|
||||
@@ -533,6 +554,17 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
||||
dmg = int(float64(dmg) * 1.5)
|
||||
st.pendingRageAttack = false
|
||||
}
|
||||
// Phase 10 SUB2a — Berserker rage: +flat per hit, plus Frenzy
|
||||
// multiplicative bump that approximates the bonus-attack-per-turn we
|
||||
// can't model in one-shot combat.
|
||||
if player.Mods.BerserkerRage {
|
||||
if player.Mods.RageMeleeDmg > 0 {
|
||||
dmg += player.Mods.RageMeleeDmg
|
||||
}
|
||||
if player.Mods.FrenzyDmgBonus > 0 {
|
||||
dmg = int(float64(dmg) * (1 + player.Mods.FrenzyDmgBonus))
|
||||
}
|
||||
}
|
||||
dmg = max(1, dmg)
|
||||
|
||||
action := "hit"
|
||||
@@ -620,6 +652,11 @@ func resolveEnemyAttack(st *combatState, player, enemy *Combatant, phase *Combat
|
||||
if isCrit {
|
||||
dmg *= 2
|
||||
}
|
||||
// Phase 10 SUB2a — Berserker rage halves incoming weapon damage.
|
||||
// Applied AFTER crit-doubling so the resistance survives crits.
|
||||
if player.Mods.BerserkerRage && player.Mods.PhysicalResistRage {
|
||||
dmg = max(1, dmg/2)
|
||||
}
|
||||
dmg = max(1, dmg)
|
||||
|
||||
if petDeflect {
|
||||
|
||||
Reference in New Issue
Block a user