fix(rules): minimum damage - a landed hit reduced below 1 by DR/energy resistance deals 1 (CRB Damage - Minimum Damage, nonlethal approximated as normal)
This commit is contained in:
@@ -180,6 +180,7 @@ _CHARGE_AC_PENALTY = 2 # PF1e: -2 AC until start of next turn after charging
|
|||||||
_CHARGE_MIN_CELLS = 2 # PF1e: charge must move at least 10 ft (2 squares)
|
_CHARGE_MIN_CELLS = 2 # PF1e: charge must move at least 10 ft (2 squares)
|
||||||
_UP_CLOSE_RANGE_FT = 30 # PF1e: Up Close and Deadly works within 30 ft
|
_UP_CLOSE_RANGE_FT = 30 # PF1e: Up Close and Deadly works within 30 ft
|
||||||
_GRAPPLED_CMB_PENALTY = 2 # PF1e: grappled/pinned -2 on non-grapple/escape CMB
|
_GRAPPLED_CMB_PENALTY = 2 # PF1e: grappled/pinned -2 on non-grapple/escape CMB
|
||||||
|
_MINIMUM_HIT_DAMAGE = 1 # PF1e (CRB "Damage" - Minimum Damage): a hit deals >= 1
|
||||||
_DISARM_RETALIATION_MARGIN = 10 # PF1e SRD: disarm fails by 10 -> attacker drops
|
_DISARM_RETALIATION_MARGIN = 10 # PF1e SRD: disarm fails by 10 -> attacker drops
|
||||||
# PF1e: Point-Blank Shot grants +1/+1 within 30 ft with ranged weapons
|
# PF1e: Point-Blank Shot grants +1/+1 within 30 ft with ranged weapons
|
||||||
_POINT_BLANK_RANGE_FT = 30
|
_POINT_BLANK_RANGE_FT = 30
|
||||||
@@ -2981,9 +2982,15 @@ class CombatEngine:
|
|||||||
)
|
)
|
||||||
physical = max(0, damage - energy_share)
|
physical = max(0, damage - energy_share)
|
||||||
damage = self._apply_dr(physical, weapon, target) + energy_share
|
damage = self._apply_dr(physical, weapon, target) + energy_share
|
||||||
return crit, False, self._apply_energy_resistance_per_component(
|
final = self._apply_energy_resistance_per_component(
|
||||||
damage, weapon, rolls, target=target
|
damage, weapon, rolls, target=target
|
||||||
)
|
)
|
||||||
|
# PF1e (CRB "Damage" — Minimum Damage): "If penalties reduce the
|
||||||
|
# damage result to less than 1, a hit still deals 1 point of
|
||||||
|
# nonlethal damage." The engine has no nonlethal damage track
|
||||||
|
# (documented approximation): the floor is applied as 1 point of
|
||||||
|
# normal damage on a landed hit.
|
||||||
|
return crit, False, max(_MINIMUM_HIT_DAMAGE, final)
|
||||||
|
|
||||||
def _attack_cover_bonus(
|
def _attack_cover_bonus(
|
||||||
self, attacker: CombatantState, target: CombatantState, *, ranged: bool
|
self, attacker: CombatantState, target: CombatantState, *, ranged: bool
|
||||||
|
|||||||
+26
-3
@@ -200,15 +200,38 @@ def test_dr_reduces_damage() -> None:
|
|||||||
assert b.hp == 4
|
assert b.hp == 4
|
||||||
|
|
||||||
|
|
||||||
def test_dr_floors_damage_at_zero() -> None:
|
def test_dr_floors_hit_at_one_minimum_damage() -> None:
|
||||||
|
# PF1e (CRB "Damage" - Minimum Damage): "If penalties reduce the damage
|
||||||
|
# result to less than 1, a hit still deals 1 point of nonlethal damage"
|
||||||
|
# (no nonlethal track: floored to 1 point of normal damage).
|
||||||
a_spec = make_combatant("a", attack_bonus=2)
|
a_spec = make_combatant("a", attack_bonus=2)
|
||||||
b_spec = make_combatant("b", dr=DamageReduction(amount=5))
|
b_spec = make_combatant("b", dr=DamageReduction(amount=5))
|
||||||
a = make_state(a_spec, "players", (1, 1))
|
a = make_state(a_spec, "players", (1, 1))
|
||||||
b = make_state(b_spec, "monsters", (1, 2))
|
b = make_state(b_spec, "monsters", (1, 2))
|
||||||
engine = make_engine([12, 3], [a, b])
|
engine = make_engine([12, 3], [a, b])
|
||||||
result = engine.resolve_attack(a, b, a_spec.attacks[0])
|
result = engine.resolve_attack(a, b, a_spec.attacks[0])
|
||||||
assert result.damage == 0
|
assert result.hit is True
|
||||||
assert b.hp == 6
|
assert result.damage == 1
|
||||||
|
assert b.hp == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_energy_resistance_floors_hit_at_one() -> None:
|
||||||
|
a_spec = make_combatant("a", attack_bonus=2)
|
||||||
|
b_spec = make_combatant("b").model_copy(update={"resistances": {"fire": 10}})
|
||||||
|
fire = AttackSpec(
|
||||||
|
id="a-fire",
|
||||||
|
name="flame blade",
|
||||||
|
kind="melee",
|
||||||
|
attack_bonus=2,
|
||||||
|
damage=[DamageComponent(formula=parse_dice("1d6"), types=["fire"])],
|
||||||
|
)
|
||||||
|
a = make_state(a_spec, "players", (1, 1))
|
||||||
|
b = make_state(b_spec, "monsters", (1, 2))
|
||||||
|
engine = make_engine([12, 5], [a, b])
|
||||||
|
result = engine.resolve_attack(a, b, fire)
|
||||||
|
assert result.hit is True
|
||||||
|
assert result.damage == 1
|
||||||
|
assert b.hp == 5
|
||||||
|
|
||||||
|
|
||||||
def test_dr_bypass_ignores_reduction() -> None:
|
def test_dr_bypass_ignores_reduction() -> None:
|
||||||
|
|||||||
@@ -251,12 +251,14 @@ def test_dr_spares_the_energy_component_of_a_mixed_attack() -> None:
|
|||||||
|
|
||||||
def test_physical_only_damage_vs_dr_unchanged() -> None:
|
def test_physical_only_damage_vs_dr_unchanged() -> None:
|
||||||
"""(h) a physical-only weapon keeps the exact old DR arithmetic (regression
|
"""(h) a physical-only weapon keeps the exact old DR arithmetic (regression
|
||||||
lock for the split): 1d8 = 3 vs DR 5 floors to 0."""
|
lock for the split): 1d8 = 3 vs DR 5 reduces below 1 — the CRB "Damage"
|
||||||
|
Minimum Damage floor then leaves a landed hit dealing 1 (nonlethal is not
|
||||||
|
modeled: 1 point of normal damage)."""
|
||||||
weapon = _attack("warhammer", (("1d8", ("bludgeoning",)),))
|
weapon = _attack("warhammer", (("1d8", ("bludgeoning",)),))
|
||||||
hero = _combatant("hero", attacks=(weapon,))
|
hero = _combatant("hero", attacks=(weapon,))
|
||||||
foe = _combatant("foe").model_copy(update={"dr": DamageReduction(amount=5)})
|
foe = _combatant("foe").model_copy(update={"dr": DamageReduction(amount=5)})
|
||||||
eng = _engine([hero, foe], [(0, 0), (0, 1)], ["players", "monsters"],
|
eng = _engine([hero, foe], [(0, 0), (0, 1)], ["players", "monsters"],
|
||||||
[15, 3])
|
[15, 3])
|
||||||
result = eng.resolve_attack(eng._states[0], eng._states[1], weapon)
|
result = eng.resolve_attack(eng._states[0], eng._states[1], weapon)
|
||||||
assert result.damage == 0
|
assert result.damage == 1
|
||||||
assert eng._states[1].hp == 100
|
assert eng._states[1].hp == 99 # the Minimum Damage floor lands 1 point
|
||||||
|
|||||||
Reference in New Issue
Block a user