mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
UX S2: caster onboarding — strip reaction defaults, deterministic healing_word, route hints
B2: Removed shield/counterspell from Mage/Sorcerer defaults and hellish_rebuke/counterspell
from Warlock — reactions are EffectReaction and combat has no reaction window yet, so
auto-granting them just litters the spellbook with dead entries. Substituted L1/L3
staples (sleep, fly, burning_hands, chromatic_orb where class-tagged appropriately).
B3: Replaced the stale "Mage, Cleric, Ranger, and Arcane Trickster (L5+)" enumeration in
!cast/!spells learn/!prepare error paths with class-aware route hints via a new
spellRouteHintFor() helper.
R15: Removed the duplicate hand-authored healing_word_spell entry; Cleric default now
references the canonical SRD healing_word (cleric/druid/bard tagged). parseSpell
and lookupSpell now resolve "healing word" deterministically.
R16: !cast --drop now appends "(slot refunded)" when applicable.
R17: Prep-cap message rewritten to "Your prepared list is full (N/N)."
R19: Removed shillelagh from Druid default — the overlay flags it cleric/ranger only and
it has no real attack profile yet (BuffSelf, no DamageDice).
Bonus correctness: dndSpellRegistry now unions Classes on overlay collision instead of
replacing wholesale. The hand-authored buildSpellList() entries were tagged Mage-only,
so every Sorcerer/Warlock/Bard/Druid spell that collided with the SRD silently lost its
class list — meaning auto-granted defaults were rejected at the classOK gate. Union
merge restores SRD coverage without forcing a hand-edit of every overlay entry.
Tests:
- TestDefaultKnownSpellsHaveNoReactions covers B2.
- TestDefaultKnownSpellsAreCastableByClass guards the overlay-narrow regression.
- TestHealingWordResolvesDeterministically covers R15.
- TestCastNonCasterErrorHasNoClassEnumeration covers B3.
- Existing dnd_prepare_test.go updated to reference healing_word.
This commit is contained in:
@@ -80,8 +80,7 @@ func (p *AdventurePlugin) handleDnDCastCmd(ctx MessageContext, args string) erro
|
||||
}
|
||||
if !isSpellcaster(c) {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"%s isn't a caster class. `!cast` is for Mage, Cleric, Ranger, and Arcane Trickster Rogues (L5+).",
|
||||
titleClass(c.Class)))
|
||||
"%s doesn't cast spells.", titleClass(c.Class)))
|
||||
}
|
||||
|
||||
if args == "" {
|
||||
@@ -363,8 +362,10 @@ func (p *AdventurePlugin) dndCastDrop(ctx MessageContext, c *DnDCharacter) error
|
||||
|
||||
// Refund the queued slot (if any) — voluntary drop is the player's
|
||||
// choice; restore the slot rather than forfeit it.
|
||||
slotRefunded := false
|
||||
if pc, ok := decodePendingCast(c.PendingCast); ok && pc.SlotLevel > 0 {
|
||||
_ = refundSpellSlot(ctx.Sender, pc.SlotLevel)
|
||||
slotRefunded = true
|
||||
}
|
||||
|
||||
dropped := []string{}
|
||||
@@ -381,7 +382,11 @@ func (p *AdventurePlugin) dndCastDrop(ctx MessageContext, c *DnDCharacter) error
|
||||
if err := SaveDnDCharacter(c); err != nil {
|
||||
return p.SendDM(ctx.Sender, "Couldn't drop: "+err.Error())
|
||||
}
|
||||
return p.SendDM(ctx.Sender, "Dropped: "+strings.Join(dropped, ", ")+".")
|
||||
msg := "Dropped: " + strings.Join(dropped, ", ") + "."
|
||||
if slotRefunded {
|
||||
msg += " _(slot refunded)_"
|
||||
}
|
||||
return p.SendDM(ctx.Sender, msg)
|
||||
}
|
||||
|
||||
// ── !spells command ─────────────────────────────────────────────────────────
|
||||
@@ -487,7 +492,8 @@ func renderSpellsList(c *DnDCharacter) string {
|
||||
|
||||
func (p *AdventurePlugin) handleSpellsLearn(ctx MessageContext, c *DnDCharacter, raw string) error {
|
||||
if c.Class != ClassMage {
|
||||
return p.SendDM(ctx.Sender, "`!spells learn` is for Mage. Cleric uses `!prepare`; Ranger spells are auto-known.")
|
||||
return p.SendDM(ctx.Sender, "`!spells learn` is for Mage. "+spellRouteHintFor(c)+
|
||||
" Run `!spells` to see what you already know.")
|
||||
}
|
||||
if raw == "" {
|
||||
return p.SendDM(ctx.Sender, "Usage: `!spells learn <spell name>`")
|
||||
@@ -561,7 +567,8 @@ func (p *AdventurePlugin) handleDnDPrepareCmd(ctx MessageContext, args string) e
|
||||
return p.SendDM(ctx.Sender, "Couldn't load your Adv 2.0 sheet.")
|
||||
}
|
||||
if c.Class != ClassCleric {
|
||||
return p.SendDM(ctx.Sender, "`!prepare` is for Cleric. Mage uses `!spells learn`; Ranger spells are auto-known.")
|
||||
return p.SendDM(ctx.Sender, "`!prepare` is for Cleric. "+spellRouteHintFor(c)+
|
||||
" Run `!spells` to see what you already know.")
|
||||
}
|
||||
args = strings.TrimSpace(args)
|
||||
if args == "" {
|
||||
@@ -611,7 +618,8 @@ func (p *AdventurePlugin) handleDnDPrepareCmd(ctx MessageContext, args string) e
|
||||
}
|
||||
if prepCount+1 > cap {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"Prep cap is %d. Unprepare a spell first: `!prepare clear <name>`.", cap))
|
||||
"Your prepared list is full (%d/%d). Unprepare one first: `!prepare clear <name>`.",
|
||||
prepCount, cap))
|
||||
}
|
||||
if err := setSpellPrepared(ctx.Sender, spell.ID, true); err != nil {
|
||||
return p.SendDM(ctx.Sender, "Couldn't prepare: "+err.Error())
|
||||
@@ -640,6 +648,30 @@ func renderClericPrepStatus(c *DnDCharacter) string {
|
||||
prepCount, cap)
|
||||
}
|
||||
|
||||
// spellRouteHintFor returns a one-sentence pointer telling the player which
|
||||
// command actually advances their spellbook, scoped to their class. Used by
|
||||
// the !spells learn / !prepare wrong-class error messages so the player isn't
|
||||
// left guessing after we reject them.
|
||||
func spellRouteHintFor(c *DnDCharacter) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
switch c.Class {
|
||||
case ClassMage:
|
||||
return "Mage learns spells with `!spells learn <name>`."
|
||||
case ClassCleric:
|
||||
return "Cleric picks today's spells with `!prepare <name>`."
|
||||
case ClassRogue:
|
||||
if c.Subclass == SubclassArcaneTrickster {
|
||||
return "Arcane Trickster learns spells with `!spells learn <name>` once it unlocks."
|
||||
}
|
||||
return ""
|
||||
case ClassDruid, ClassBard, ClassSorcerer, ClassWarlock, ClassPaladin, ClassRanger:
|
||||
return "Your class learns spells automatically as you level — nothing to prepare."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func renderCastHelp(c *DnDCharacter) string {
|
||||
|
||||
Reference in New Issue
Block a user