fix(combat): channel energy targets living vs undead
This commit is contained in:
@@ -295,6 +295,7 @@ class Action:
|
||||
hex_name: str | None = None # "evil_eye", "slumber", "cackle"
|
||||
deed_name: str | None = None # "up_close_and_deadly", "quick_clear"
|
||||
class_ability: str | None = None # "deed"|"spell_combat"|"inspire_courage"|"hex"|"channel"
|
||||
channel_mode: Literal["heal", "harm"] | None = None # channel energy polarity for this use
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -1156,7 +1157,9 @@ class CombatEngine:
|
||||
"""Dispatch a class-ability action by kind, reading its carrier field."""
|
||||
kind = action.kind
|
||||
if kind == "channel_energy":
|
||||
self._execute_channel_energy(state, target)
|
||||
self._execute_channel_energy(
|
||||
state, target, action.channel_mode or "heal"
|
||||
)
|
||||
elif kind == "hex":
|
||||
self._execute_hex(state, target, action.hex_name)
|
||||
elif kind == "deed":
|
||||
@@ -1393,9 +1396,18 @@ class CombatEngine:
|
||||
)
|
||||
|
||||
def _execute_channel_energy(
|
||||
self, state: CombatantState, target: CombatantState | None
|
||||
self,
|
||||
state: CombatantState,
|
||||
target: CombatantState | None,
|
||||
channel_mode: Literal["heal", "harm"],
|
||||
) -> None:
|
||||
"""Execute channel energy (burst 30ft, 2d6 heal/damage, spends channel energy)."""
|
||||
"""Execute channel energy (positive energy, burst 30ft, 2d6).
|
||||
|
||||
Heal mode heals all living creatures in the burst; harm mode damages
|
||||
all undead creatures in the burst — the target's side does not matter
|
||||
(PF1e Channel Energy: positive energy heals living creatures and
|
||||
damages undead creatures within 30 feet).
|
||||
"""
|
||||
if not state.spend_resource("channel_energy", 1):
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
@@ -1407,8 +1419,7 @@ class CombatEngine:
|
||||
|
||||
# Channel energy affects all creatures within 30ft (6 cells)
|
||||
radius_cells = 30 // _SQUARE_FT # 6 cells
|
||||
center = state.pos
|
||||
affected = self._creatures_in_range(center, radius_cells)
|
||||
affected = self._creatures_in_range(state.pos, radius_cells)
|
||||
|
||||
if not affected:
|
||||
self._log(
|
||||
@@ -1417,22 +1428,21 @@ class CombatEngine:
|
||||
)
|
||||
return
|
||||
|
||||
# Determine if healing or harming based on target relationship
|
||||
is_harm = target.side != state.side
|
||||
wanted_type = "living" if channel_mode == "heal" else "undead"
|
||||
hit = [s for s in affected if s.combatant.creature_type == wanted_type]
|
||||
|
||||
# 2d6 damage/healing
|
||||
dmg = 1 + self._rng.roll_die(11) # 2d6
|
||||
|
||||
for affected_state in affected:
|
||||
is_ally = affected_state.side == state.side
|
||||
if is_ally and not is_harm:
|
||||
for affected_state in hit:
|
||||
if channel_mode == "heal":
|
||||
self._channel_heal(state, affected_state, dmg)
|
||||
elif not is_ally and is_harm:
|
||||
else:
|
||||
self._channel_damage(state, affected_state, dmg)
|
||||
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"channel energy ({'harm' if is_harm else 'heal'}) {dmg} -> {len(affected)} targets"
|
||||
f"channel energy ({channel_mode}) {dmg} -> {len(hit)} targets"
|
||||
)
|
||||
|
||||
def _channel_heal(
|
||||
@@ -2524,8 +2534,15 @@ def default_policy(engine: CombatEngine, state: CombatantState) -> tuple[Action,
|
||||
return (Action(kind="withdraw", target_id=target.combatant.id),)
|
||||
|
||||
# Channel energy (oni): standard action that heal/harms in a 30ft burst.
|
||||
if _emit_channel_energy(engine, state):
|
||||
return (Action(kind="channel_energy", target_id=target.combatant.id),)
|
||||
channel_mode = _emit_channel_energy(engine, state)
|
||||
if channel_mode is not None:
|
||||
return (
|
||||
Action(
|
||||
kind="channel_energy",
|
||||
target_id=target.combatant.id,
|
||||
channel_mode=channel_mode,
|
||||
),
|
||||
)
|
||||
|
||||
# Hex (nairda): standard action when the target is within close range.
|
||||
if _emit_hex(engine, state, target):
|
||||
@@ -2558,21 +2575,24 @@ _GROUPED_ENEMY_COUNT = 2
|
||||
_HEX_RANGE_FT = 25
|
||||
|
||||
|
||||
def _emit_channel_energy(engine: CombatEngine, state: CombatantState) -> bool:
|
||||
"""True when oni should channel energy: resource available and a trigger is met."""
|
||||
def _emit_channel_energy(
|
||||
engine: CombatEngine, state: CombatantState
|
||||
) -> Literal["heal", "harm"] | None:
|
||||
"""Channel mode when oni should channel (heal living / harm undead)."""
|
||||
if state.resources_left.get("channel_energy", 0) <= 0:
|
||||
return False
|
||||
return None
|
||||
if _find_ally_needing_heal(engine, state) is not None:
|
||||
return True
|
||||
return "heal"
|
||||
radius_cells = 30 // _SQUARE_FT
|
||||
grouped_enemies = [
|
||||
grouped_undead_enemies = [
|
||||
s
|
||||
for s in engine.states
|
||||
if s.side != state.side
|
||||
and s.active
|
||||
and s.combatant.creature_type == "undead"
|
||||
and engine.grid.distance(state.pos, s.pos) <= radius_cells
|
||||
]
|
||||
return len(grouped_enemies) >= _GROUPED_ENEMY_COUNT
|
||||
return "harm" if len(grouped_undead_enemies) >= _GROUPED_ENEMY_COUNT else None
|
||||
|
||||
|
||||
def _emit_hex(
|
||||
|
||||
@@ -267,6 +267,7 @@ class Combatant(_FrozenModel):
|
||||
name: str
|
||||
level: int = Field(ge=1)
|
||||
size: str = "Medium"
|
||||
creature_type: Literal["living", "undead", "construct"] = "living"
|
||||
abilities: AbilityScores
|
||||
hp_max: int = Field(ge=1)
|
||||
ac: ACProfile
|
||||
|
||||
Reference in New Issue
Block a user