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:
+13
-5
@@ -8,11 +8,19 @@
|
|||||||
"cast_time": "standard",
|
"cast_time": "standard",
|
||||||
"effects": [
|
"effects": [
|
||||||
{
|
{
|
||||||
"type": "condition",
|
"type": "area",
|
||||||
"condition_name": "shaken",
|
"radius_ft": 50,
|
||||||
"duration_rounds": 10,
|
"targets": "enemies",
|
||||||
"save_negates": true
|
"save_for_half": false,
|
||||||
|
"effects": [
|
||||||
|
{
|
||||||
|
"type": "condition",
|
||||||
|
"condition_name": "shaken",
|
||||||
|
"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
|
for sub in effect.effects
|
||||||
),
|
),
|
||||||
save_for_half=effect.save_for_half,
|
save_for_half=effect.save_for_half,
|
||||||
|
targets=effect.targets,
|
||||||
)
|
)
|
||||||
return effect
|
return effect
|
||||||
|
|
||||||
|
|||||||
@@ -679,6 +679,55 @@ class TestBlessAlliesOnly:
|
|||||||
for b in c.active_buffs for m in b.modifiers)
|
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:
|
class TestOutOfRange:
|
||||||
"""Given: a target beyond the spell's maximum range
|
"""Given: a target beyond the spell's maximum range
|
||||||
When: _cast_spell is called
|
When: _cast_spell is called
|
||||||
|
|||||||
Reference in New Issue
Block a user