fix(rules): point-cast cones/lines no longer demand line of effect to the aim point (SRD aiming-a-spell.md: a cone/line 'shoots away from you in the direction you designate' and 'starts from any corner of your square' - the LoE-to-the-space rule binds targets and burst origins only) - _point_target_rejection now requires in-bounds for every area and caster->point LoE only for bursts (and area-less spells, conservatively); template LoE stays enforced by cone_cells/line_cells
This commit is contained in:
@@ -130,6 +130,7 @@ from pf1e_simulator.loaders.combatant import CombatantLoadError, load_combatant
|
||||
from pf1e_simulator.models import PREDEFINED_SKILLS
|
||||
from pf1e_simulator.spells import (
|
||||
AreaEffect,
|
||||
AreaShape,
|
||||
BuffEffect,
|
||||
CastTime,
|
||||
ConditionEffect,
|
||||
@@ -4778,7 +4779,7 @@ class CombatEngine:
|
||||
)
|
||||
return None
|
||||
if target_pos is not None:
|
||||
rejection = self._point_target_rejection(state, target_pos)
|
||||
rejection = self._point_target_rejection(state, target_pos, spell_name)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
if spell_name is None:
|
||||
@@ -4789,20 +4790,59 @@ class CombatEngine:
|
||||
)
|
||||
|
||||
def _point_target_rejection(
|
||||
self, state: CombatantState, target_pos: tuple[int, int]
|
||||
self,
|
||||
state: CombatantState,
|
||||
target_pos: tuple[int, int],
|
||||
spell_name: str | None,
|
||||
) -> ActionResult | None:
|
||||
"""U7b gate: a point-targeted area must be in bounds and reachable."""
|
||||
if self._grid.in_bounds(target_pos) and has_line_of_effect_cells(
|
||||
self._grid, state.occupied_cells(), frozenset({target_pos})
|
||||
"""U7b gate: a point-targeted area must be in bounds and reachable.
|
||||
|
||||
The point must always be in bounds. Line of effect caster -> point is
|
||||
required only for a BURST (or a spell with no area, conservatively):
|
||||
the burst's point of origin is that space and SRD aiming-a-spell.md
|
||||
demands "a clear line of effect ... to any space in which you wish to
|
||||
create an effect", and a burst "can't affect creatures with total
|
||||
cover from its point of origin". A cone or line instead "shoots away
|
||||
from you ... in the direction you designate" and "starts from any
|
||||
corner of your square": the aim point only names a DIRECTION from the
|
||||
caster's own space, so no line of effect to it is required — LoE is
|
||||
enforced by the template itself (the cone drops cells it cannot reach
|
||||
from its origin, the line stops at the first barrier).
|
||||
"""
|
||||
if not self._grid.in_bounds(target_pos):
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"cast_spell rejected ({_REASON_INVALID_DESTINATION})"
|
||||
)
|
||||
return ActionResult(
|
||||
kind="cast_spell", ok=False, reason=_REASON_INVALID_DESTINATION
|
||||
)
|
||||
if self._point_cast_area_shape(spell_name) == "burst" and not (
|
||||
has_line_of_effect_cells(
|
||||
self._grid, state.occupied_cells(), frozenset({target_pos})
|
||||
)
|
||||
):
|
||||
return None
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"cast_spell rejected ({_REASON_INVALID_DESTINATION})"
|
||||
)
|
||||
return ActionResult(
|
||||
kind="cast_spell", ok=False, reason=_REASON_INVALID_DESTINATION
|
||||
)
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"cast_spell rejected ({_REASON_INVALID_DESTINATION})"
|
||||
)
|
||||
return ActionResult(
|
||||
kind="cast_spell", ok=False, reason=_REASON_INVALID_DESTINATION
|
||||
)
|
||||
return None
|
||||
|
||||
def _point_cast_area_shape(self, spell_name: str | None) -> AreaShape:
|
||||
"""Shape of the spell's top-level area: 'burst' when it has none.
|
||||
|
||||
A spell with no AreaEffect never honours ``target_pos`` downstream;
|
||||
the gate keeps its historical burst-grade strictness for those.
|
||||
"""
|
||||
spell = self._spell_registry.get(spell_name) if spell_name is not None else None
|
||||
if spell is not None:
|
||||
for effect in spell.effects:
|
||||
if isinstance(effect, AreaEffect):
|
||||
return effect.shape
|
||||
return "burst"
|
||||
|
||||
def _execute_maneuver(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user