fix(rules): Up Close and Deadly deed requires a firearm attack (Ultimate Combat)
This commit is contained in:
@@ -1419,7 +1419,9 @@ Règles modélisées :
|
|||||||
demi-niveau du canaliseur + modificateur de Charisme` pour demi-dégâts
|
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`
|
— 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
|
(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),
|
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 —
|
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
|
Quick Clear » : « As a standard action, you can spend 1 point from your grit
|
||||||
|
|||||||
@@ -3096,13 +3096,21 @@ class CombatEngine:
|
|||||||
) -> int:
|
) -> int:
|
||||||
"""Consume the Up Close and Deadly deed on the first attack resolved.
|
"""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
|
The deed applies +1d6 to a FIREARM attack that hits a target within
|
||||||
reset after the first attack in all cases (the benefit is lost if the
|
30 ft (Ultimate Combat "Deeds - Up Close and Deadly": "makes a
|
||||||
attack is out of range or not ranged).
|
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:
|
if not attacker.deed_up_close_and_deadly:
|
||||||
return damage
|
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)
|
bonus = self._rng.roll_die(6)
|
||||||
damage += bonus
|
damage += bonus
|
||||||
target.hp -= bonus
|
target.hp -= bonus
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ Covers:
|
|||||||
action when the combatant has the Power Attack feat, and is a logged no-op
|
action when the combatant has the Power Attack feat, and is a logged no-op
|
||||||
otherwise.
|
otherwise.
|
||||||
- ``deed_up_close_and_deadly`` consumption: the +1d6 bonus applies to the
|
- ``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
|
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 or not ranged.
|
with no bonus when the first attack is out of range, not ranged, or not a
|
||||||
|
firearm.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -33,6 +34,7 @@ def _combatant(
|
|||||||
kind: str = "melee",
|
kind: str = "melee",
|
||||||
range_increment_ft: int | None = None,
|
range_increment_ft: int | None = None,
|
||||||
features: list | None = None,
|
features: list | None = None,
|
||||||
|
firearm: bool = False,
|
||||||
) -> Combatant:
|
) -> Combatant:
|
||||||
attack = AttackSpec(
|
attack = AttackSpec(
|
||||||
id=f"{cid}-w",
|
id=f"{cid}-w",
|
||||||
@@ -41,6 +43,7 @@ def _combatant(
|
|||||||
attack_bonus=5,
|
attack_bonus=5,
|
||||||
damage=[DamageComponent(formula=parse_dice("1d8"), types=["slashing"])],
|
damage=[DamageComponent(formula=parse_dice("1d8"), types=["slashing"])],
|
||||||
range_increment_ft=range_increment_ft,
|
range_increment_ft=range_increment_ft,
|
||||||
|
firearm=firearm,
|
||||||
)
|
)
|
||||||
return Combatant(
|
return Combatant(
|
||||||
id=cid,
|
id=cid,
|
||||||
@@ -124,11 +127,15 @@ class TestActivatePowerAttack:
|
|||||||
class TestDeedUpCloseAndDeadly:
|
class TestDeedUpCloseAndDeadly:
|
||||||
"""Given: an attacker with deed_up_close_and_deadly set
|
"""Given: an attacker with deed_up_close_and_deadly set
|
||||||
When: an attack is resolved
|
When: an attack is resolved
|
||||||
Then: the first ranged attack within 30 ft gains +1d6 and the flag resets;
|
Then: the first firearm attack within 30 ft gains +1d6 and the flag
|
||||||
otherwise the flag resets with no bonus."""
|
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:
|
def test_firearm_within_30ft_adds_d6_and_resets(self) -> None:
|
||||||
attacker = _combatant("gunner", kind="ranged", range_increment_ft=60)
|
attacker = _combatant(
|
||||||
|
"gunner", kind="ranged", range_increment_ft=60, firearm=True
|
||||||
|
)
|
||||||
target = _foe()
|
target = _foe()
|
||||||
engine, a, t = _engine(attacker, target, [15, 4, 3])
|
engine, a, t = _engine(attacker, target, [15, 4, 3])
|
||||||
a.deed_up_close_and_deadly = True
|
a.deed_up_close_and_deadly = True
|
||||||
@@ -137,8 +144,22 @@ class TestDeedUpCloseAndDeadly:
|
|||||||
assert result.damage == 4 + 3 # 1d8(4) + 1d6(3)
|
assert result.damage == 4 + 3 # 1d8(4) + 1d6(3)
|
||||||
assert a.deed_up_close_and_deadly is False
|
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:
|
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()
|
target = _foe()
|
||||||
engine, a, t = _engine(attacker, target, [15, 4])
|
engine, a, t = _engine(attacker, target, [15, 4])
|
||||||
a.deed_up_close_and_deadly = True
|
a.deed_up_close_and_deadly = True
|
||||||
|
|||||||
Reference in New Issue
Block a user