fix(rules): Up Close and Deadly deed requires a firearm attack (Ultimate Combat)

This commit is contained in:
2026-09-16 19:55:53 +02:00
parent 2807f719ca
commit 350844fc68
3 changed files with 43 additions and 12 deletions
+3 -1
View File
@@ -1419,7 +1419,9 @@ Règles modélisées :
demi-niveau du canaliseur + modificateur de Charisme` pour demi-dégâts
— CRB Channel Energy ; le mode soin n'accorde pas de sauvegarde), sortilège `evil_eye` (nairda), deed `up_close_and_deadly`
(tammara, action bonus +1d6 à ≤ 30 ft — consommé sur la première attaque
résolue : si elle est à distance ET à ≤ 30 ft, +1d6 s'ajoute aux dégâts ;
résolue : si elle est une attaque **d'arme à feu** à distance ET à ≤ 30 ft
(Ultimate Combat « Deeds — Up Close and Deadly » : « a successful attack
with a firearm against a target within 30 feet »), +1d6 s'ajoute aux dégâts ;
sinon le bonus est perdu ; le drapeau est réinitialisé dans tous les cas),
deed `quick_clear` (S5a — conformance implémentée, Ultimate Combat « Deeds —
Quick Clear » : « As a standard action, you can spend 1 point from your grit
+12 -4
View File
@@ -3096,13 +3096,21 @@ class CombatEngine:
) -> int:
"""Consume the Up Close and Deadly deed on the first attack resolved.
The deed applies +1d6 to a ranged attack within 30 ft; the flag is
reset after the first attack in all cases (the benefit is lost if the
attack is out of range or not ranged).
The deed applies +1d6 to a FIREARM attack that hits a target within
30 ft (Ultimate Combat "Deeds - Up Close and Deadly": "makes a
successful attack with a firearm against a target within 30 feet");
the flag is reset after the first attack in all cases (the benefit is
lost if the attack misses, is not a firearm, is out of range or is
not ranged at all).
"""
if not attacker.deed_up_close_and_deadly:
return damage
if hit and self._is_ranged_attack(weapon) and dist_ft <= _UP_CLOSE_RANGE_FT:
if (
hit
and weapon.firearm
and self._is_ranged_attack(weapon)
and dist_ft <= _UP_CLOSE_RANGE_FT
):
bonus = self._rng.roll_die(6)
damage += bonus
target.hp -= bonus
+28 -7
View File
@@ -5,8 +5,9 @@ Covers:
action when the combatant has the Power Attack feat, and is a logged no-op
otherwise.
- ``deed_up_close_and_deadly`` consumption: the +1d6 bonus applies to the
first ranged attack within 30 ft and the flag is reset; the flag is reset
with no bonus when the first attack is out of range or not ranged.
first firearm attack within 30 ft and the flag is reset; the flag is reset
with no bonus when the first attack is out of range, not ranged, or not a
firearm.
"""
from __future__ import annotations
@@ -33,6 +34,7 @@ def _combatant(
kind: str = "melee",
range_increment_ft: int | None = None,
features: list | None = None,
firearm: bool = False,
) -> Combatant:
attack = AttackSpec(
id=f"{cid}-w",
@@ -41,6 +43,7 @@ def _combatant(
attack_bonus=5,
damage=[DamageComponent(formula=parse_dice("1d8"), types=["slashing"])],
range_increment_ft=range_increment_ft,
firearm=firearm,
)
return Combatant(
id=cid,
@@ -124,11 +127,15 @@ class TestActivatePowerAttack:
class TestDeedUpCloseAndDeadly:
"""Given: an attacker with deed_up_close_and_deadly set
When: an attack is resolved
Then: the first ranged attack within 30 ft gains +1d6 and the flag resets;
otherwise the flag resets with no bonus."""
Then: the first firearm attack within 30 ft gains +1d6 and the flag
resets; otherwise the flag resets with no bonus (Ultimate Combat deed:
"makes a successful attack with a firearm against a target within 30
feet")."""
def test_ranged_within_30ft_adds_d6_and_resets(self) -> None:
attacker = _combatant("gunner", kind="ranged", range_increment_ft=60)
def test_firearm_within_30ft_adds_d6_and_resets(self) -> None:
attacker = _combatant(
"gunner", kind="ranged", range_increment_ft=60, firearm=True
)
target = _foe()
engine, a, t = _engine(attacker, target, [15, 4, 3])
a.deed_up_close_and_deadly = True
@@ -137,8 +144,22 @@ class TestDeedUpCloseAndDeadly:
assert result.damage == 4 + 3 # 1d8(4) + 1d6(3)
assert a.deed_up_close_and_deadly is False
def test_non_firearm_ranged_gets_no_bonus(self) -> None:
# A bow is a ranged attack within 30 ft but NOT a firearm: the deed
# gives no bonus and is consumed on the resolved attack.
attacker = _combatant("archer", kind="ranged", range_increment_ft=60)
target = _foe()
engine, a, t = _engine(attacker, target, [15, 4])
a.deed_up_close_and_deadly = True
result = engine.resolve_attack(a, t, attacker.attacks[0])
assert result.hit is True
assert result.damage == 4 # no +1d6 (not a firearm)
assert a.deed_up_close_and_deadly is False
def test_out_of_range_resets_without_bonus(self) -> None:
attacker = _combatant("gunner", kind="ranged", range_increment_ft=60)
attacker = _combatant(
"gunner", kind="ranged", range_increment_ft=60, firearm=True
)
target = _foe()
engine, a, t = _engine(attacker, target, [15, 4])
a.deed_up_close_and_deadly = True