diff --git a/README.md b/README.md index 692ce4d..7f66fe8 100644 --- a/README.md +++ b/README.md @@ -1022,7 +1022,10 @@ Règles modélisées : les bonus de CA des huit types autorisés (« A creature can also add any circumstance, deflection, dodge, insight, luck, morale, profane, and sacred bonuses to AC to its CMD » — bonus issus des buffs/conditions/ - dons, défense totale et combat défensif inclus) et une créature au + dons, défense totale et combat défensif inclus), **toutes les pénalités de CA + (« Any penalties to a creature's AC also apply to its CMD » — pénalités sans + type comme grapplé −2 ou à plat ventre −4 contre la mêlée, la manœuvre + étant un jet d'attaque de mêlée)** et une créature au contact pris de dos ne ajoute pas son bonus de Dextérité (« A flat-footed creature does not add its Dexterity bonus to its CMD » — la pénalité de Dextérité, qui n'est pas un bonus, reste appliquée). diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index 8392dbf..6f392db 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -4192,11 +4192,20 @@ class CombatEngine: dex_mod = target.combatant.abilities.dex_mod if self.has_condition_flag(target, "denies_dex_to_ac"): dex_mod = min(0, dex_mod) - ac_bonuses = resolve_modifiers( + # PF1e (SRD Combat Maneuvers): "Any penalties to a creature's AC also + # apply to its CMD." — negative AC modifiers ride the CMD whatever + # their bonus type; only positive ones are restricted to the eight + # SRD-listed types. + ac_mods = [ m for m in self._stat_modifiers(target, "ac") - if m.bonus_type in _CMD_AC_BONUS_TYPES - ) + if m.bonus_type in _CMD_AC_BONUS_TYPES or m.value < 0 + ] + # A combat maneuver is a melee attack roll (SRD: "Combat maneuvers are + # attack rolls"), so the prone defender's -4 melee AC penalty applies. + if self._has_condition_named(target, "prone"): + ac_mods.append(StatModifier(target="ac", value=-4)) + ac_bonuses = resolve_modifiers(ac_mods) # defensive_ac_bonus (total defense / fight defensively) is a dodge # bonus and aid_ac_bonus a circumstance bonus to AC — both types the # SRD lets a creature add to its CMD. diff --git a/tests/test_maneuvers.py b/tests/test_maneuvers.py index 77655c1..f1a24e5 100644 --- a/tests/test_maneuvers.py +++ b/tests/test_maneuvers.py @@ -504,9 +504,10 @@ def test_cmd_adds_eligible_ac_bonus_types() -> None: assert engine._maneuver_cmd(buffed) == 12 + 4 + 2 -def test_cmd_ignores_non_ac_bonus_types() -> None: - # Enhancement/natural armor bonuses and untyped penalties are NOT added - # to CMD (only the eight SRD-listed AC bonus types are). +def test_cmd_ignores_non_ac_bonus_types_keeps_penalties() -> None: + # Enhancement bonuses are NOT among the eight SRD-listed types — ignored. + # But "Any penalties to a creature's AC also apply to its CMD" (SRD + # Combat Maneuvers): the untyped -4 penalty rides the CMD. engine = make_engine([], []) state = _buffed( make_combatant("b"), @@ -515,7 +516,28 @@ def test_cmd_ignores_non_ac_bonus_types() -> None: StatModifier(target="ac", value=-4), ), ) - assert engine._maneuver_cmd(state) == 12 + assert engine._maneuver_cmd(state) == 12 - 4 + + +def test_cmd_includes_prone_and_grappled_penalties() -> None: + # A maneuver is a melee attack roll, so the prone -4 melee AC penalty and + # the grappled -2 AC penalty both apply to CMD (SRD: "Any penalties to a + # creature's AC also apply to its CMD"). + engine = make_engine([], []) + prone = make_state(make_combatant("p"), "p", (0, 0)) + prone.active_conditions.append( + ActiveCondition( + condition=CONDITIONS_BY_NAME["prone"], rounds_left=1, source="test" + ) + ) + assert engine._maneuver_cmd(prone) == 12 - 4 + grappled = make_state(make_combatant("g"), "p", (0, 1)) + grappled.active_conditions.append( + ActiveCondition( + condition=CONDITIONS_BY_NAME["grappled"], rounds_left=1, source="test" + ) + ) + assert engine._maneuver_cmd(grappled) == 12 - 2 def test_cmd_flat_footed_drops_dex_bonus_keeps_penalty() -> None: