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,
|
SaveType,
|
||||||
SpellEffect,
|
SpellEffect,
|
||||||
SummonEffect,
|
SummonEffect,
|
||||||
|
close_range_ft,
|
||||||
save_descriptors_for_effects,
|
save_descriptors_for_effects,
|
||||||
spell_range_ft,
|
spell_range_ft,
|
||||||
spell_save_descriptors,
|
spell_save_descriptors,
|
||||||
@@ -8884,7 +8885,6 @@ def _normal_actions(
|
|||||||
|
|
||||||
|
|
||||||
_GROUPED_ENEMY_COUNT = 2
|
_GROUPED_ENEMY_COUNT = 2
|
||||||
_HEX_RANGE_FT = 25
|
|
||||||
|
|
||||||
|
|
||||||
def _emit_channel_energy(
|
def _emit_channel_energy(
|
||||||
@@ -8910,12 +8910,17 @@ def _emit_channel_energy(
|
|||||||
def _emit_hex(
|
def _emit_hex(
|
||||||
engine: CombatEngine, state: CombatantState, target: CombatantState | None
|
engine: CombatEngine, state: CombatantState, target: CombatantState | None
|
||||||
) -> bool:
|
) -> 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:
|
if state.resources_left.get("hexes", 0) <= 0:
|
||||||
return False
|
return False
|
||||||
if target is None or not target.active:
|
if target is None or not target.active:
|
||||||
return False
|
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(
|
def _spell_combat_actions(
|
||||||
|
|||||||
@@ -368,13 +368,19 @@ class SpellSpec:
|
|||||||
description: str = ""
|
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:
|
def spell_range_ft(spell: SpellSpec, caster_level: int) -> int:
|
||||||
if spell.range == "personal":
|
if spell.range == "personal":
|
||||||
return 0
|
return 0
|
||||||
if spell.range == "touch":
|
if spell.range == "touch":
|
||||||
return 5
|
return 5
|
||||||
if spell.range == "close":
|
if spell.range == "close":
|
||||||
return 25 + 5 * (caster_level // 2)
|
return close_range_ft(caster_level)
|
||||||
if spell.range == "medium":
|
if spell.range == "medium":
|
||||||
return 100 + 10 * caster_level
|
return 100 + 10 * caster_level
|
||||||
return 400 + 40 * 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)
|
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:
|
def test_nairda_silent_when_hexes_exhausted() -> None:
|
||||||
nairda = _combatant("nairda", resources={"hexes": 0})
|
nairda = _combatant("nairda", resources={"hexes": 0})
|
||||||
goblin = _combatant("goblin", hp_max=6)
|
goblin = _combatant("goblin", hp_max=6)
|
||||||
|
|||||||
Reference in New Issue
Block a user