mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
Adv 2.0 D&D Phase 10 SUB2-AT: Arcane Trickster spellcasting
Slots into the Phase 9 spell pipeline so an L5+ Rogue who picks Arcane
Trickster becomes a third-caster on the Mage list, INT-based.
- isSpellcaster(c) gate: full casters by class plus AT Rogue at L5+.
!cast / !spells now consult this; the cast class-gate also accepts
Mage-tagged spells when the player is an AT Rogue.
- subclassSpellSlots() / slotsForCharacter() / setSpellSlotsForCharacter()
layer subclass slots on top of the class baseline. AT pool: 3 L1 at
L5–6, 4 L1 + 2 L2 at L7+, +2 L3 at L13+ (third-caster table).
- spellcastingMod returns INT for AT Rogue.
- L7 Magical Ambush proxy: spellSaveDC bumps +2 once Rogue+AT hits L7,
standing in for "targets have disadvantage when casting from hidden"
since the one-shot combat layer doesn't model hidden state.
- ensureSpellsForCharacter bootstraps a Mage-list starter spellbook
(minor_illusion, shocking_grasp, magic_missile, shield, sleep at L5;
mirror_image + misty_step at L7+; hypnotic_pattern at L13+) and
refreshes the slot pool on level/subclass change.
- applySubclassChoice runs ensureSpellsForCharacter for AT so the
spellbook is provisioned at selection time (idempotent).
- !cast clamps upcasts to highestAvailableSlotForChar when AT, since
third-caster's slot ceiling lags behind the spell list.
Tests added: AT spellcaster gate (L5 cutoff, non-AT Rogue rejected), INT
mod, subclass slot pool by tier, L7 ambush DC bump, ensureSpells default
list (all on Mage list, slots correct), end-to-end !cast queues
magic_missile for AT Rogue, !cast rejects plain Thief Rogue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,9 +71,9 @@ func (p *AdventurePlugin) handleDnDCastCmd(ctx MessageContext, args string) erro
|
||||
if err != nil || c == nil {
|
||||
return p.SendDM(ctx.Sender, "Couldn't load your Adv 2.0 sheet.")
|
||||
}
|
||||
if !classIsCaster(c.Class) {
|
||||
if !isSpellcaster(c) {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"%s isn't a caster class. `!cast` is for Mage, Cleric, and Ranger.",
|
||||
"%s isn't a caster class. `!cast` is for Mage, Cleric, Ranger, and Arcane Trickster Rogues (L5+).",
|
||||
titleClass(c.Class)))
|
||||
}
|
||||
|
||||
@@ -119,10 +119,15 @@ func (p *AdventurePlugin) handleDnDCastCmd(ctx MessageContext, args string) erro
|
||||
"Unknown spell %q. Run `!spells` to list what you know.", strings.Join(spellTokens, " ")))
|
||||
}
|
||||
|
||||
// Class gate.
|
||||
// Class gate. Arcane Trickster Rogues use the Mage spell list (Phase 10
|
||||
// SUB2-AT), so accept Mage-tagged spells when the player has that subclass.
|
||||
effectiveClass := c.Class
|
||||
if c.Class == ClassRogue && c.Subclass == SubclassArcaneTrickster {
|
||||
effectiveClass = ClassMage
|
||||
}
|
||||
classOK := false
|
||||
for _, cl := range spell.Classes {
|
||||
if cl == c.Class {
|
||||
if cl == effectiveClass {
|
||||
classOK = true
|
||||
break
|
||||
}
|
||||
@@ -131,6 +136,14 @@ func (p *AdventurePlugin) handleDnDCastCmd(ctx MessageContext, args string) erro
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"%s is not on the %s spell list.", spell.Name, titleClass(c.Class)))
|
||||
}
|
||||
// AT slot ceiling — capped at L1 until L7 (then L2), tracks third-caster
|
||||
// progression. Refuse upcasting beyond what the subclass actually has.
|
||||
if c.Class == ClassRogue && c.Subclass == SubclassArcaneTrickster {
|
||||
if max := highestAvailableSlotForChar(c); spell.Level > max {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"Arcane Trickster L%d only has up to L%d slots.", c.Level, max))
|
||||
}
|
||||
}
|
||||
|
||||
// Reaction spells deferred.
|
||||
if spell.Effect == EffectReaction {
|
||||
@@ -368,7 +381,7 @@ func (p *AdventurePlugin) handleDnDSpellsCmd(ctx MessageContext, args string) er
|
||||
if err != nil || c == nil {
|
||||
return p.SendDM(ctx.Sender, "Couldn't load your Adv 2.0 sheet.")
|
||||
}
|
||||
if !classIsCaster(c.Class) {
|
||||
if !isSpellcaster(c) {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"%s isn't a caster class.", titleClass(c.Class)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user