fix(rules): apply the power attack penalty to melee touch attacks
PF1e (CRB Power Attack): "take a -1 penalty on all melee attack rolls ... The bonus damage does not apply to touch attacks." A melee touch (kind "touch", no range increment) is a melee attack, so _power_attack_amt now rides its attack and confirmation rolls; the +2X/+3X damage bonus stays gated on kind == "melee". Ranged attacks and rays are unaffected. The test that pinned the old melee-weapon-only simplification was rewritten to lock the corrected behavior; README and abilities.py 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:
@@ -743,8 +743,11 @@ Règles modélisées :
|
|||||||
ex. le contact paralysant du liche) n'en fait rien. Conformance P2 : le
|
ex. le contact paralysant du liche) n'en fait rien. Conformance P2 : le
|
||||||
contact de mêlée (touch sans incrément) est une **attaque de mêlée** — il
|
contact de mêlée (touch sans incrément) est une **attaque de mêlée** — il
|
||||||
bénéficie du flanquement, de la hauteur (+1), du +4 contre une cible inerme
|
bénéficie du flanquement, de la hauteur (+1), du +4 contre une cible inerme
|
||||||
et peut porter une attaque sournoise ; Power Attack reste réservé aux armes
|
et peut porter une attaque sournoise ; Power Attack (conformance A6 — CRB :
|
||||||
de mêlée (le contact n'en bénéficie pas).
|
« a –1 penalty on all melee attack rolls… The bonus damage does not apply to
|
||||||
|
touch attacks ») s'applique à **la touche** du contact de mêlée (pénalité −X
|
||||||
|
au jet d'attaque et de confirmation) mais son bonus de dégâts reste réservé
|
||||||
|
aux armes de mêlée (le contact ne gagne pas le +2X/+3X).
|
||||||
- Jets de sauvegarde : `resolve_save(state, save_type, dc)` — fort/ref/will.
|
- Jets de sauvegarde : `resolve_save(state, save_type, dc)` — fort/ref/will.
|
||||||
1 naturel = échec automatique, 20 naturel = succès automatique. Les
|
1 naturel = échec automatique, 20 naturel = succès automatique. Les
|
||||||
modificateurs d'effets (`StatModifier` avec `target` correspondant) sont
|
modificateurs d'effets (`StatModifier` avec `target` correspondant) sont
|
||||||
@@ -1205,7 +1208,9 @@ la lecture mais non appliquée dans la résolution.
|
|||||||
- `abilities.py` — dons et capacités : `AbilitySpec` (dataclass avec
|
- `abilities.py` — dons et capacités : `AbilitySpec` (dataclass avec
|
||||||
catégorie feat/class/racial/trait) et fabriques de dons (Weapon Focus ciblé
|
catégorie feat/class/racial/trait) et fabriques de dons (Weapon Focus ciblé
|
||||||
par arme, Toughness lié au niveau, Power Attack actif −X attaque / +2X
|
par arme, Toughness lié au niveau, Power Attack actif −X attaque / +2X
|
||||||
dégâts, X = BAB//4 + 1, mêlée uniquement, activé comme action libre via le
|
dégâts, X = BAB//4 + 1 — la pénalité −X vise toutes les attaques de mêlée
|
||||||
|
(contact de mêlée inclus, conformance A6), le bonus de dégâts uniquement les
|
||||||
|
armes de mêlée —, activé comme action libre via le
|
||||||
kind `activate_power_attack` qui pose `state.power_attack`). Conformance
|
kind `activate_power_attack` qui pose `state.power_attack`). Conformance
|
||||||
B16 (CRB « Power Attack ») : le bonus de dégâts passe à **+3X (×1,5)** pour
|
B16 (CRB « Power Attack ») : le bonus de dégâts passe à **+3X (×1,5)** pour
|
||||||
une arme marquée `two_handed: true` dans le JSON de l'attaque (arme à deux
|
une arme marquée `two_handed: true` dans le JSON de l'attaque (arme à deux
|
||||||
|
|||||||
@@ -139,8 +139,9 @@ DODGE = AbilitySpec(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Power Attack: active feat — declare at turn start, -X atk / +2X dmg on all
|
# Power Attack: active feat — declare at turn start, -X atk on all melee
|
||||||
# melee attacks this turn, where X = BAB//4 + 1 (min 1). No passive modifiers;
|
# attack rolls (melee touch included) and +2X dmg on melee WEAPON damage only
|
||||||
|
# (never touch), where X = BAB//4 + 1 (min 1). No passive modifiers;
|
||||||
# the engine checks for this feat and the ``power_attack`` flag on state.
|
# the engine checks for this feat and the ``power_attack`` flag on state.
|
||||||
POWER_ATTACK = AbilitySpec(
|
POWER_ATTACK = AbilitySpec(
|
||||||
name="Power Attack",
|
name="Power Attack",
|
||||||
|
|||||||
@@ -2283,7 +2283,12 @@ class CombatEngine:
|
|||||||
if self._is_melee_attack(weapon) and self._is_helpless(target):
|
if self._is_melee_attack(weapon) and self._is_helpless(target):
|
||||||
atk_mods.append(StatModifier(target="attack", value=_HELPLESS_MELEE_BONUS))
|
atk_mods.append(StatModifier(target="attack", value=_HELPLESS_MELEE_BONUS))
|
||||||
attack_mod = resolve_modifiers(atk_mods)
|
attack_mod = resolve_modifiers(atk_mods)
|
||||||
pa_amt = self._power_attack_amt(attacker) if weapon.kind == "melee" else 0
|
# PF1e (CRB Power Attack): the -X penalty rides ALL melee attack rolls
|
||||||
|
# — a melee touch (no range increment) is a melee attack. The damage
|
||||||
|
# bonus it buys stays weapon-only (see _resolve_hit_damage).
|
||||||
|
pa_amt = (
|
||||||
|
self._power_attack_amt(attacker) if self._is_melee_attack(weapon) else 0
|
||||||
|
)
|
||||||
fd_penalty = (
|
fd_penalty = (
|
||||||
_FIGHT_DEFENSIVELY_ATTACK_PENALTY if attacker.fight_defensively else 0
|
_FIGHT_DEFENSIVELY_ATTACK_PENALTY if attacker.fight_defensively else 0
|
||||||
)
|
)
|
||||||
@@ -2445,7 +2450,10 @@ class CombatEngine:
|
|||||||
# weapon used in two hands, or a primary natural with 1.5x Strength
|
# weapon used in two hands, or a primary natural with 1.5x Strength
|
||||||
# (modeled via the weapon's `two_handed` flag). The off-hand /
|
# (modeled via the weapon's `two_handed` flag). The off-hand /
|
||||||
# secondary-natural halving (-50%, +1X) is NOT modeled: two-weapon
|
# secondary-natural halving (-50%, +1X) is NOT modeled: two-weapon
|
||||||
# fighting is not in the engine (documented simplification).
|
# fighting is not in the engine (documented simplification). The
|
||||||
|
# bonus damage never applies to touch attacks (CRB), which is why
|
||||||
|
# this gate is kind == "melee" while the -X penalty also rides melee
|
||||||
|
# touch rolls.
|
||||||
if weapon.kind == "melee":
|
if weapon.kind == "melee":
|
||||||
damage += pa_amt * (3 if weapon.two_handed else 2)
|
damage += pa_amt * (3 if weapon.two_handed else 2)
|
||||||
if crit:
|
if crit:
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ B8 Flanking: the flanking ally must actually THREATEN the target (a reach
|
|||||||
weapon's 5-ft dead zone does not threaten) and must not be unable to act.
|
weapon's 5-ft dead zone does not threaten) and must not be unable to act.
|
||||||
B9 Melee touch (kind "touch", no range increment) is a melee attack: it gains
|
B9 Melee touch (kind "touch", no range increment) is a melee attack: it gains
|
||||||
flanking, high ground, the vs-helpless +4 and can sneak attack; Power
|
flanking, high ground, the vs-helpless +4 and can sneak attack; Power
|
||||||
Attack stays melee-only.
|
Attack's -X penalty rides its attack rolls while the bonus damage never
|
||||||
|
applies to touch (A6 refinement — the original "melee-only feat" reading
|
||||||
|
was itself a deviation).
|
||||||
B10 Blinded: NO flat -2 on attack rolls (the modeled 50% total-concealment
|
B10 Blinded: NO flat -2 on attack rolls (the modeled 50% total-concealment
|
||||||
miss is the RAW penalty); the -4 Dex-skill penalty rides the dex_skills
|
miss is the RAW penalty); the -4 Dex-skill penalty rides the dex_skills
|
||||||
target (B22).
|
target (B22).
|
||||||
@@ -233,7 +235,7 @@ def test_melee_touch_can_sneak_attack() -> None:
|
|||||||
assert any("sneak attack" in line for line in eng._transcript)
|
assert any("sneak attack" in line for line in eng._transcript)
|
||||||
|
|
||||||
|
|
||||||
def test_power_attack_still_excludes_melee_touch() -> None:
|
def test_power_attack_penalizes_melee_touch_attack_roll() -> None:
|
||||||
atk = _combatant(
|
atk = _combatant(
|
||||||
"atk", bab=5, attacks=(_touch("atk"),), features=("Power Attack",)
|
"atk", bab=5, attacks=(_touch("atk"),), features=("Power Attack",)
|
||||||
)
|
)
|
||||||
@@ -242,8 +244,11 @@ def test_power_attack_still_excludes_melee_touch() -> None:
|
|||||||
a = eng._states[0]
|
a = eng._states[0]
|
||||||
a.power_attack = True
|
a.power_attack = True
|
||||||
result = eng.resolve_attack(a, eng._states[1], _touch("atk"))
|
result = eng.resolve_attack(a, eng._states[1], _touch("atk"))
|
||||||
# Power Attack is melee-weapon-only: the touch swing keeps its full bonus.
|
# PF1e (CRB Power Attack): the -X penalty rides ALL melee attack rolls and
|
||||||
assert result.total == 10 + 10
|
# a melee touch IS a melee attack (X = 5//4 + 1 = 2); the bonus damage
|
||||||
|
# never applies to touch attacks — 1d6 = 2, no +2X.
|
||||||
|
assert result.total == 10 + 10 - 2
|
||||||
|
assert result.damage == 2
|
||||||
|
|
||||||
|
|
||||||
# ── B10: blinded has no flat attack penalty ──────────────────────────────────
|
# ── B10: blinded has no flat attack penalty ──────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user