fix(rules): Born Damned and Axe to Grind become documented no-ops (SRD: +2 saves vs curses has no curse descriptor; +1 damage vs foes threatened only by you is not expressible - old Fort-vs-poison and axe-damage bonuses were wrong)

This commit is contained in:
2026-09-19 10:41:21 +02:00
parent d0db208610
commit 6d0ee1bfeb
4 changed files with 50 additions and 15 deletions
+1
View File
@@ -128,6 +128,7 @@
}
],
"source": "fiches_personnages/misty_sheet.json",
"notes": "Traits : 'Born Damned' est un no-op documenté (+2 sauvegardes vs malédictions, descripteur 'curse' absent du moteur) ; 'Axe to Grind' est un no-op documenté (+1 dégâts vs ennemi menacé seulement par soi, condition non exprimable).",
"features": [
"Harmonic Spell",
"Weapon Finesse",
+15 -6
View File
@@ -259,11 +259,17 @@ WEAPON_FINESSE = AbilitySpec(
effects=(StatModifier(target="attack", value=0, condition="weapon_finesse"),),
)
# Axe to Grind: +1 trait bonus on damage with axes
# Axe to Grind (Basic Combat trait, APG): "+1 trait bonus on damage against
# foes who are threatened by only you." The "threatened by only you" gate
# needs the target's threat map at damage-modifier resolution time, which the
# StatModifier condition system does not carry (conditions are evaluated from
# weapon/range/save-descriptor context only) -> documented traced no-op. The
# previous "+1 damage with axes, condition='axe'" was the wrong effect (the
# SRD bonus is not tied to wielding an axe at all).
AXE_TO_GRIND = AbilitySpec(
name="Axe to Grind",
category="trait",
effects=(StatModifier(target="damage", value=1, condition="axe"),),
source_detail="trait (not modeled: +1 damage vs foes threatened only by you)",
)
# Spell Vulnerability (divination): -2 penalty on saves vs divination
@@ -276,13 +282,16 @@ SPELL_VULNERABILITY_DIVINATION = AbilitySpec(
),
)
# Born Damned (tiefling): darkness 1/day, +2 on saves vs poison
# Born Damned (Race trait, Tiefling): "You gain a +2 trait bonus on saving
# throws against curses and magical effects that produce curses." The engine
# has no "curse" save descriptor (curses are not modeled as spell effects),
# so the bonus has nothing to gate on -> documented traced no-op. The
# previous "+2 Fort vs poison" was the wrong effect (poison is not a curse).
# The tiefling darkness 1/day SLA is likewise not modeled.
BORN_DAMNED = AbilitySpec(
name="Born Damned (race: tiefling)",
category="racial",
effects=(
StatModifier(target="fort", value=2, condition="poison"),
),
source_detail="racial (not modeled: +2 saves vs curses; no curse descriptor)",
)
# Gifted Adept: +1 caster level for one spell
+15 -9
View File
@@ -9,7 +9,8 @@ Then: the modifier applies only when the runtime condition holds;
Supported condition families (plan B1):
- ``target_within_30ft_ranged`` — ranged-style weapon (or touch with
increment) fired at a target within 30 ft (Point-Blank Shot).
- ``axe`` — wielded weapon whose name contains "axe" (Axe to Grind).
- ``axe`` — wielded weapon whose name contains "axe" (engine capability; the
real Axe to Grind trait is a documented no-op, see test_feats.py).
- save descriptors (fear, cold, electricity, fey, divination, poison,
disease, mind_affecting) — the saving throw's context, threaded from the
fear aura and from spell effects/schools (Bitter, Lonely, Breeze-Kissed,
@@ -27,7 +28,6 @@ from typing import Literal
import pytest
from pf1e_simulator.abilities import (
AXE_TO_GRIND,
BITTER,
BORN_DAMNED,
BREEZE_KISSED,
@@ -190,9 +190,16 @@ def test_point_blank_shot_inactive_for_melee_weapons() -> None:
assert t.hp == 17 # damage 3, no +1
def test_axe_to_grind_applies_to_axe_weapon() -> None:
"""Axe to Grind: +1 damage with a weapon whose name contains 'axe'."""
hero = make_combatant("hero", weapon_name="Battleaxe", features=(AXE_TO_GRIND,))
_AXE_DAMAGE_PROBE = AbilitySpec(
name="Axe Damage Probe",
category="feat",
effects=(StatModifier(target="damage", value=1, condition="axe"),),
)
def test_axe_condition_applies_to_axe_weapon() -> None:
"""Engine 'axe' condition: +1 damage with a weapon whose name contains 'axe'."""
hero = make_combatant("hero", weapon_name="Battleaxe", features=(_AXE_DAMAGE_PROBE,))
foe = make_combatant("foe", ac_total=16, hp=20)
a = make_state(hero, "players", (1, 1))
t = make_state(foe, "monsters", (1, 2))
@@ -202,9 +209,9 @@ def test_axe_to_grind_applies_to_axe_weapon() -> None:
assert t.hp == 16 # 3 + 1 (axe)
def test_axe_to_grind_inactive_without_axe() -> None:
"""Axe to Grind: no bonus when the weapon is not an axe."""
hero = make_combatant("hero", weapon_name="Shortsword", features=(AXE_TO_GRIND,))
def test_axe_condition_inactive_without_axe() -> None:
"""Engine 'axe' condition: no bonus when the weapon is not an axe."""
hero = make_combatant("hero", weapon_name="Shortsword", features=(_AXE_DAMAGE_PROBE,))
foe = make_combatant("foe", ac_total=16, hp=20)
a = make_state(hero, "players", (1, 1))
t = make_state(foe, "monsters", (1, 2))
@@ -227,7 +234,6 @@ def test_axe_to_grind_inactive_without_axe() -> None:
(FEY_TOUCHED, "will", 19, frozenset({"fey"})),
(HALF_FORGOTTEN_SECRETS, "fort", 20, frozenset({"disease"})),
(HALF_FORGOTTEN_SECRETS, "will", 20, frozenset({"mind_affecting"})),
(BORN_DAMNED, "fort", 20, frozenset({"poison"})),
(PINGRE, "fort", 19, frozenset({"poison"})),
],
)
+19
View File
@@ -12,6 +12,8 @@ import json
from pathlib import Path
from pf1e_simulator.abilities import (
AXE_TO_GRIND,
BORN_DAMNED,
COMBAT_REFLEXES,
EXALTED_SOCIETY,
FORTIFIED_DRINKER,
@@ -253,3 +255,20 @@ def test_fortified_drinker_is_inert_documented_noop() -> None:
poison, condition='alcohol'" modifier is gone)."""
assert FORTIFIED_DRINKER.effects == ()
assert "not modeled" in FORTIFIED_DRINKER.source_detail
def test_born_damned_is_inert_documented_noop() -> None:
"""SRD (Tiefling race trait): +2 trait bonus on saves against curses. No
'curse' save descriptor exists -> inert no-op (old +2 Fort vs poison was
the wrong bonus entirely)."""
assert BORN_DAMNED.effects == ()
assert "not modeled" in BORN_DAMNED.source_detail
def test_axe_to_grind_is_inert_documented_noop() -> None:
"""SRD (APG combat trait): +1 trait bonus on damage against foes
threatened by only you. The threat-map gate is not expressible in the
StatModifier condition system -> inert no-op (old +1 damage with axes was
the wrong effect; the SRD bonus is not tied to axes)."""
assert AXE_TO_GRIND.effects == ()
assert "not modeled" in AXE_TO_GRIND.source_detail