diff --git a/README.md b/README.md index c02ad83..9ef669d 100644 --- a/README.md +++ b/README.md @@ -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 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 - 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 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), diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index 3e9213d..840da0a 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -4195,7 +4195,9 @@ class CombatEngine: 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). + 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): self._log( @@ -4223,11 +4225,20 @@ class CombatEngine: # 2d6 damage/healing 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: if channel_mode == "heal": self._channel_heal(state, affected_state, dmg) else: - self._channel_damage(state, affected_state, dmg) + self._channel_damage(state, affected_state, dmg, channel_dc) self._log( f"round {self._current_round} {state.combatant.id}: " @@ -4248,15 +4259,20 @@ class CombatEngine: ) def _channel_damage( - self, state: CombatantState, affected_state: CombatantState, dmg: int + self, state: CombatantState, affected_state: CombatantState, dmg: int, dc: int ) -> 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 - affected_state.hp -= dmg - self._stats[state.combatant.id].damage_dealt += dmg - self._stats[affected_state.combatant.id].damage_taken += dmg + affected_state.hp -= dealt + self._stats[state.combatant.id].damage_dealt += dealt + self._stats[affected_state.combatant.id].damage_taken += dealt self._log( - f"round {self._current_round} {state.combatant.id}: " - f"channel energy damages {affected_state.combatant.id} {dmg} " + f"round {self._current_round} {state.combatant.id}: channel energy " + 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})" ) if affected_state.dead: @@ -4269,7 +4285,7 @@ class CombatEngine: kind="damage_taken", actor=state, target=affected_state, - damage=dmg, + damage=dealt, ) ) diff --git a/tests/test_class_abilities.py b/tests/test_class_abilities.py index fb8d509..4136447 100644 --- a/tests/test_class_abilities.py +++ b/tests/test_class_abilities.py @@ -155,15 +155,32 @@ def test_oni_channel_harm_does_not_damage_living_enemy() -> 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) 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") engine._execute(me, action) 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: