fix(rules): allow a Will save to halve channeled energy damage
PF1e CRB (Channel Energy): creatures damaged by channeled energy get a Will save (DC 10 + half the channeler's level + her Charisma modifier) for half damage (floor). Harm mode only; heal mode unchanged. Charisma is used RAW even for casters whose spellcasting ability differs (channeling is a Charisma class feature). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -949,7 +949,9 @@ Règles modélisées :
|
|||||||
si un allié est < 50 % de ses PV → soigne tous les êtres vivants à 30 ft ; sinon, si au
|
si un allié est < 50 % de ses PV → soigne tous les êtres vivants à 30 ft ; sinon, si au
|
||||||
moins 2 morts-vivants ennemis sont groupés à 30 ft → les blesse en 2d6 ; le camp de la
|
moins 2 morts-vivants ennemis sont groupés à 30 ft → les blesse en 2d6 ; le camp de la
|
||||||
cible n'est pas pris en compte — seul `creature_type` compte, `selective_channeling` non
|
cible n'est pas pris en compte — seul `creature_type` compte, `selective_channeling` non
|
||||||
modélisé), sortilège `evil_eye` (nairda), deed `up_close_and_deadly`
|
modélisé ; les créatures blessées obtiennent un jet de Volonté DD `10 +
|
||||||
|
demi-niveau du canaliseur + modificateur de Charisme` pour demi-dégâts
|
||||||
|
— CRB Channel Energy ; le mode soin n'accorde pas de sauvegarde), sortilège `evil_eye` (nairda), deed `up_close_and_deadly`
|
||||||
(tammara, action bonus +1d6 à ≤ 30 ft — consommé sur la première attaque
|
(tammara, action bonus +1d6 à ≤ 30 ft — consommé sur la première attaque
|
||||||
résolue : si elle est à distance ET à ≤ 30 ft, +1d6 s'ajoute aux dégâts ;
|
résolue : si elle est à distance ET à ≤ 30 ft, +1d6 s'ajoute aux dégâts ;
|
||||||
sinon le bonus est perdu ; le drapeau est réinitialisé dans tous les cas),
|
sinon le bonus est perdu ; le drapeau est réinitialisé dans tous les cas),
|
||||||
|
|||||||
@@ -4195,7 +4195,9 @@ class CombatEngine:
|
|||||||
Heal mode heals all living creatures in the burst; harm mode damages
|
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
|
all undead creatures in the burst — the target's side does not matter
|
||||||
(PF1e Channel Energy: positive energy heals living creatures and
|
(PF1e Channel Energy: positive energy heals living creatures and
|
||||||
damages undead creatures within 30 feet).
|
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).
|
||||||
"""
|
"""
|
||||||
if not state.spend_resource("channel_energy", 1):
|
if not state.spend_resource("channel_energy", 1):
|
||||||
self._log(
|
self._log(
|
||||||
@@ -4223,11 +4225,20 @@ class CombatEngine:
|
|||||||
# 2d6 damage/healing
|
# 2d6 damage/healing
|
||||||
dmg = 1 + self._rng.roll_die(11) # 2d6
|
dmg = 1 + self._rng.roll_die(11) # 2d6
|
||||||
|
|
||||||
|
# PF1e (CRB, Channel Energy): creatures damaged by the channel get a
|
||||||
|
# Will save (DC 10 + half the channeler's level + her Charisma
|
||||||
|
# modifier) for half damage. Charisma is used RAW even when the
|
||||||
|
# caster's spellcasting ability is another one (channeling is a
|
||||||
|
# Charisma class feature). Heal mode grants no save.
|
||||||
|
channel_dc = 10 + state.combatant.level // 2 + (
|
||||||
|
(state.combatant.abilities.cha_score - 10) // 2
|
||||||
|
)
|
||||||
|
|
||||||
for affected_state in hit:
|
for affected_state in hit:
|
||||||
if channel_mode == "heal":
|
if channel_mode == "heal":
|
||||||
self._channel_heal(state, affected_state, dmg)
|
self._channel_heal(state, affected_state, dmg)
|
||||||
else:
|
else:
|
||||||
self._channel_damage(state, affected_state, dmg)
|
self._channel_damage(state, affected_state, dmg, channel_dc)
|
||||||
|
|
||||||
self._log(
|
self._log(
|
||||||
f"round {self._current_round} {state.combatant.id}: "
|
f"round {self._current_round} {state.combatant.id}: "
|
||||||
@@ -4248,15 +4259,20 @@ class CombatEngine:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _channel_damage(
|
def _channel_damage(
|
||||||
self, state: CombatantState, affected_state: CombatantState, dmg: int
|
self, state: CombatantState, affected_state: CombatantState, dmg: int, dc: int
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Apply channel energy damage: Will save (DC from the channeler) for half."""
|
||||||
|
save = self.resolve_save(affected_state, "will", dc)
|
||||||
|
dealt = dmg // 2 if save.success else dmg
|
||||||
hp_before = affected_state.hp
|
hp_before = affected_state.hp
|
||||||
affected_state.hp -= dmg
|
affected_state.hp -= dealt
|
||||||
self._stats[state.combatant.id].damage_dealt += dmg
|
self._stats[state.combatant.id].damage_dealt += dealt
|
||||||
self._stats[affected_state.combatant.id].damage_taken += dmg
|
self._stats[affected_state.combatant.id].damage_taken += dealt
|
||||||
self._log(
|
self._log(
|
||||||
f"round {self._current_round} {state.combatant.id}: "
|
f"round {self._current_round} {state.combatant.id}: channel energy "
|
||||||
f"channel energy damages {affected_state.combatant.id} {dmg} "
|
f"damages {affected_state.combatant.id} {dealt} "
|
||||||
|
f"(Will DC{dc} d20={save.roll}+{save.total - save.roll}={save.total} "
|
||||||
|
f"{'SAVED half' if save.success else 'FAILED full'}) "
|
||||||
f"({hp_before}->{affected_state.hp})"
|
f"({hp_before}->{affected_state.hp})"
|
||||||
)
|
)
|
||||||
if affected_state.dead:
|
if affected_state.dead:
|
||||||
@@ -4269,7 +4285,7 @@ class CombatEngine:
|
|||||||
kind="damage_taken",
|
kind="damage_taken",
|
||||||
actor=state,
|
actor=state,
|
||||||
target=affected_state,
|
target=affected_state,
|
||||||
damage=dmg,
|
damage=dealt,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -155,15 +155,32 @@ def test_oni_channel_harm_does_not_damage_living_enemy() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_oni_channel_harm_damages_undead() -> None:
|
def test_oni_channel_harm_damages_undead() -> None:
|
||||||
"""Harm mode damages undead creatures in the burst regardless of side."""
|
"""Harm mode damages undead creatures in the burst regardless of side;
|
||||||
|
a failed Will save takes full damage (CRB Channel Energy)."""
|
||||||
oni = _combatant("oni", resources={"channel_energy": 6}, hp_max=30)
|
oni = _combatant("oni", resources={"channel_energy": 6}, hp_max=30)
|
||||||
lich = _combatant("lich", hp_max=100, creature_type="undead")
|
lich = _combatant("lich", hp_max=100, creature_type="undead")
|
||||||
engine, me, enemy = _engine(oni, [lich], rng_queue=[11])
|
engine, me, enemy = _engine(oni, [lich], rng_queue=[11, 5])
|
||||||
|
|
||||||
action = Action(kind="channel_energy", target_id=lich.id, channel_mode="harm")
|
action = Action(kind="channel_energy", target_id=lich.id, channel_mode="harm")
|
||||||
engine._execute(me, action)
|
engine._execute(me, action)
|
||||||
|
|
||||||
assert enemy[0].hp == 100 - (1 + 11) # 2d6 = 1 + roll_die(11)
|
assert enemy[0].hp == 100 - (1 + 11) # 2d6 = 1 + roll_die(11)
|
||||||
|
# Will save fails: d20 5 + will 4 = 9 < DC 14 (10 + lvl//2 2 + cha 2)
|
||||||
|
assert any("Will DC14" in line and "FAILED full" in line for line in engine._transcript)
|
||||||
|
|
||||||
|
|
||||||
|
def test_oni_channel_harm_will_save_halves_damage() -> None:
|
||||||
|
"""CRB Channel Energy: an undead that passes the Will save takes half
|
||||||
|
damage (floor)."""
|
||||||
|
oni = _combatant("oni", resources={"channel_energy": 6}, hp_max=30)
|
||||||
|
lich = _combatant("lich", hp_max=100, creature_type="undead")
|
||||||
|
engine, me, enemy = _engine(oni, [lich], rng_queue=[11, 20])
|
||||||
|
|
||||||
|
action = Action(kind="channel_energy", target_id=lich.id, channel_mode="harm")
|
||||||
|
engine._execute(me, action)
|
||||||
|
|
||||||
|
assert enemy[0].hp == 100 - (1 + 11) // 2 # 2d6 = 12, saved for half = 6
|
||||||
|
assert any("Will DC14" in line and "SAVED half" in line for line in engine._transcript)
|
||||||
|
|
||||||
|
|
||||||
def test_oni_channel_heal_heals_living_enemies_in_range() -> None:
|
def test_oni_channel_heal_heals_living_enemies_in_range() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user