fix(rules): add AC bonuses and flat-footed Dexterity to CMD
PF1e (SRD Combat Maneuver Defense): "A creature can also add any circumstance, deflection, dodge, insight, luck, morale, profane, and sacred bonuses to AC to its CMD [...] A flat-footed creature does not add its Dexterity bonus to its CMD." _maneuver_cmd now sums the target's eligible AC bonus modifiers (plus the dodge/circumstance runtime bonuses from total defense, fight defensively and aid another) and drops the Dexterity bonus (keeping any penalty) while the target denies its Dexterity to AC. README updated. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -986,6 +986,14 @@ Règles modélisées :
|
||||
le modificateur de Dextérité à la place de celui de Force ; lacune
|
||||
documentée : l'allonge nulle et la co-occupation des créatures Tiny et
|
||||
plus petites ne sont pas modélisées (abstraction à la grille de 5 ft).
|
||||
Conformance L2 (SRD « Combat Maneuver Defense ») : le CMD calculé ajoute
|
||||
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
|
||||
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).
|
||||
trip → `prone` ; disarm → `disarmed` (−4 attaque) ; bull_rush → pousse la
|
||||
cible (direction sélectionnable via `Action.direction`) ;
|
||||
grapple → `grappled`/`pinned` ; escape → **une** sauvegarde (bonus statique de la
|
||||
|
||||
@@ -303,6 +303,15 @@ _NEIGHBOR_OFFSETS: tuple[tuple[int, int], ...] = (
|
||||
)
|
||||
|
||||
|
||||
# PF1e (SRD Combat Maneuver Defense): the AC bonus types a creature may add
|
||||
# to its CMD. Penalties and other bonus types (armor, shield, natural,
|
||||
# enhancement, competence, untyped) stay out of the CMD.
|
||||
_CMD_AC_BONUS_TYPES: frozenset[str] = frozenset({
|
||||
"circumstance", "deflection", "dodge", "insight", "luck", "morale",
|
||||
"profane", "sacred",
|
||||
})
|
||||
|
||||
|
||||
def _save_label(result: SaveResult | None) -> str:
|
||||
if result is None:
|
||||
return "NO SAVE"
|
||||
@@ -4051,8 +4060,29 @@ class CombatEngine:
|
||||
)
|
||||
|
||||
def _maneuver_cmd(self, target: CombatantState) -> int:
|
||||
if target.combatant.cmd:
|
||||
return target.combatant.cmd
|
||||
target_cmb = self._maneuver_cmb(target)
|
||||
return target.combatant.cmd or (10 + target_cmb + target.combatant.abilities.dex_mod)
|
||||
# PF1e (SRD Combat Maneuver Defense): "A creature can also add any
|
||||
# circumstance, deflection, dodge, insight, luck, morale, profane,
|
||||
# and sacred bonuses to AC to its CMD. [...] A flat-footed creature
|
||||
# does not add its Dexterity bonus to its CMD." A Dexterity penalty
|
||||
# is not a bonus, so it still applies when flat-footed.
|
||||
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(
|
||||
m
|
||||
for m in self._stat_modifiers(target, "ac")
|
||||
if m.bonus_type in _CMD_AC_BONUS_TYPES
|
||||
)
|
||||
# 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.
|
||||
return (
|
||||
10 + target_cmb + dex_mod + ac_bonuses
|
||||
+ target.defensive_ac_bonus + target.aid_ac_bonus
|
||||
)
|
||||
|
||||
def _maneuver_trip(
|
||||
self, state: CombatantState, target: CombatantState, m: _ManeuverRoll
|
||||
|
||||
+67
-1
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
|
||||
from pf1e_simulator.combat import _SIZE_MODIFIERS, CombatantState, CombatEngine
|
||||
from pf1e_simulator.conditions import CONDITIONS_BY_NAME
|
||||
from pf1e_simulator.effects import ActiveCondition
|
||||
from pf1e_simulator.effects import ActiveBuff, ActiveCondition, StatModifier
|
||||
from pf1e_simulator.grid import Grid
|
||||
from pf1e_simulator.map import MapSpec, TerrainType
|
||||
from pf1e_simulator.models import (
|
||||
@@ -300,3 +300,69 @@ def test_small_or_larger_cmb_uses_strength() -> None:
|
||||
medium = make_state(_resized(make_combatant("m"), "Medium", 8, 18), "p", (0, 1))
|
||||
assert engine._maneuver_cmb(small) == 2 - 1 - 1 # bab + str + size
|
||||
assert engine._maneuver_cmb(medium) == 2 - 1 + 0 # bab + str + size
|
||||
|
||||
|
||||
def _buffed(spec: Combatant, mods: tuple[StatModifier, ...]) -> CombatantState:
|
||||
state = make_state(spec, "p", (0, 0))
|
||||
state.active_buffs.append(
|
||||
ActiveBuff(modifiers=mods, rounds_left=1, source="test")
|
||||
)
|
||||
return state
|
||||
|
||||
|
||||
def test_cmd_adds_eligible_ac_bonus_types() -> None:
|
||||
# SRD CMD: "A creature can also add any circumstance, deflection, dodge,
|
||||
# insight, luck, morale, profane, and sacred bonuses to AC to its CMD."
|
||||
# Medium str 10 dex 10 bab 2: base CMD 10 + (2+0+0) + 0 = 12.
|
||||
engine = make_engine([], [])
|
||||
base = make_state(make_combatant("b"), "p", (0, 0))
|
||||
assert engine._maneuver_cmd(base) == 12
|
||||
buffed = _buffed(
|
||||
make_combatant("b"),
|
||||
(
|
||||
StatModifier(target="ac", value=4, bonus_type="dodge"),
|
||||
StatModifier(target="ac", value=2, bonus_type="morale"),
|
||||
),
|
||||
)
|
||||
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).
|
||||
engine = make_engine([], [])
|
||||
state = _buffed(
|
||||
make_combatant("b"),
|
||||
(
|
||||
StatModifier(target="ac", value=2, bonus_type="enhancement"),
|
||||
StatModifier(target="ac", value=-4),
|
||||
),
|
||||
)
|
||||
assert engine._maneuver_cmd(state) == 12
|
||||
|
||||
|
||||
def test_cmd_flat_footed_drops_dex_bonus_keeps_penalty() -> None:
|
||||
# SRD CMD: "A flat-footed creature does not add its Dexterity bonus to
|
||||
# its CMD." A Dex penalty is not a bonus, so it still applies.
|
||||
engine = make_engine([], [])
|
||||
dexy = _resized(make_combatant("d"), "Medium", 10, 18) # dex +4
|
||||
flat = make_state(dexy, "p", (0, 0))
|
||||
flat.active_conditions.append(
|
||||
ActiveCondition(
|
||||
condition=CONDITIONS_BY_NAME["flat_footed"],
|
||||
rounds_left=1,
|
||||
source="test",
|
||||
)
|
||||
)
|
||||
assert engine._maneuver_cmd(make_state(dexy, "p", (0, 1))) == 10 + 2 + 4
|
||||
assert engine._maneuver_cmd(flat) == 10 + 2 # Dex bonus gone
|
||||
clumsy = _resized(make_combatant("c"), "Medium", 10, 6) # dex -2
|
||||
clumsy_flat = make_state(clumsy, "p", (0, 2))
|
||||
clumsy_flat.active_conditions.append(
|
||||
ActiveCondition(
|
||||
condition=CONDITIONS_BY_NAME["flat_footed"],
|
||||
rounds_left=1,
|
||||
source="test",
|
||||
)
|
||||
)
|
||||
assert engine._maneuver_cmd(clumsy_flat) == 10 + 2 - 2 # penalty kept
|
||||
|
||||
Reference in New Issue
Block a user