fix(rules): compute CMD from Strength and Dexterity directly

PF1e (SRD Combat Maneuvers): CMD = 10 + BAB + Strength modifier +
Dexterity modifier + special size modifier. The Tiny-or-smaller
Dex-for-Str substitution is CMB-only, but _maneuver_cmd reused
_maneuver_cmb for the base, so Tiny/Diminutive/Fine creatures got
10 + BAB + 2xDex + size and dropped Strength (live bug on the Tiny PC
ierlieth). The CMD base is now computed with str_mod directly; CMB
keeps its substitution.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-12 10:54:21 +02:00
parent e1c9e78b4e
commit 5a848fd07c
2 changed files with 22 additions and 2 deletions
+12
View File
@@ -461,6 +461,18 @@ def test_small_or_larger_cmb_uses_strength() -> None:
assert engine._maneuver_cmb(medium) == 2 - 1 + 0 # bab + str + size
def test_tiny_or_smaller_cmd_uses_strength_and_dexterity() -> None:
# SRD Combat Maneuvers: "CMD = 10 + Base attack bonus + Strength modifier
# + Dexterity modifier + special size modifier" — the Tiny-or-smaller
# Dex-for-Str substitution is CMB-only. Tiny: str -1, dex +4, bab 2,
# size -2 -> 10 + 2 - 1 + 4 - 2 = 13 (not 10 + 2 + 4 - 2 + 4 = 18).
engine = make_engine([], [])
for size in ("Tiny", "Diminutive", "Fine"):
tiny = make_state(_resized(make_combatant("t"), size, 8, 18), "p", (0, 0))
expected = 10 + 2 - 1 + 4 + _SIZE_MODIFIERS[size.lower()]
assert engine._maneuver_cmd(tiny) == expected
def _buffed(spec: Combatant, mods: tuple[StatModifier, ...]) -> CombatantState:
state = make_state(spec, "p", (0, 0))
state.active_buffs.append(