diff --git a/data/pcs/misty.json b/data/pcs/misty.json index 3979722..22012fb 100644 --- a/data/pcs/misty.json +++ b/data/pcs/misty.json @@ -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", diff --git a/src/pf1e_simulator/abilities.py b/src/pf1e_simulator/abilities.py index d69500e..f7d3c99 100644 --- a/src/pf1e_simulator/abilities.py +++ b/src/pf1e_simulator/abilities.py @@ -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 diff --git a/tests/test_conditional_modifiers.py b/tests/test_conditional_modifiers.py index a83252e..d697b3b 100644 --- a/tests/test_conditional_modifiers.py +++ b/tests/test_conditional_modifiers.py @@ -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"})), ], ) diff --git a/tests/test_feats.py b/tests/test_feats.py index d3ee9ac..9dd8d51 100644 --- a/tests/test_feats.py +++ b/tests/test_feats.py @@ -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