fix(rules): helpless Dex-0 AC adds exactly -5 on flat-footed base; touch base strips full Dex mod when helpless (no negative-Dex double count)
This commit is contained in:
@@ -2707,9 +2707,11 @@ class CombatEngine:
|
|||||||
PF1e (CRB): a prone defender takes -4 AC against melee attacks but
|
PF1e (CRB): a prone defender takes -4 AC against melee attacks but
|
||||||
gains +4 AC against ranged attacks; a helpless defender (paralyzed,
|
gains +4 AC against ranged attacks; a helpless defender (paralyzed,
|
||||||
asleep, unconscious, helpless) has an effective Dexterity score of 0,
|
asleep, unconscious, helpless) has an effective Dexterity score of 0,
|
||||||
so its whole Dex modifier gives way to the Dex-0 penalty (-5) —
|
so its whole Dex modifier gives way to the Dex-0 penalty (-5). The
|
||||||
flat-footed AC already drops a positive Dex bonus, a native Dex
|
flat-footed base already excludes ALL Dex (bonus and penalty alike),
|
||||||
penalty must still be replaced."""
|
so the mod is exactly -5 there; on the touch base the stored Dex
|
||||||
|
modifier is stripped at the base (see resolve_attack) before this
|
||||||
|
-5 is added."""
|
||||||
mods = self._stat_modifiers(target, "ac")
|
mods = self._stat_modifiers(target, "ac")
|
||||||
if self._has_condition_named(target, "prone"):
|
if self._has_condition_named(target, "prone"):
|
||||||
mods.append(
|
mods.append(
|
||||||
@@ -2718,8 +2720,7 @@ class CombatEngine:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if self._is_helpless(target):
|
if self._is_helpless(target):
|
||||||
dex_penalty = min(0, target.combatant.abilities.dex_mod)
|
mods.append(StatModifier(target="ac", value=_DEX_ZERO_MOD))
|
||||||
mods.append(StatModifier(target="ac", value=_DEX_ZERO_MOD - dex_penalty))
|
|
||||||
return mods
|
return mods
|
||||||
|
|
||||||
def resolve_attack(
|
def resolve_attack(
|
||||||
@@ -2804,9 +2805,14 @@ class CombatEngine:
|
|||||||
):
|
):
|
||||||
base_ac = full_ac_base
|
base_ac = full_ac_base
|
||||||
elif denies_dex:
|
elif denies_dex:
|
||||||
base_ac = (
|
# A flat-footed defender loses only a POSITIVE Dex bonus (a
|
||||||
target.combatant.ac.touch - max(0, target.combatant.abilities.dex_mod)
|
# Dex penalty stays on touch AC); a HELPLESS defender has an
|
||||||
)
|
# effective Dex score of 0, so its whole stored Dex modifier
|
||||||
|
# is stripped here and the exact -5 Dex-0 penalty is added by
|
||||||
|
# _attack_ac_mods.
|
||||||
|
dex_mod = target.combatant.abilities.dex_mod
|
||||||
|
strip = dex_mod if self._is_helpless(target) else max(0, dex_mod)
|
||||||
|
base_ac = target.combatant.ac.touch - strip
|
||||||
else:
|
else:
|
||||||
base_ac = target.combatant.ac.touch
|
base_ac = target.combatant.ac.touch
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -328,8 +328,9 @@ def test_stunned_is_not_helpless() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_helpless_dex_zero_replaces_a_native_dex_penalty() -> None:
|
def test_helpless_dex_zero_replaces_a_native_dex_penalty() -> None:
|
||||||
# A Dex 8 (-1) helpless target: the -1 penalty gives way to the Dex-0 -5
|
# A Dex 8 (-1) helpless target: the flat-footed base already excludes
|
||||||
# (adjustment -5 - (-1) = -4 on top of the flat-footed base).
|
# ALL Dex (bonus and penalty alike), so effective Dex 0 adds exactly -5.
|
||||||
|
# The old formula added -5 - (-1) = -4, double-counting the Dex penalty.
|
||||||
eng = _engine(
|
eng = _engine(
|
||||||
[_combatant("atk"), _combatant("def", ac=14, dex=8)],
|
[_combatant("atk"), _combatant("def", ac=14, dex=8)],
|
||||||
[(0, 0), (0, 1)],
|
[(0, 0), (0, 1)],
|
||||||
@@ -337,7 +338,68 @@ def test_helpless_dex_zero_replaces_a_native_dex_penalty() -> None:
|
|||||||
)
|
)
|
||||||
_add_condition(eng._states[1], PARALYZED)
|
_add_condition(eng._states[1], PARALYZED)
|
||||||
result = eng.resolve_attack(eng._states[0], eng._states[1], _melee("atk"))
|
result = eng.resolve_attack(eng._states[0], eng._states[1], _melee("atk"))
|
||||||
assert result.ac == 10 # 14 - 4
|
assert result.ac == 9 # 14 - 5
|
||||||
|
|
||||||
|
|
||||||
|
def _touch(cid: str) -> AttackSpec:
|
||||||
|
return AttackSpec(
|
||||||
|
id=f"{cid}-touch",
|
||||||
|
name="touch",
|
||||||
|
kind="touch",
|
||||||
|
attack_bonus=5,
|
||||||
|
damage=[DamageComponent(formula=parse_dice("1d6"), types=["cold"])],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_helpless_positive_dex_melee_ac_is_flat_footed_minus5() -> None:
|
||||||
|
# Dex 16 (+3): flat-footed base drops the +3 already; Dex-0 adds -5.
|
||||||
|
eng = _engine(
|
||||||
|
[_combatant("atk"), _combatant("def", ac=16, dex=16)],
|
||||||
|
[(0, 0), (0, 1)],
|
||||||
|
[10, 4],
|
||||||
|
)
|
||||||
|
_add_condition(eng._states[1], PARALYZED)
|
||||||
|
result = eng.resolve_attack(eng._states[0], eng._states[1], _melee("atk"))
|
||||||
|
assert result.ac == 11 # 16 - 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_helpless_positive_dex_touch_ac_strips_dex_then_minus5() -> None:
|
||||||
|
# Dex 16 (+3): touch base 16 includes the +3; effective Dex 0 strips it
|
||||||
|
# (16 - 3) and adds -5 -> 8.
|
||||||
|
eng = _engine(
|
||||||
|
[_combatant("atk"), _combatant("def", ac=16, dex=16)],
|
||||||
|
[(0, 0), (0, 1)],
|
||||||
|
[10, 4],
|
||||||
|
)
|
||||||
|
_add_condition(eng._states[1], PARALYZED)
|
||||||
|
result = eng.resolve_attack(eng._states[0], eng._states[1], _touch("atk"))
|
||||||
|
assert result.ac == 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_helpless_negative_dex_touch_ac_strips_penalty_then_minus5() -> None:
|
||||||
|
# Dex 8 (-1): touch base 14 includes the -1; effective Dex 0 replaces it
|
||||||
|
# (14 + 1) and adds -5 -> 10.
|
||||||
|
eng = _engine(
|
||||||
|
[_combatant("atk"), _combatant("def", ac=14, dex=8)],
|
||||||
|
[(0, 0), (0, 1)],
|
||||||
|
[10, 4],
|
||||||
|
)
|
||||||
|
_add_condition(eng._states[1], PARALYZED)
|
||||||
|
result = eng.resolve_attack(eng._states[0], eng._states[1], _touch("atk"))
|
||||||
|
assert result.ac == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_flat_footed_negative_dex_touch_keeps_penalty_without_minus5() -> None:
|
||||||
|
# Control: a merely flat-footed (NOT helpless) Dex 8 target loses only a
|
||||||
|
# positive Dex bonus — the -1 stays on touch AC and no Dex-0 -5 applies.
|
||||||
|
eng = _engine(
|
||||||
|
[_combatant("atk"), _combatant("def", ac=14, dex=8)],
|
||||||
|
[(0, 0), (0, 1)],
|
||||||
|
[10, 4],
|
||||||
|
)
|
||||||
|
_add_condition(eng._states[1], CONDITIONS_BY_NAME["flat_footed"])
|
||||||
|
result = eng.resolve_attack(eng._states[0], eng._states[1], _touch("atk"))
|
||||||
|
assert result.ac == 14
|
||||||
|
|
||||||
|
|
||||||
# ── B6: nauseated — single move action only, no casting ──────────────────────
|
# ── B6: nauseated — single move action only, no casting ──────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user