Combat engine §6: the rooms get the small change, the boss keeps the big slots

The first cut let a caster cast in auto-resolved rooms and capped nothing but
upcasting. The sweep said that was half a fix. Room deaths fell for every caster
— mage 131 -> 105, sorcerer 118 -> 105 — and fleeing collapsed, which is what §6
predicted. But elite and boss deaths exploded behind it: mage 7 -> 38 and 19 ->
61, sorcerer 14 -> 51 and 15 -> 65. They were winning the trash rooms and
arriving at the thing that matters with an empty pool. Net, mage and sorcerer
came out of §6 worse than they went in (-34 and -37 runs cleared).

"Never upcast" is not a reserve. A mage's native-level spells ARE its boss kit,
so casting them at native level in a goblin room still spends the dragon's
slots. Only a level cap reserves anything.

So an ordinary room may reach for a 2nd-level slot and no higher. Every caster
still owns a real spell down there — a cleric's inflict_wounds, everyone else's
scorching_ray — but fireball, spirit_guardians, flame_strike and every slot the
turn engine would upcast into stay in the caster's pocket until there is
something worth spending them on.

Claude-Session: https://claude.ai/code/session_01B2MwktU4RgfWkar8HM3zZn
This commit is contained in:
prosolis
2026-07-12 10:01:56 -07:00
parent bae83271fe
commit 064ecb1848
2 changed files with 102 additions and 10 deletions

View File

@@ -66,9 +66,27 @@ func (p *AdventurePlugin) autoCastForAutoResolve(
return true
}
// roomSlotCap is the highest slot level an ordinary room may spend. Cantrips are
// free and always available; anything above this is reserved for the elite and
// the boss.
//
// The first cut of §6 had no cap — "never upcast" was supposed to be enough. It
// was not, and the sweep said so: room deaths fell for every caster (mage 131 →
// 105) while elite and boss deaths exploded (mage 7 → 38 and 19 → 61, sorcerer
// 14 → 51 and 15 → 65). Casters were winning the trash rooms and arriving at the
// thing that matters with an empty pool. Native-level is not a reserve, because
// a mage's native-level spells ARE its boss kit; only a level cap is.
//
// 2 is where the two demands stop overlapping. Every caster owns a damage spell
// at or below it (a cleric's guiding_bolt and inflict_wounds are both 1st), so
// the rooms still get a real spell — but a fireball, a spirit_guardians, a
// flame_strike, and every slot the turn engine would upcast into stays in the
// caster's pocket until there is something worth spending it on.
const roomSlotCap = 2
// pickAutoResolveSpell is the best damage spell this caster can actually cast
// right now: known, prepared, on their list, and backed by a slot they still
// hold.
// right now: known, prepared, on their list, at or under the room slot cap, and
// backed by a slot they still hold.
//
// It casts at the spell's NATIVE level and never upcasts. simPickSpell upcasts
// aggressively — correct there, because it runs in the turn engine at an elite
@@ -115,8 +133,12 @@ func pickAutoResolveSpell(uid id.UserID, c *DnDCharacter) (SpellDefinition, int,
if !onList {
continue
}
// A cantrip is free; a leveled spell needs a slot still in hand.
// A cantrip is free; a leveled spell needs a slot still in hand, and the
// room only gets to reach for the small ones.
if sp.Level > 0 {
if sp.Level > roomSlotCap {
continue
}
if pair, ok := slots[sp.Level]; !ok || pair[0]-pair[1] <= 0 {
continue
}