fix(rules): convert bane to a 50-ft enemies-only burst
SRD spells/bane.md: enemies within a 50-ft burst centered on the caster take a -1 penalty on attack rolls and a -1 penalty on saving throws against fear effects; Will negates. The -1 penalty is approximated with the shaken condition (-2), matching the existing Doom approximation. - bane.json: single-target condition -> AreaEffect burst radius_ft 50, targets "enemies", ConditionEffect shaken 10 rounds save_negates (range/save kept as medium/will for the registry pin) - combat.py: _metamagic_transform_sub now copies AreaEffect.targets so an Extend Spell on an area spell preserves the allies/enemies filter - tests: TestBaneEnemiesOnly (enemy in burst debuffed, allies/caster unaffected, successful Will save negates)
This commit is contained in:
@@ -6,6 +6,12 @@
|
||||
"save": "will",
|
||||
"base_dc": 10,
|
||||
"cast_time": "standard",
|
||||
"effects": [
|
||||
{
|
||||
"type": "area",
|
||||
"radius_ft": 50,
|
||||
"targets": "enemies",
|
||||
"save_for_half": false,
|
||||
"effects": [
|
||||
{
|
||||
"type": "condition",
|
||||
@@ -13,6 +19,8 @@
|
||||
"duration_rounds": 10,
|
||||
"save_negates": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": "Enemies within range take a -1 penalty on attack rolls and saving throws (approximated with the shaken condition, -2). Will negates. Concentration 1 min/level approximated as 10 rounds."
|
||||
"description": "SRD spells/bane.md: enemies within a 50-ft burst centered on the caster take a -1 penalty on attack rolls and a -1 penalty on saving throws against fear effects; Will negates. The -1 penalty is approximated with the shaken condition (-2 on attacks and saves), matching the existing Doom approximation. Duration 1 min/level approximated as 10 rounds."
|
||||
}
|
||||
@@ -6613,6 +6613,7 @@ class CombatEngine:
|
||||
for sub in effect.effects
|
||||
),
|
||||
save_for_half=effect.save_for_half,
|
||||
targets=effect.targets,
|
||||
)
|
||||
return effect
|
||||
|
||||
|
||||
@@ -679,6 +679,55 @@ class TestBlessAlliesOnly:
|
||||
for b in c.active_buffs for m in b.modifiers)
|
||||
|
||||
|
||||
class TestBaneEnemiesOnly:
|
||||
"""Given: a caster casting Bane (SRD: 50-ft burst, enemies, Will negates)
|
||||
When: _cast_spell is called
|
||||
Then: enemies in the burst are debuffed, allies and caster are not."""
|
||||
|
||||
def _bane_engine(
|
||||
self,
|
||||
targets: list[Combatant],
|
||||
enemy: CombatantState,
|
||||
rng_queue: list[int],
|
||||
) -> CombatEngine:
|
||||
caster = _make_caster()
|
||||
c = CombatantState(combatant=caster, side="players", pos=(0, 0), hp=caster.hp_max)
|
||||
states = [c, enemy, *targets]
|
||||
legend = {".": TerrainType(type="floor", move_cost=1)}
|
||||
spec = MapSpec(name="test", terrain=tuple(["." * 12] * 12), legend=legend)
|
||||
grid = Grid.from_spec(spec)
|
||||
return CombatEngine(
|
||||
ScriptedRng(rng_queue), grid, states,
|
||||
spell_registry=load_spell_registry(_SPELLS_DIR),
|
||||
)
|
||||
|
||||
def test_enemy_debuffed_allies_unaffected(self) -> None:
|
||||
ally = CombatantState(
|
||||
combatant=_make_target("ally", hp_max=20), side="players",
|
||||
pos=(1, 0), hp=20,
|
||||
)
|
||||
enemy = CombatantState(
|
||||
combatant=_make_target("enemy", hp_max=20), side="monsters",
|
||||
pos=(2, 0), hp=20,
|
||||
)
|
||||
engine = self._bane_engine([ally], enemy, [2]) # d20=2 → save fails
|
||||
engine._cast_spell(
|
||||
engine._states[0], enemy, "Bane",
|
||||
)
|
||||
assert any(c.condition.name == "shaken" for c in enemy.active_conditions)
|
||||
assert not ally.active_conditions
|
||||
assert not engine._states[0].active_conditions
|
||||
|
||||
def test_successful_save_negates(self) -> None:
|
||||
enemy = CombatantState(
|
||||
combatant=_make_target("enemy", hp_max=20), side="monsters",
|
||||
pos=(2, 0), hp=20,
|
||||
)
|
||||
engine = self._bane_engine([], enemy, [20]) # d20=20 → save succeeds
|
||||
engine._cast_spell(engine._states[0], enemy, "Bane")
|
||||
assert not enemy.active_conditions
|
||||
|
||||
|
||||
class TestOutOfRange:
|
||||
"""Given: a target beyond the spell's maximum range
|
||||
When: _cast_spell is called
|
||||
|
||||
Reference in New Issue
Block a user