J3 D8-a: caster picker data fixes + Spiritual Weapon pick

Bard L1 += thunderwave, L2 += heat_metal; cleric L1 += inflict_wounds;
shatter overlay Classes broadened (defensive — mergeClassList already
unioned). New simPickSpiritualWeapon runs before simPickSpell so a
cleric with an L2 slot opens fights with the BuffSelf-tagged spell that
the regular picker (damage-effects only) was skipping; existing
spiritWeaponStrike per-round mace path lights up.

Measured L10 n=100/zone: cleric 21.0 → 23.2% (+2.2pp), bard within
noise. T3+ wall unchanged — picker upcasting + concentration-damage
modeling (D8-b/c) are the bigger levers, queued in plan §8.
This commit is contained in:
prosolis
2026-05-27 22:00:02 -07:00
parent 4576c75722
commit 2fdb280477
4 changed files with 77 additions and 5 deletions

View File

@@ -862,6 +862,15 @@ func (s *SimRunner) simPickCombatAction(uid id.UserID, sess *CombatSession) (kin
}
}
if isSpellcaster(c) && !simMartialFirstClass(c.Class) {
// Cleric: Spiritual Weapon is a BuffSelf that fires a spectral
// bonus-action attack each round via SpiritWeaponProc/Dmg mods —
// simPickSpell skips BuffSelf entries by design, so a cleric
// otherwise never spends an L2 slot on it. Force the pick once
// per fight (BuffSpiritProc==0) so the picker doesn't pretend
// it's not a damage option.
if id := simPickSpiritualWeapon(c, uid, sess); id != "" {
return "cast", id
}
if id := simPickSpell(c, uid); id != "" {
return "cast", id
}
@@ -885,6 +894,42 @@ func simMartialFirstClass(class DnDClass) bool {
return false
}
// simPickSpiritualWeapon returns "spiritual_weapon" when a cleric should
// open the fight with it: the buff is not already active on the session,
// the spell is prepared, and an L2 slot is available. Returns "" otherwise.
// The buff path in combat_cmd.go folds into BuffSpiritProc/Dmg via
// applyBuffDelta, which the turn engine fires each round via
// spiritWeaponStrike — so one cast is worth more than a single L2 damage
// spell across a multi-round fight.
func simPickSpiritualWeapon(c *DnDCharacter, uid id.UserID, sess *CombatSession) string {
if c == nil || c.Class != ClassCleric || sess == nil {
return ""
}
if sess.Statuses.BuffSpiritProc > 0 {
return ""
}
known, err := listKnownSpells(uid)
if err != nil {
return ""
}
prepared := false
for _, k := range known {
if k.SpellID == "spiritual_weapon" && k.Prepared {
prepared = true
break
}
}
if !prepared {
return ""
}
slots, _ := getSpellSlots(uid)
pair, ok := slots[2]
if !ok || pair[0]-pair[1] <= 0 {
return ""
}
return "spiritual_weapon"
}
// simPickSpell returns the spell ID a competent player would cast this
// turn, or "" when no usable spell is available (forcing a !attack).
// Selection rules: