fix(rules): use Dexterity for the CMB of Tiny and smaller creatures
PF1e (SRD Combat Maneuvers): "Creatures that are size Tiny or smaller use their Dexterity modifier in place of their Strength modifier to determine their CMB." _maneuver_cmb now swaps Str for Dex when the size is Tiny, Diminutive or Fine. True 0-reach and co-occupancy for Tiny-or-smaller stay unmodelled (5-ft grid abstraction), documented in the README. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -976,11 +976,17 @@ Règles modélisées :
|
||||
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
|
||||
et les jets de manœuvres de combat » ; l'escape par compétence n'est pas
|
||||
pénalisé (un test de compétence n'est pas un jet de manœuvre).
|
||||
trip → `prone` ; disarm → `disarmed` (−4 attaque) ; bull_rush → pousse la
|
||||
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
|
||||
et les jets de manœuvres de combat » ; l'escape par compétence n'est pas
|
||||
pénalisé (un test de compétence n'est pas un jet de manœuvre). Conformance
|
||||
L1 (SRD « Combat Maneuvers ») : « Creatures that are size Tiny or smaller
|
||||
use their Dexterity modifier in place of their Strength modifier to
|
||||
determine their CMB » — Tiny/Diminutive/Fine calculent donc leur CMB avec
|
||||
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).
|
||||
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
|
||||
compétence `escape_artist` ou CMB, + un d20) contre CMD — une seule tentative,
|
||||
|
||||
@@ -271,6 +271,10 @@ _SIZE_MODIFIERS: dict[str, int] = {
|
||||
"medium": 0, "large": 1, "huge": 2, "gargantuan": 4, "colossal": 8,
|
||||
}
|
||||
|
||||
# PF1e (SRD Combat Maneuvers): creatures Tiny or smaller use their Dexterity
|
||||
# modifier in place of their Strength modifier to determine their CMB.
|
||||
_TINY_OR_SMALLER_SIZES: frozenset[str] = frozenset({"tiny", "diminutive", "fine"})
|
||||
|
||||
# PF1e natural reach for tall creatures (simplification: no long/quadruped
|
||||
# distinction). Unknown sizes fall back to 5 ft (the historical behavior).
|
||||
_SIZE_NATURAL_REACH_FT: dict[str, int] = {
|
||||
@@ -4033,9 +4037,17 @@ class CombatEngine:
|
||||
)
|
||||
|
||||
def _maneuver_cmb(self, state: CombatantState) -> int:
|
||||
# PF1e (SRD Combat Maneuvers): "Creatures that are size Tiny or
|
||||
# smaller use their Dexterity modifier in place of their Strength
|
||||
# modifier to determine their CMB."
|
||||
ability_mod = (
|
||||
state.combatant.abilities.dex_mod
|
||||
if state.combatant.size.lower() in _TINY_OR_SMALLER_SIZES
|
||||
else state.combatant.abilities.str_mod
|
||||
)
|
||||
return (
|
||||
state.combatant.cmb
|
||||
or (state.combatant.bab + state.combatant.abilities.str_mod + self._size_mod(state))
|
||||
or (state.combatant.bab + ability_mod + self._size_mod(state))
|
||||
)
|
||||
|
||||
def _maneuver_cmd(self, target: CombatantState) -> int:
|
||||
|
||||
+32
-1
@@ -7,7 +7,7 @@ it (effectively disarmed). See the plan task-12.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pf1e_simulator.combat import CombatantState, CombatEngine
|
||||
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.grid import Grid
|
||||
@@ -269,3 +269,34 @@ def test_drag_no_space() -> None:
|
||||
assert b.pos == (1, 2)
|
||||
assert a.pos == (1, 1)
|
||||
assert "no space" in engine._transcript[-1]
|
||||
|
||||
|
||||
def _resized(spec: Combatant, size: str, str_score: int, dex_score: int) -> Combatant:
|
||||
return spec.model_copy(
|
||||
update={
|
||||
"size": size,
|
||||
"abilities": spec.abilities.model_copy(
|
||||
update={"str_score": str_score, "dex_score": dex_score}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_tiny_or_smaller_cmb_uses_dexterity() -> None:
|
||||
# SRD Combat Maneuvers: "Creatures that are size Tiny or smaller use
|
||||
# their Dexterity modifier in place of their Strength modifier to
|
||||
# determine their CMB." Tiny: str -1, dex +4, bab 2, size -2 -> 2+4-2.
|
||||
engine = make_engine([], [])
|
||||
for size in ("Tiny", "Diminutive", "Fine"):
|
||||
tiny = make_state(_resized(make_combatant("t"), size, 8, 18), "p", (0, 0))
|
||||
expected = 2 + 4 + _SIZE_MODIFIERS[size.lower()]
|
||||
assert engine._maneuver_cmb(tiny) == expected
|
||||
|
||||
|
||||
def test_small_or_larger_cmb_uses_strength() -> None:
|
||||
# The Dex substitution stops at Tiny: Small and larger use Strength.
|
||||
engine = make_engine([], [])
|
||||
small = make_state(_resized(make_combatant("s"), "Small", 8, 18), "p", (0, 0))
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user