From 13e4a2774ef904bbf012802a10111f05741a002f Mon Sep 17 00:00:00 2001 From: ctan Date: Sat, 19 Sep 2026 12:47:40 +0200 Subject: [PATCH] fix(rules): cover the feat/trait no-op batch in test_feats (assert the eight documented inert specs, the two traced-noop names, and misty's resistances/bite data encoding) --- tests/test_feats.py | 69 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/test_feats.py b/tests/test_feats.py index dad7ac0..808d4cf 100644 --- a/tests/test_feats.py +++ b/tests/test_feats.py @@ -12,17 +12,23 @@ import json from pathlib import Path from pf1e_simulator.abilities import ( + AMBITIOUS, AXE_TO_GRIND, BITTER, BORN_DAMNED, + BREEZE_KISSED, COMBAT_REFLEXES, EXALTED_SOCIETY, EXTREMELY_FASHIONABLE, + FOCUSED_STUDY, FORTIFIED_DRINKER, FRIENDLY_FACE, + LARGER_THAN_LIFE, + LIKE_THE_WIND, LONELY, MOUNTED_COMBAT, POINT_BLANK_SHOT, + WHISPERING_WIND, AbilitySpec, ) from pf1e_simulator.combat import CombatantState, CombatEngine @@ -305,6 +311,69 @@ def test_extremely_fashionable_is_inert_documented_noop() -> None: assert "not modeled" in EXTREMELY_FASHIONABLE.source_detail +# ── SRD conformance: feat/trait divergence batch (size/speed/skill gates) ───── + + +def test_larger_than_life_is_inert_documented_noop() -> None: + """SRD (Basic Combat trait): considered one size larger for Intimidate + while wielding a firearm. Intimidate and size-for-skill are not modeled + -> inert no-op (the old flat +1 Intimidate was the wrong effect).""" + assert LARGER_THAN_LIFE.effects == () + assert "not modeled" in LARGER_THAN_LIFE.source_detail + + +def test_ambitious_is_inert_documented_noop() -> None: + """SRD (Basic Social trait): +4 trait bonus on Diplomacy vs creatures + with 5+ more HD. The HD-differential gate is not expressible -> inert.""" + assert AMBITIOUS.effects == () + assert "not modeled" in AMBITIOUS.source_detail + + +def test_focused_study_is_inert_documented_noop() -> None: + """SRD (human alternate racial trait): +2 on one chosen skill (re-chosen + at 8th/16th). The chosen skill is never resolved in combat -> inert.""" + assert FOCUSED_STUDY.effects == () + assert "not modeled" in FOCUSED_STUDY.source_detail + + +def test_esha_sylph_traits_are_inert_documented_noops() -> None: + """SRD (ARG sylph alternate racial traits; Esha's sheet declares race + Sylph so the picks are legal): Whispering Wind = +4 racial Stealth, + Like the Wind = +5 ft base speed, Breeze-Kissed = +2 racial AC vs + nonmagical ranged + 1/day gust. None is expressible (no stealth checks, + speed_ft modifiers unresolved, AC is a final sheet value) -> all inert; + the previously coded Fly/save/speed bonuses were fabricated.""" + for spec in (WHISPERING_WIND, LIKE_THE_WIND, BREEZE_KISSED): + assert spec.effects == (), spec.name + assert "not modeled" in spec.source_detail, spec.name + + +def test_fey_touched_and_pingre_are_traced_noop_names() -> None: + """Not PF1e features (no SRD entry under those names): Harvie's + "Fey-touched" and Nairda's joke trait "Pingre" join the decorative + traced-noop list; their old coded bonuses were fabricated.""" + assert "fey-touched" in _NOOP_FEATS + assert "pingre" in _NOOP_FEATS + for name in ("Fey-touched", "Pingre"): + spec = _resolve_feat(name, 1) + assert spec.name == name + assert spec.effects == () + + +def test_misty_scaled_skin_and_maw_or_claw_encoded_in_data() -> None: + """SRD (ARG tiefling daemon-spawn): Scaled Skin (electricity) = resist + electricity 5 (replaces fiendish resistance - no cold/fire); Maw or Claw + = bite natural attack, 1d6 for a Medium tiefling (the Foundry sheet's + "1d4 estimé" is an author guess, discrepancy recorded in the JSON notes). + Both benefits are Combatant data, not stat modifiers.""" + misty = load_combatant(Path("data/pcs/misty.json")) + assert misty.resistances == {"electricity": 5} + bites = [a for a in misty.attacks if a.name.startswith("Bite")] + assert len(bites) == 1 + formula = bites[0].damage[0].formula + assert (formula.count, formula.sides, formula.bonus) == (1, 6, 0) + + def _make_battle_combatant( cid: str, *, features: tuple[AbilitySpec, ...] = (), hp_max: int = 30 ) -> Combatant: