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:
@@ -27,6 +27,11 @@ type DnDAbility struct {
|
||||
ID string
|
||||
Name string
|
||||
Class DnDClass
|
||||
// Subclass: if non-empty, ability is only available when the player's
|
||||
// subclass matches. Phase 10 adds the first such ability (Berserker
|
||||
// rage). Used by parseAbility/arm gating and by classActiveAbilities
|
||||
// when listing for !arm / !abilities.
|
||||
Subclass DnDSubclass
|
||||
Resource string // "stamina", "spell_slot", "favor", etc.
|
||||
Description string
|
||||
// Effect — applied to mods at combat start when armed
|
||||
@@ -83,17 +88,35 @@ func parseAbility(s string) (DnDAbility, bool) {
|
||||
return DnDAbility{}, false
|
||||
}
|
||||
|
||||
// classActiveAbilities returns the active abilities a class can know.
|
||||
// classActiveAbilities returns the active abilities a class can know,
|
||||
// excluding subclass-gated entries (those are listed by characterActiveAbilities).
|
||||
func classActiveAbilities(class DnDClass) []DnDAbility {
|
||||
var out []DnDAbility
|
||||
for _, a := range dndActiveAbilities {
|
||||
if a.Class == class {
|
||||
if a.Class == class && a.Subclass == "" {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// characterActiveAbilities returns abilities available to a given character,
|
||||
// including their subclass-gated abilities. Used for !arm listing so the
|
||||
// Berserker sees `rage` once they've chosen the subclass.
|
||||
func characterActiveAbilities(c *DnDCharacter) []DnDAbility {
|
||||
var out []DnDAbility
|
||||
for _, a := range dndActiveAbilities {
|
||||
if a.Class != c.Class {
|
||||
continue
|
||||
}
|
||||
if a.Subclass != "" && a.Subclass != c.Subclass {
|
||||
continue
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Resource pool ────────────────────────────────────────────────────────────
|
||||
|
||||
// classResourceMax returns (resource_type, max_value) for a class.
|
||||
@@ -201,6 +224,12 @@ func (p *AdventurePlugin) handleDnDArmCmd(ctx MessageContext, args string) error
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"%s is a %s ability — your class is %s.", ab.Name, titleClass(ab.Class), titleClass(c.Class)))
|
||||
}
|
||||
if ab.Subclass != "" && ab.Subclass != c.Subclass {
|
||||
needed, _ := subclassInfo(ab.Subclass)
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"%s is a %s subclass ability — choose that subclass via `!subclass`.",
|
||||
ab.Name, needed.Display))
|
||||
}
|
||||
|
||||
if c.ArmedAbility != "" {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
@@ -244,7 +273,7 @@ func (p *AdventurePlugin) handleDnDArmCmd(ctx MessageContext, args string) error
|
||||
}
|
||||
|
||||
func renderArmList(c *DnDCharacter) string {
|
||||
abilities := classActiveAbilities(c.Class)
|
||||
abilities := characterActiveAbilities(c)
|
||||
var b strings.Builder
|
||||
b.WriteString("**Active Abilities** (pre-arm via `!arm <name>`)\n\n")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user