fix(rules): selective channeling modeled as a policy-chosen exclusion - Action(kind=channel_energy) gains channel_exclude, honored only with the Selective Channeling feat and capped at the Charisma modifier (SRD feats/selective-channeling.md: 'choose a number of targets in the area up to your Charisma modifier... not affected by your channeled energy'); without the feat the list is ignored and logged (SRD Normal: all targets affected); observable in current data because heal mode also heals living ENEMIES in the burst (deny them the heal) and harm mode can spare an undead ally; default_policy never excludes (opt-in, like weapon_id/destination/metamagic); golden transcripts unchanged; +5 tests

This commit is contained in:
2026-09-19 22:49:40 +02:00
parent 9f0d86273a
commit 5fe2b72339
3 changed files with 209 additions and 8 deletions
+55 -2
View File
@@ -191,6 +191,9 @@ _FIRING_MELEE_PENALTY = -4 # PF1e: ranged attack into melee
_FIRING_MELEE_SAFE_FT = 10 # PF1e: 2 cells (10 ft) = the -4 penalty is avoided
_SNEAK_ATTACK_FEATURE = "Sneak Attack" # PF1e: rogue class feature (precision dice)
_PRECISE_SHOT_FEATURE = "Precise Shot" # PF1e: feat negating firing-into-melee penalty
# PF1e: feat letting a channeler exclude up to her Cha modifier targets from
# her own channel energy (SRD feats/selective-channeling.md).
_SELECTIVE_CHANNELING_FEAT = "Selective Channeling"
_IMPROVED_CRITICAL_FEATURE = "Improved Critical" # PF1e: feat doubling the threat range
_CRIT_RANGE_BASE = 21 # threat range top is 21 - min natural die (20 -> 20-20)
_DYING_DC = 10 # PF1e: DC of the per-round stabilization Constitution check
@@ -1114,6 +1117,12 @@ class Action:
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
# PF1e (SRD Feats > Selective Channeling): "When you channel energy, you
# can choose a number of targets in the area up to your Charisma
# modifier. These targets are not affected by your channeled energy."
# Ids the channeler's policy excludes from THIS channel; honored only
# with the feat, capped at the Charisma modifier (excess entries drop).
channel_exclude: tuple[str, ...] = ()
direction: tuple[int, int] | None = None # steering for directional maneuvers
trick_condition: TrickCondition | None = None # dirty trick condition choice
# weapon id for attack / full_attack / retrieve_weapon / repair_weapon
@@ -4698,7 +4707,8 @@ class CombatEngine:
kind = action.kind
if kind == "channel_energy":
self._execute_channel_energy(
state, target, action.channel_mode or "heal"
state, target, action.channel_mode or "heal",
exclude=action.channel_exclude,
)
elif kind == "hex":
self._execute_hex(state, target, action.hex_name)
@@ -5752,6 +5762,8 @@ class CombatEngine:
state: CombatantState,
target: CombatantState | None,
channel_mode: Literal["heal", "harm"],
*,
exclude: tuple[str, ...] = (),
) -> None:
"""Execute channel energy (positive energy, burst 30ft, 2d6).
@@ -5760,7 +5772,8 @@ class CombatEngine:
(PF1e Channel Energy: positive energy heals living creatures and
damages undead creatures within 30 feet). Damaged creatures get a
Will save (DC 10 + half the channeler's level + her Cha modifier)
for half damage (CRB Channel Energy).
for half damage (CRB Channel Energy). ``exclude`` is the Selective
Channeling payload (see ``_selective_channel``).
"""
if not state.spend_resource("channel_energy", 1):
self._log(
@@ -5784,6 +5797,7 @@ class CombatEngine:
wanted_type = "living" if channel_mode == "heal" else "undead"
hit = [s for s in affected if s.combatant.creature_type == wanted_type]
hit = self._selective_channel(state, hit, exclude)
# 2d6 damage/healing
dmg = 1 + self._rng.roll_die(11) # 2d6
@@ -5808,6 +5822,45 @@ class CombatEngine:
f"channel energy ({channel_mode}) {dmg} -> {len(hit)} targets"
)
def _selective_channel(
self,
state: CombatantState,
hit: list[CombatantState],
exclude: tuple[str, ...],
) -> list[CombatantState]:
"""Apply the Selective Channeling feat to a resolved channel target list.
PF1e (SRD Feats > Selective Channeling, feats/selective-channeling.md):
"When you channel energy, you can choose a number of targets in the
area up to your Charisma modifier. These targets are not affected by
your channeled energy. Normal: All targets in a 30-foot burst are
affected when you channel energy. You can only choose whether or not
you are affected." Without the feat the exclusion list is ignored
(the engine's default is "all targets affected, channeler included");
with it, the first ``Cha modifier`` ids are dropped from the affected
set (deterministic truncation of an over-long list). The policy
chooses the ids: default_policy never excludes (available via a
custom policy, like the other opt-in payloads).
"""
if not exclude:
return hit
if not self._has_feat(state, _SELECTIVE_CHANNELING_FEAT):
self._log(
f"round {self._current_round} {state.combatant.id}: "
f"selective channeling ignored (no feat)"
)
return hit
cha_mod = (state.combatant.abilities.cha_score - 10) // 2
allowed = set(exclude[:max(0, cha_mod)])
kept = [s for s in hit if s.combatant.id not in allowed]
dropped = [s.combatant.id for s in hit if s.combatant.id in allowed]
if dropped:
self._log(
f"round {self._current_round} {state.combatant.id}: "
f"selective channeling excludes {', '.join(dropped)}"
)
return kept
def _channel_heal(
self, state: CombatantState, affected_state: CombatantState, dmg: int
) -> None: