fix(combat): witch hex trigger uses the SRD close range scaled by caster level (25 ft + 5 ft per 2 levels, extracted as spells.close_range_ft and shared with spell_range_ft) instead of the fixed _HEX_RANGE_FT 25 - a level-5 witch reaches 35 ft; + scaling regression test
This commit is contained in:
@@ -141,6 +141,7 @@ from pf1e_simulator.spells import (
|
||||
SaveType,
|
||||
SpellEffect,
|
||||
SummonEffect,
|
||||
close_range_ft,
|
||||
save_descriptors_for_effects,
|
||||
spell_range_ft,
|
||||
spell_save_descriptors,
|
||||
@@ -8884,7 +8885,6 @@ def _normal_actions(
|
||||
|
||||
|
||||
_GROUPED_ENEMY_COUNT = 2
|
||||
_HEX_RANGE_FT = 25
|
||||
|
||||
|
||||
def _emit_channel_energy(
|
||||
@@ -8910,12 +8910,17 @@ def _emit_channel_energy(
|
||||
def _emit_hex(
|
||||
engine: CombatEngine, state: CombatantState, target: CombatantState | None
|
||||
) -> bool:
|
||||
"""True when nairda should hex: resource available and target within close range."""
|
||||
"""True when nairda should hex: resource available and target within close range.
|
||||
|
||||
APG hexes carry a close range, which scales with the caster level
|
||||
(SRD, Magic - Range: "Close - 25 ft. + 5 ft. per 2 caster levels").
|
||||
"""
|
||||
if state.resources_left.get("hexes", 0) <= 0:
|
||||
return False
|
||||
if target is None or not target.active:
|
||||
return False
|
||||
return engine.creature_distance(state, target) * _SQUARE_FT <= _HEX_RANGE_FT
|
||||
dist_ft = engine.creature_distance(state, target) * _SQUARE_FT
|
||||
return dist_ft <= close_range_ft(state.combatant.level)
|
||||
|
||||
|
||||
def _spell_combat_actions(
|
||||
|
||||
@@ -368,13 +368,19 @@ class SpellSpec:
|
||||
description: str = ""
|
||||
|
||||
|
||||
def close_range_ft(caster_level: int) -> int:
|
||||
"""PF1e (SRD, Magic — Range): "Close — 25 ft. + 5 ft. per 2 caster levels".
|
||||
Shared by spell ranges and class features with a close range (witch hexes)."""
|
||||
return 25 + 5 * (caster_level // 2)
|
||||
|
||||
|
||||
def spell_range_ft(spell: SpellSpec, caster_level: int) -> int:
|
||||
if spell.range == "personal":
|
||||
return 0
|
||||
if spell.range == "touch":
|
||||
return 5
|
||||
if spell.range == "close":
|
||||
return 25 + 5 * (caster_level // 2)
|
||||
return close_range_ft(caster_level)
|
||||
if spell.range == "medium":
|
||||
return 100 + 10 * caster_level
|
||||
return 400 + 40 * caster_level
|
||||
|
||||
@@ -319,6 +319,22 @@ def test_nairda_emits_hex_when_available() -> None:
|
||||
assert any(a.kind == "hex" for a in actions)
|
||||
|
||||
|
||||
def test_nairda_hex_close_range_scales_with_caster_level() -> None:
|
||||
"""SRD (Magic - Range): close = 25 ft + 5 ft per 2 caster levels -> 35 ft
|
||||
at level 5; the fixed 25 ft cut off a 30 ft target. APG hexes carry a
|
||||
close range, so a goblin at 30 ft (6 squares, 4 diagonals) gets hexed
|
||||
and one at 40 ft (8 squares) does not."""
|
||||
engine, me, _ = _engine(_combatant("nairda", resources={"hexes": 5}), [])
|
||||
near = _state(_combatant("goblin", hp_max=6), "monsters", (1, 1), hp=6)
|
||||
engine.states.append(near)
|
||||
assert any(a.kind == "hex" for a in default_policy(engine, me))
|
||||
|
||||
engine2, me2, _ = _engine(_combatant("nairda-b", resources={"hexes": 5}), [])
|
||||
far = _state(_combatant("goblin-b", hp_max=6), "monsters", (11, 9), hp=6)
|
||||
engine2.states.append(far)
|
||||
assert not any(a.kind == "hex" for a in default_policy(engine2, me2))
|
||||
|
||||
|
||||
def test_nairda_silent_when_hexes_exhausted() -> None:
|
||||
nairda = _combatant("nairda", resources={"hexes": 0})
|
||||
goblin = _combatant("goblin", hp_max=6)
|
||||
|
||||
Reference in New Issue
Block a user