Files
gogobee/internal/plugin/dnd_passives.go
prosolis 9e1a1f606c Adv 2.0 D&D layer: Phases 1-8 + Phase 9 SP1+SP2
Combines all uncommitted D&D work into one checkpoint:

Phases 1-8 (per gogobee_dnd_session_summary.md):
- Race/class/stats system, !setup flow, !sheet, !respec
- d20-vs-AC combat with race/class passives, auto-migration
- XP/level-up, skill checks, NPC refund hooks
- Equipment slot mapping, rarity, !rest short/long
- Pre-arm active abilities, resource pools
- Bestiary, !roll/!stats/!level, onboarding DM
- Audit fixes A-I, flavor wiring under internal/flavor/
- Phase 8 weapon dice + armor AC tables (gogobee_equipment_appendix)

Phase 9 SP1 — spell system foundations:
- 79 SpellDefinitions (76 in-scope + 3 reaction-deferred)
- dnd_known_spells, dnd_spell_slots tables
- pending_cast / concentration_* columns on dnd_character
- Slot tables for Mage/Cleric/Ranger, DC + attack-bonus math
- Long-rest refreshes slots; respec wipes spell state
- Auto-grant default known list on !setup confirm and auto-migration

Phase 9 SP2 — !cast / !spells / !prepare commands:
- Out-of-combat HEAL and UTILITY resolve immediately
- Damage/control/buff queue as pending_cast for next combat
- Audit-style save-then-debit, slot refund on save failure
- !cast --drop, concentration supersession, prep-cap enforcement
- Reaction spells refused with Phase 11 note

SP3 (combat-time resolution of pending_cast) and SP4 (full prep/learn
tests) are next. Build clean, full test suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:21 -07:00

90 lines
3.3 KiB
Go

package plugin
// Phase 3 — class passives applied via CombatModifiers.
//
// Phase 3 keeps abilities passive-only: they auto-trigger via the existing
// CombatModifiers machinery rather than being player-activated mid-fight
// (the combat engine is one-shot, no turn-based UI). Active/reactive
// abilities arrive once we have a model for them — likely tied to !arena
// pre-arming or to a turn-based PvP variant.
// ── Class passive definitions ────────────────────────────────────────────────
// DnDClassAbility is the lore/UI representation of a class's signature
// passive. Used by !abilities and !sheet for display; mechanics live in
// applyClassPassives below.
type DnDClassAbility struct {
Name string
Description string
}
var dndClassAbilities = map[DnDClass]DnDClassAbility{
ClassFighter: {
Name: "Battle Trained",
Description: "Years of weapon drill add +5% to all damage you deal.",
},
ClassRogue: {
Name: "Sneak Attack",
Description: "Your first strike each combat lands as a critical hit, doubling its damage.",
},
ClassMage: {
Name: "Arcane Focus",
Description: "Practiced channeling adds +1 to your attack rolls.",
},
ClassCleric: {
Name: "Divine Favor",
Description: "When you fall below half HP, divine intervention restores 5 HP. Once per combat.",
},
ClassRanger: {
Name: "Hunter's Mark",
Description: "You read your prey's weak points: +5% damage and +1 to attack rolls.",
},
}
// applyRacePassives sets the combat-impacting flags from the player's race.
// Races whose passives apply to skill checks or non-combat scenarios
// (Tiefling fire resist, Elf sleep immunity, Half-Elf bonus profs, Human
// floating +1) are handled in their respective phases and have no combat
// hook in Phase 3.
func applyRacePassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharacter) {
switch c.Race {
case RaceHalfling:
mods.LuckyReroll = true
case RaceOrc:
mods.RageReady = true
case RaceDwarf:
mods.PoisonResist = true
}
}
// applyClassPassives mutates a player's CombatModifiers to apply their
// class passive. Called after applyDnDPlayerLayer + DerivePlayerStats but
// BEFORE consumable application (so consumables can stack on top).
//
// Some passives ride on existing CombatModifiers fields:
// Rogue's Sneak Attack reuses AutoCritFirst (the consumable Crystal Berry
// field) — the engine already implements first-hit-auto-crit semantics.
// Cleric's Divine Favor reuses HealItem, the under-50%-HP heal trigger.
//
// This means:
// - A Rogue carrying a Crystal Berry doesn't get *two* auto-crits;
// AutoCritFirst is already a one-shot bool.
// - A Cleric carrying a healing potion stacks: passive 5 + potion 8 = 13.
// The passive heal triggers first since both use the same threshold.
func applyClassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharacter) {
switch c.Class {
case ClassFighter:
mods.DamageBonus += 0.05
case ClassRogue:
mods.AutoCritFirst = true
case ClassMage:
stats.AttackBonus++
case ClassCleric:
// Passive heal at <50% HP. Stacks additively with consumable HealItem.
mods.HealItem += 5
case ClassRanger:
mods.DamageBonus += 0.05
stats.AttackBonus++
}
}