From 1862c22b7b6d98db11629e4a2810164078564955 Mon Sep 17 00:00:00 2001 From: ctan Date: Fri, 11 Sep 2026 23:11:57 +0200 Subject: [PATCH] fix(rules): apply maneuver AoO damage as a penalty to the maneuver PF1e (CRB Combat Maneuvers): "If you are hit by the target, you take the damage normally and apply that amount as a penalty to the attack roll to perform the maneuver." Capture the provoked AoO's damage in _execute_maneuver and subtract it from the check total (floored at 0; natural 20 still succeeds, natural 1 still fails; dropping below 0 hp still forfeits). Escape and the Improved-feat exemption are untouched. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- README.md | 8 ++++++-- src/pf1e_simulator/combat.py | 20 +++++++++++++++++--- tests/test_conformance_p2.py | 24 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 11c8324..851d094 100644 --- a/README.md +++ b/README.md @@ -946,8 +946,12 @@ Règles modélisées : (conformance P2). Conformance P2 : tenter une manœuvre (sauf `escape`) **provoque une attaque d'opportunité** de la cible, résolue **avant** le jet de CMB ; le don « Improved » correspondant (Improved Trip, …) - annule cette provocation ; si l'AoO fait passer l'attaquant sous 0 pv, la - manœuvre est perdue. L'AoO de manœuvre consomme le budget d'AoO du round de + annule cette provocation ; les dégâts infligés par cette AoO sont appliqués + comme **pénalité au jet de manœuvre** (conformance A2 — CRB « Combat + Maneuvers » : « apply that amount as a penalty to the attack roll to perform + the maneuver », total planché à 0, 20 naturel réussit / 1 naturel échoue + toujours) ; si l'AoO fait passer l'attaquant sous 0 pv, la manœuvre est + perdue. L'AoO de manœuvre consomme le budget d'AoO du round de la cible (Combat Reflexes inclus). Conformance B16 (CRB « Power Attack ») : la pénalité −X de Power Attack actif s'applique **au jet de CMB** de la manœuvre (X = BAB//4 + 1) — la pénalité couvre « les jets d'attaque de mêlée diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index fb23dcb..fccff81 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -3839,10 +3839,16 @@ class CombatEngine: # PF1e (CRB, Combat Maneuver): attempting the maneuver provokes an # attack of opportunity from the target — resolved BEFORE the CMB - # check — unless the attacker holds the Improved feat for it. Being - # knocked below 0 hp by that AoO forfeits the maneuver. + # check — unless the attacker holds the Improved feat for it. Damage + # taken from that AoO is applied as a penalty to the maneuver check + # (CRB: "apply that amount as a penalty to the attack roll to perform + # the maneuver"); being knocked below 0 hp by it forfeits the + # maneuver. + aoo_damage = 0 if not self._has_feat(state, _IMPROVED_MANEUVER_FEATS[maneuver]): + hp_before = state.hp self._maneuver_provoked_aoo(state, target) + aoo_damage = max(0, hp_before - state.hp) if not state.active: return @@ -3855,7 +3861,15 @@ class CombatEngine: attacker_cmb = self._maneuver_cmb(state) - pa_amt target_cmd = self._maneuver_cmd(target) roll = self._rng.d20() - total = roll + attacker_cmb + # PF1e (CRB Combat Maneuvers): the provoked-AoO damage rides the + # check total (floored at 0); natural 20/1 keep their rules below. + total = max(0, roll + attacker_cmb - aoo_damage) + if aoo_damage: + self._log( + f"round {self._current_round} {state.combatant.id}: " + f"maneuver AoO dealt {aoo_damage} damage " + f"(-{aoo_damage} on the check)" + ) m = _ManeuverRoll( roll=roll, attacker_cmb=attacker_cmb, total=total, target_cmd=target_cmd, diff --git a/tests/test_conformance_p2.py b/tests/test_conformance_p2.py index 7294dac..3e397bb 100644 --- a/tests/test_conformance_p2.py +++ b/tests/test_conformance_p2.py @@ -441,6 +441,30 @@ def test_maneuver_aoo_can_drop_attacker_and_forfeit_trip() -> None: ) +def test_maneuver_aoo_damage_penalizes_the_check() -> None: + """A provoked AoO that deals N damage turns a would-be hit into a miss. + + PF1e (CRB Combat Maneuvers): "If you are hit by the target, you take the + damage normally and apply that amount as a penalty to the attack roll to + perform the maneuver." b's AoO deals 6; the trip roll 15 + CMB 1 = 16 + would beat CMD 11 by 5 — under the -6 penalty the total is 10 and the + trip fails. + """ + a = _combatant("a", bab=1) + b = _combatant("b", bab=1, attacks=(_sword("b", bonus=5),)) + # b's AoO: d20 15 + 5 = 20 vs AC 10 -> HIT 6 damage (a survives at 94). + eng = _engine([a, b], [(1, 1), (1, 2)], ["players", "monsters"], [15, 6, 15]) + eng._execute_maneuver(eng._states[0], eng._states[1], "trip") + assert eng._states[0].hp == 94 + assert eng._states[0].active is True + assert not any( + ac.condition.name == "prone" for ac in eng._states[1].active_conditions + ) + assert any("maneuver AoO dealt 6 damage" in line for line in eng._transcript) + trip_line = next(line for line in eng._transcript if "trip vs b" in line) + assert "d20=15+1=10 CMD 11 -> FAIL" in trip_line + + def test_improved_trip_skips_the_provoking_aoo() -> None: a = _combatant("a", features=("Improved Trip",)) b = _combatant("b")