mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
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:
@@ -819,7 +819,7 @@ func defaultKnownSpells(class DnDClass, level int) []string {
|
||||
case ClassCleric:
|
||||
out := []string{"sacred_flame", "guidance", "mending"}
|
||||
if level >= 1 {
|
||||
out = append(out, "cure_wounds", "healing_word", "bless", "guiding_bolt", "shield_of_faith")
|
||||
out = append(out, "cure_wounds", "healing_word", "bless", "guiding_bolt", "inflict_wounds", "shield_of_faith")
|
||||
}
|
||||
if maxSlot >= 2 {
|
||||
out = append(out, "spiritual_weapon", "lesser_restoration", "aid")
|
||||
@@ -878,10 +878,10 @@ func defaultKnownSpells(class DnDClass, level int) []string {
|
||||
case ClassBard:
|
||||
out := []string{"vicious_mockery", "minor_illusion", "message"}
|
||||
if level >= 1 {
|
||||
out = append(out, "cure_wounds", "healing_word", "heroism", "hideous_laughter", "faerie_fire")
|
||||
out = append(out, "cure_wounds", "healing_word", "heroism", "hideous_laughter", "faerie_fire", "thunderwave")
|
||||
}
|
||||
if maxSlot >= 2 {
|
||||
out = append(out, "hold_person", "shatter", "invisibility", "lesser_restoration")
|
||||
out = append(out, "hold_person", "shatter", "heat_metal", "invisibility", "lesser_restoration")
|
||||
}
|
||||
if maxSlot >= 3 {
|
||||
out = append(out, "hypnotic_pattern", "dispel_magic", "fear")
|
||||
|
||||
@@ -190,7 +190,7 @@ func buildSpellList() []SpellDefinition {
|
||||
Description: "A foe locks rigid mid-step. Anything you hit them with next lands devastatingly well.",
|
||||
Upcast: "+1 target per slot above 2nd"},
|
||||
{ID: "shatter", Name: "Shatter", Level: 2, School: "evocation",
|
||||
Classes: mage, Effect: EffectDamageSave, CastTime: CastAction,
|
||||
Classes: []DnDClass{ClassMage, ClassBard, ClassSorcerer}, Effect: EffectDamageSave, CastTime: CastAction,
|
||||
SaveStat: "CON", DamageDice: "3d8", DamageType: "thunder", AOE: true,
|
||||
Description: "An invisible chord rings, then snaps — everything brittle nearby cracks at once.",
|
||||
Upcast: "louder per slot above 2nd"},
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user