fix(rules): Esha sylph traits and invented names to documented traced no-ops (Whispering Wind/Like the Wind/Breeze-Kissed are real sylph racial traits whose SRD effects the engine cannot resolve - the coded bonuses were fabricated; Fey-touched and Pingre are not PF1e features and move to _NOOP_FEATS; descriptor gating stays covered via local probe specs)
This commit is contained in:
@@ -198,28 +198,35 @@ VAGABOND_CHILD = AbilitySpec(
|
|||||||
effects=(StatModifier(target="sleight_of_hand", value=1),),
|
effects=(StatModifier(target="sleight_of_hand", value=1),),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Whispering Wind: +1 trait bonus on Fly checks
|
# Whispering Wind (Sylph alternate racial trait, ARG): "+4 racial bonus on
|
||||||
|
# Stealth checks" (replaces the sylph spell-like ability). Esha's sheet
|
||||||
|
# declares race Sylph, so the pick is legal; the engine resolves no Stealth
|
||||||
|
# checks -> documented traced no-op.
|
||||||
WHISPERING_WIND = AbilitySpec(
|
WHISPERING_WIND = AbilitySpec(
|
||||||
name="Whispering Wind",
|
name="Whispering Wind",
|
||||||
category="trait",
|
category="racial",
|
||||||
effects=(StatModifier(target="fly", value=1),),
|
source_detail="racial sylph (not modeled: +4 racial Stealth, no stealth checks)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Breeze-Kissed: +2 trait bonus on saves vs. cold and electricity
|
# Breeze-Kissed (Sylph alternate racial trait, ARG): "+2 racial bonus to AC
|
||||||
|
# against nonmagical ranged attacks" plus a 1/day gust bull rush or trip
|
||||||
|
# (replaces air affinity). No magical/nonmagical AC filter exists (AC is a
|
||||||
|
# final sheet value) and the 1/day maneuver is not selectable -> documented
|
||||||
|
# traced no-op.
|
||||||
BREEZE_KISSED = AbilitySpec(
|
BREEZE_KISSED = AbilitySpec(
|
||||||
name="Breeze-Kissed",
|
name="Breeze-Kissed",
|
||||||
category="trait",
|
category="racial",
|
||||||
effects=(
|
source_detail="racial sylph (not modeled: +2 AC vs nonmagical ranged, 1/day gust)",
|
||||||
StatModifier(target="fort", value=2, condition="cold"),
|
|
||||||
StatModifier(target="ref", value=2, condition="electricity"),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Like the Wind: +10 ft movement speed
|
# Like the Wind (Sylph alternate racial trait, ARG): "+5 foot bonus to base
|
||||||
|
# speed" (replaces energy resistance). The engine never resolves "speed_ft"
|
||||||
|
# stat modifiers (movement reads the final JSON speed), and Esha's sheet
|
||||||
|
# carries the final land speed 25 -> documented traced no-op.
|
||||||
LIKE_THE_WIND = AbilitySpec(
|
LIKE_THE_WIND = AbilitySpec(
|
||||||
name="Like the Wind",
|
name="Like the Wind",
|
||||||
category="trait",
|
category="racial",
|
||||||
effects=(StatModifier(target="speed_ft", value=10),),
|
source_detail="racial sylph (not modeled: +5 ft base speed, speed modifiers unresolved)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Friendly Face (feat, Animal Archive): "Other animals of roughly the same
|
# Friendly Face (feat, Animal Archive): "Other animals of roughly the same
|
||||||
@@ -232,15 +239,6 @@ FRIENDLY_FACE = AbilitySpec(
|
|||||||
source_detail="feat (not modeled: animal initial attitude +2 steps)",
|
source_detail="feat (not modeled: animal initial attitude +2 steps)",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fey-touched: +1 trait bonus on saves vs. fey effects
|
|
||||||
FEY_TOUCHED = AbilitySpec(
|
|
||||||
name="Fey-touched",
|
|
||||||
category="trait",
|
|
||||||
effects=(
|
|
||||||
StatModifier(target="will", value=1, condition="fey"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Combat Reflexes: additional AoO per round equal to Dex mod
|
# Combat Reflexes: additional AoO per round equal to Dex mod
|
||||||
COMBAT_REFLEXES = AbilitySpec(
|
COMBAT_REFLEXES = AbilitySpec(
|
||||||
name="Combat Reflexes",
|
name="Combat Reflexes",
|
||||||
@@ -343,16 +341,6 @@ HALF_FORGOTTEN_SECRETS = AbilitySpec(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pingre: +1 trait bonus on Appraise and saves vs poison
|
|
||||||
PINGRE = AbilitySpec(
|
|
||||||
name="Pingre",
|
|
||||||
category="trait",
|
|
||||||
effects=(
|
|
||||||
StatModifier(target="appraise", value=1),
|
|
||||||
StatModifier(target="fort", value=1, condition="poison"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Summon Good Monster: expanded summon list
|
# Summon Good Monster: expanded summon list
|
||||||
SUMMON_GOOD_MONSTER = AbilitySpec(name="Summon Good Monster", category="feat")
|
SUMMON_GOOD_MONSTER = AbilitySpec(name="Summon Good Monster", category="feat")
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ from pf1e_simulator.abilities import (
|
|||||||
EXTRA_HEX,
|
EXTRA_HEX,
|
||||||
EXTREMELY_FASHIONABLE,
|
EXTREMELY_FASHIONABLE,
|
||||||
FATES_FAVORED,
|
FATES_FAVORED,
|
||||||
FEY_TOUCHED,
|
|
||||||
FOCUSED_STUDY,
|
FOCUSED_STUDY,
|
||||||
FORTIFIED_DRINKER,
|
FORTIFIED_DRINKER,
|
||||||
FRIENDLY_FACE,
|
FRIENDLY_FACE,
|
||||||
@@ -62,7 +61,6 @@ from pf1e_simulator.abilities import (
|
|||||||
LONELY,
|
LONELY,
|
||||||
MAW_OR_CLAW,
|
MAW_OR_CLAW,
|
||||||
MOUNTED_COMBAT,
|
MOUNTED_COMBAT,
|
||||||
PINGRE,
|
|
||||||
POINT_BLANK_SHOT,
|
POINT_BLANK_SHOT,
|
||||||
POWER_ATTACK,
|
POWER_ATTACK,
|
||||||
PRECISE_SHOT,
|
PRECISE_SHOT,
|
||||||
@@ -110,7 +108,6 @@ _KNOWN_FEATS: dict[str, AbilitySpec] = {
|
|||||||
"breeze-kissed": BREEZE_KISSED,
|
"breeze-kissed": BREEZE_KISSED,
|
||||||
"like the wind": LIKE_THE_WIND,
|
"like the wind": LIKE_THE_WIND,
|
||||||
"friendly face": FRIENDLY_FACE,
|
"friendly face": FRIENDLY_FACE,
|
||||||
"fey-touched": FEY_TOUCHED,
|
|
||||||
"combat reflexes": COMBAT_REFLEXES,
|
"combat reflexes": COMBAT_REFLEXES,
|
||||||
"armor proficiency (light)": ARMOR_PROFICIENCY_LIGHT,
|
"armor proficiency (light)": ARMOR_PROFICIENCY_LIGHT,
|
||||||
"armor proficiency (medium)": ARMOR_PROFICIENCY_MEDIUM,
|
"armor proficiency (medium)": ARMOR_PROFICIENCY_MEDIUM,
|
||||||
@@ -128,7 +125,6 @@ _KNOWN_FEATS: dict[str, AbilitySpec] = {
|
|||||||
"extra hex": EXTRA_HEX,
|
"extra hex": EXTRA_HEX,
|
||||||
"eschew materials": ESCHEW_MATERIALS,
|
"eschew materials": ESCHEW_MATERIALS,
|
||||||
"half-forgotten secrets (dhampir; ajibachana)": HALF_FORGOTTEN_SECRETS,
|
"half-forgotten secrets (dhampir; ajibachana)": HALF_FORGOTTEN_SECRETS,
|
||||||
"pingre": PINGRE,
|
|
||||||
"summon good monster": SUMMON_GOOD_MONSTER,
|
"summon good monster": SUMMON_GOOD_MONSTER,
|
||||||
"sacred summons": SACRED_SUMS,
|
"sacred summons": SACRED_SUMS,
|
||||||
"selective channeling": SELECTIVE_CHANNELING,
|
"selective channeling": SELECTIVE_CHANNELING,
|
||||||
@@ -147,13 +143,18 @@ _KNOWN_FEATS: dict[str, AbilitySpec] = {
|
|||||||
"focused study (trait racial humain \u2014 remplace le bonus feat)": FOCUSED_STUDY,
|
"focused study (trait racial humain \u2014 remplace le bonus feat)": FOCUSED_STUDY,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Feat names with no combat-mechanical definition (flavor / race descriptors / jokes).
|
# Feat names with no combat-mechanical definition (flavor / race descriptors /
|
||||||
# These are resolved to traced no-ops so the loader never silently drops a name.
|
# jokes). These are resolved to traced no-ops so the loader never silently
|
||||||
|
# drops a name. "Fey-touched" (Harvie's sheet) and "Pingre" (Nairda's sheet,
|
||||||
|
# French for "miser") are not PF1e features; the SRD has no entry under those
|
||||||
|
# names, so no effect can be attributed to them.
|
||||||
_NOOP_FEATS: frozenset[str] = frozenset({
|
_NOOP_FEATS: frozenset[str] = frozenset({
|
||||||
"6. vert - yes mistress",
|
"6. vert - yes mistress",
|
||||||
"animal (race)",
|
"animal (race)",
|
||||||
"drake (race)",
|
"drake (race)",
|
||||||
|
"fey-touched",
|
||||||
"orange - lazy ass",
|
"orange - lazy ass",
|
||||||
|
"pingre",
|
||||||
"plot armor",
|
"plot armor",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ Supported condition families (plan B1):
|
|||||||
real Axe to Grind trait is a documented no-op, see test_feats.py).
|
real Axe to Grind trait is a documented no-op, see test_feats.py).
|
||||||
- save descriptors (fear, cold, electricity, fey, divination, poison,
|
- save descriptors (fear, cold, electricity, fey, divination, poison,
|
||||||
disease, mind_affecting) — the saving throw's context, threaded from the
|
disease, mind_affecting) — the saving throw's context, threaded from the
|
||||||
fear aura and from spell effects/schools (Breeze-Kissed, Fey-touched,
|
fear aura and from spell effects/schools (real feats: Spell Vulnerability,
|
||||||
Spell Vulnerability, Pingre, Half-Forgotten Secrets, Lonely's -2 vs
|
Half-Forgotten Secrets, Lonely's -2 vs mind_affecting standing in for
|
||||||
mind_affecting standing in for charm).
|
charm; the remaining descriptors are exercised through the local probe
|
||||||
|
specs below).
|
||||||
- Unsupported (mounted, adjacent_ally_stealth_synergy, weapon_finesse,
|
- Unsupported (mounted, adjacent_ally_stealth_synergy, weapon_finesse,
|
||||||
alcohol) and unknown names always evaluate to False.
|
alcohol) and unknown names always evaluate to False.
|
||||||
"""
|
"""
|
||||||
@@ -28,13 +29,10 @@ from typing import Literal
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pf1e_simulator.abilities import (
|
from pf1e_simulator.abilities import (
|
||||||
BREEZE_KISSED,
|
|
||||||
FEY_TOUCHED,
|
|
||||||
FORTIFIED_DRINKER,
|
FORTIFIED_DRINKER,
|
||||||
HALF_FORGOTTEN_SECRETS,
|
HALF_FORGOTTEN_SECRETS,
|
||||||
LONELY,
|
LONELY,
|
||||||
MOUNTED_COMBAT,
|
MOUNTED_COMBAT,
|
||||||
PINGRE,
|
|
||||||
POINT_BLANK_SHOT,
|
POINT_BLANK_SHOT,
|
||||||
SPELL_VULNERABILITY_DIVINATION,
|
SPELL_VULNERABILITY_DIVINATION,
|
||||||
AbilitySpec,
|
AbilitySpec,
|
||||||
@@ -221,16 +219,39 @@ def test_axe_condition_inactive_without_axe() -> None:
|
|||||||
|
|
||||||
# ── Save descriptor conditions ────────────────────────────────────────────────
|
# ── Save descriptor conditions ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Probe specs exercising the descriptor gating (the loader's real feats for
|
||||||
|
# these descriptors are documented no-ops, see test_feats.py).
|
||||||
|
_COLD_FORT_PROBE = AbilitySpec(
|
||||||
|
name="Cold Fort Probe",
|
||||||
|
category="feat",
|
||||||
|
effects=(StatModifier(target="fort", value=2, condition="cold"),),
|
||||||
|
)
|
||||||
|
_ELEC_REF_PROBE = AbilitySpec(
|
||||||
|
name="Electricity Ref Probe",
|
||||||
|
category="feat",
|
||||||
|
effects=(StatModifier(target="ref", value=2, condition="electricity"),),
|
||||||
|
)
|
||||||
|
_FEY_WILL_PROBE = AbilitySpec(
|
||||||
|
name="Fey Will Probe",
|
||||||
|
category="feat",
|
||||||
|
effects=(StatModifier(target="will", value=1, condition="fey"),),
|
||||||
|
)
|
||||||
|
_POISON_FORT_PROBE = AbilitySpec(
|
||||||
|
name="Poison Fort Probe",
|
||||||
|
category="feat",
|
||||||
|
effects=(StatModifier(target="fort", value=1, condition="poison"),),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("feat", "save_type", "dc", "descriptors"),
|
("feat", "save_type", "dc", "descriptors"),
|
||||||
[
|
[
|
||||||
(BREEZE_KISSED, "fort", 20, frozenset({"cold"})),
|
(_COLD_FORT_PROBE, "fort", 20, frozenset({"cold"})),
|
||||||
(BREEZE_KISSED, "ref", 20, frozenset({"electricity"})),
|
(_ELEC_REF_PROBE, "ref", 20, frozenset({"electricity"})),
|
||||||
(FEY_TOUCHED, "will", 19, frozenset({"fey"})),
|
(_FEY_WILL_PROBE, "will", 19, frozenset({"fey"})),
|
||||||
(HALF_FORGOTTEN_SECRETS, "fort", 20, frozenset({"disease"})),
|
(HALF_FORGOTTEN_SECRETS, "fort", 20, frozenset({"disease"})),
|
||||||
(HALF_FORGOTTEN_SECRETS, "will", 20, frozenset({"mind_affecting"})),
|
(HALF_FORGOTTEN_SECRETS, "will", 20, frozenset({"mind_affecting"})),
|
||||||
(PINGRE, "fort", 19, frozenset({"poison"})),
|
(_POISON_FORT_PROBE, "fort", 19, frozenset({"poison"})),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_conditioned_save_bonus_applies_when_descriptor_present(
|
def test_conditioned_save_bonus_applies_when_descriptor_present(
|
||||||
@@ -262,9 +283,9 @@ def test_lonely_is_a_negative_mind_affecting_penalty() -> None:
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("feat", "save_type", "dc", "descriptors"),
|
("feat", "save_type", "dc", "descriptors"),
|
||||||
[
|
[
|
||||||
(BREEZE_KISSED, "fort", 19, frozenset({"fear"})),
|
(_COLD_FORT_PROBE, "fort", 19, frozenset({"fear"})),
|
||||||
(HALF_FORGOTTEN_SECRETS, "will", 19, frozenset({"fear"})),
|
(HALF_FORGOTTEN_SECRETS, "will", 19, frozenset({"fear"})),
|
||||||
(PINGRE, "fort", 19, frozenset({"cold"})),
|
(_POISON_FORT_PROBE, "fort", 19, frozenset({"cold"})),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_conditioned_save_bonus_excluded_without_matching_descriptor(
|
def test_conditioned_save_bonus_excluded_without_matching_descriptor(
|
||||||
|
|||||||
Reference in New Issue
Block a user