"""PF1e abilities: feats, class features, racial traits, character traits. All four categories share the same mechanical representation via ``AbilitySpec``. What differs is the ``category`` (for stacking and display) and ``source_detail``. This step defines passive feats — permanent stat modifiers that the engine collects alongside effects and conditions via ``_stat_modifiers``. Active feats (Power Attack, Rage) and conditional feats (Sneak Attack) will be added in later steps. Sources: PRPG Core Rulebook, Feats chapter. """ from __future__ import annotations from dataclasses import dataclass from typing import Literal from pf1e_simulator.effects import StatModifier Category = Literal["feat", "class", "racial", "trait"] @dataclass(frozen=True) class AbilitySpec: """One feat, class feature, racial trait, or character trait. ``effects`` are the StatModifiers this ability grants. For passive feats, these are always-on modifiers. For conditional feats, ``condition`` on each StatModifier gates applicability (evaluated by the engine). """ name: str category: Category source_detail: str = "" effects: tuple[StatModifier, ...] = () # ── Passive combat feats ───────────────────────────────────────────────────── # Weapon Focus: +1 attack with a specific weapon (untyped, CRB) def weapon_focus(weapon_name: str) -> AbilitySpec: return AbilitySpec( name=f"Weapon Focus ({weapon_name})", category="feat", source_detail="feat lv1", effects=( StatModifier(target="attack", value=1, weapon_filter=weapon_name), ), ) # Toughness: +1 HP per Hit Die (approximated as +level HP, untyped, CRB) # For simplicity at this stage, we model it as a flat HP bonus. # The engine doesn't yet compute per-HD bonuses, so the caller sets the value. def toughness(level: int) -> AbilitySpec: return AbilitySpec( name="Toughness", category="feat", source_detail="feat lv1", effects=( StatModifier(target="hp", value=level), ), ) # Iron Will: +2 will saves (untyped, CRB) IRON_WILL = AbilitySpec( name="Iron Will", category="feat", source_detail="feat lv1", effects=( StatModifier(target="will", value=2), ), ) # Great Fortitude: +2 fortitude saves (untyped, CRB) GREAT_FORTITUDE = AbilitySpec( name="Great Fortitude", category="feat", source_detail="feat lv1", effects=( StatModifier(target="fort", value=2), ), ) # Lightning Reflexes: +2 reflex saves (untyped, CRB) LIGHTNING_REFLEXES = AbilitySpec( name="Lightning Reflexes", category="feat", source_detail="feat lv1", effects=( StatModifier(target="ref", value=2), ), ) # Alertness: +2 Perception and Sense Motive (untyped, CRB) # Modeled as +2 will (Wisdom-based) since skills aren't implemented ALERTNESS = AbilitySpec( name="Alertness", category="feat", source_detail="feat lv1", effects=( StatModifier(target="will", value=2), ), ) # Point-Blank Shot: +1 attack and +1 damage with ranged weapons within 30 ft. # Untyped, conditional (CRB). POINT_BLANK_SHOT = AbilitySpec( name="Point-Blank Shot", category="feat", source_detail="feat lv1", effects=( StatModifier( target="attack", value=1, condition="target_within_30ft_ranged" ), StatModifier( target="damage", value=1, condition="target_within_30ft_ranged" ), ), ) # Precise Shot: no -4 penalty for shooting into melee (CRB) # This is a behavioral flag, not a stat modifier — it will be handled # as a SpecialBehavior in a later step. Defined here for completeness. PRECISE_SHOT = AbilitySpec( name="Precise Shot", category="feat", source_detail="feat lv1", ) # Dodge: +1 dodge bonus to AC (dodge type, stacks, CRB) DODGE = AbilitySpec( name="Dodge", category="feat", source_detail="feat lv1", effects=( StatModifier(target="ac", value=1, bonus_type="dodge"), ), ) # Power Attack: active feat — declare at turn start, -X atk on all melee # attack rolls (melee touch included) and +2X dmg on melee WEAPON damage only # (never touch), where X = BAB//4 + 1 (min 1). No passive modifiers; # the engine checks for this feat and the ``power_attack`` flag on state. POWER_ATTACK = AbilitySpec( name="Power Attack", category="feat", source_detail="feat lv1", ) # ── Additional feats from 8 PCs ──────────────────────────────────────────────── # Mounted Combat: +1 attack and damage while mounted, no penalty for charging (CRB) MOUNTED_COMBAT = AbilitySpec( name="Mounted Combat", category="feat", effects=( StatModifier(target="attack", value=1, condition="mounted"), StatModifier(target="damage", value=1, condition="mounted"), ), ) # Stealth Synergy: +2 bonus on Stealth checks when adjacent to ally with feat STEALTH_SYNERGY = AbilitySpec( name="Stealth Synergy", category="feat", effects=(StatModifier(target="stealth", value=2, condition="adjacent_ally_stealth_synergy"),), ) # Adopted: Gain a trait from another race ADOPTED = AbilitySpec(name="Adopted", category="trait") # Thoroughbred: +1 trait bonus on Ride and Handle Animal checks THOROUGHBRED = AbilitySpec( name="Thoroughbred", category="trait", effects=( StatModifier(target="ride", value=1), StatModifier(target="handle_animal", value=1), ), ) # Vagabond Child: +1 trait bonus on Sleight of Hand, always a class skill VAGABOND_CHILD = AbilitySpec( name="Vagabond Child", category="trait", effects=(StatModifier(target="sleight_of_hand", value=1),), ) # Whispering Wind: +1 trait bonus on Fly checks WHISPERING_WIND = AbilitySpec( name="Whispering Wind", category="trait", effects=(StatModifier(target="fly", value=1),), ) # Breeze-Kissed: +2 trait bonus on saves vs. cold and electricity BREEZE_KISSED = AbilitySpec( name="Breeze-Kissed", category="trait", effects=( StatModifier(target="fort", value=2, condition="cold"), StatModifier(target="ref", value=2, condition="electricity"), ), ) # Like the Wind: +10 ft movement speed LIKE_THE_WIND = AbilitySpec( name="Like the Wind", category="trait", effects=(StatModifier(target="speed_ft", value=10),), ) # Friendly Face: +1 trait bonus on Diplomacy checks FRIENDLY_FACE = AbilitySpec( name="Friendly Face", category="trait", effects=(StatModifier(target="diplomacy", value=1),), ) # 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 = AbilitySpec( name="Combat Reflexes", category="feat", source_detail="feat lv1", ) # Armor Proficiency (Light/Medium): no passive modifiers ARMOR_PROFICIENCY_LIGHT = AbilitySpec(name="Armor Proficiency (Light)", category="feat") ARMOR_PROFICIENCY_MEDIUM = AbilitySpec(name="Armor Proficiency (Medium)", category="feat") # Harmonic Spell: +1 caster level for one spell HARMONIC_SPELL = AbilitySpec(name="Harmonic Spell", category="feat") # Weapon Finesse: use Dex instead of Str for attack rolls with light weapons WEAPON_FINESSE = AbilitySpec( name="Weapon Finesse", category="feat", effects=(StatModifier(target="attack", value=0, condition="weapon_finesse"),), ) # Axe to Grind: +1 trait bonus on damage with axes AXE_TO_GRIND = AbilitySpec( name="Axe to Grind", category="trait", effects=(StatModifier(target="damage", value=1, condition="axe"),), ) # Spell Vulnerability (divination): -2 penalty on saves vs divination SPELL_VULNERABILITY_DIVINATION = AbilitySpec( name="Spell Vulnerability (divination; drawback)", category="trait", effects=( StatModifier(target="will", value=-2, condition="divination"), StatModifier(target="ref", value=-2, condition="divination"), ), ) # Born Damned (tiefling): darkness 1/day, +2 on saves vs poison BORN_DAMNED = AbilitySpec( name="Born Damned (race: tiefling)", category="racial", effects=( StatModifier(target="fort", value=2, condition="poison"), ), ) # Gifted Adept: +1 caster level for one spell GIFTED_ADEPT = AbilitySpec( name="Gifted Adept (dispel magic; magic)", category="trait", ) # Prehensile Tail: can retrieve small items as swift action PREHENSILE_TAIL = AbilitySpec( name="Prehensile Tail (tiefling daemon-spawn)", category="racial", ) # Scaled Skin (electricity): resist electricity 5 SCALED_SKIN_ELECTRICITY = AbilitySpec( name="Scaled Skin (electricity) (tiefling daemon-spawn)", category="racial", ) # Maw or Claw: bite attack 1d6 MAW_OR_CLAW = AbilitySpec( name="Maw or Claw (Bite) (tiefling daemon-spawn)", category="racial", ) # Brew Potion: can create potions BREW_POTION = AbilitySpec(name="Brew Potion", category="feat") # Craft Wondrous Item: can create wondrous items CRAFT_WONDROUS_ITEM = AbilitySpec(name="Craft Wondrous Item", category="feat") # Extra Hex: gain one additional hex EXTRA_HEX = AbilitySpec(name="Extra Hex", category="feat") # Eschew Materials: cast spells without material components ESCHEW_MATERIALS = AbilitySpec(name="Eschew Materials", category="feat") # Half-Forgotten Secrets (Dhampir): +2 on saves vs disease/mind-affecting HALF_FORGOTTEN_SECRETS = AbilitySpec( name="Half-Forgotten Secrets (Dhampir; Ajibachana)", category="trait", effects=( StatModifier(target="fort", value=2, condition="disease"), StatModifier(target="will", value=2, condition="mind_affecting"), ), ) # 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 = AbilitySpec(name="Summon Good Monster", category="feat") # Sacred Summons: summon good monsters as standard action SACRED_SUMS = AbilitySpec(name="Sacred Summons", category="feat") # Selective Channeling: exclude targets from channel energy SELECTIVE_CHANNELING = AbilitySpec(name="Selective Channeling", category="feat") # Extremely Fashionable: +4 trait bonus on Diplomacy checks EXTREMELY_FASHIONABLE = AbilitySpec( name="Extremely Fashionable", category="trait", effects=(StatModifier(target="diplomacy", value=4),), ) # Exalted of the Society: +1 on Knowledge (local) and (nobility) EXALTED_SOCIETY = AbilitySpec( name="Exalted of the Society (Cleric, Pathfinder Society)", category="trait", effects=( StatModifier(target="knowledge_local", value=1), StatModifier(target="knowledge_nobility", value=1), ), ) # Bitter: +1 trait bonus on saves vs fear BITTER = AbilitySpec( name="Bitter", category="trait", effects=(StatModifier(target="will", value=1, condition="fear"),), ) # Ambitious: +1 trait bonus on one Craft or Profession skill AMBITIOUS = AbilitySpec(name="Ambitious", category="trait") # Rapid Reload: reload firearms as free action RAPID_RELOAD = AbilitySpec(name="Rapid Reload (Pistol)", category="feat") # Gunsmithing: craft firearms and ammunition GUNSMITHING = AbilitySpec(name="Gunsmithing", category="feat") # Skill Focus (Perception): +3 Perception SKILL_FOCUS_PERCEPTION = AbilitySpec( name="Skill Focus (Perception)", category="feat", effects=(StatModifier(target="perception", value=3),), ) # Dazzling Display: intimidate as standard action DAZZLING_DISPLAY = AbilitySpec(name="Dazzling Display", category="feat") # Fortified Drinker: +2 on saves vs poison when drinking alcohol FORTIFIED_DRINKER = AbilitySpec( name="Fortified Drinker (Cayden Cailean)", category="trait", effects=( StatModifier(target="fort", value=2, condition="alcohol"), ), ) # Fate's Favored: +1 luck bonus on all luck bonuses FATES_FAVORED = AbilitySpec(name="Fate's Favored", category="trait") # Lonely: +1 trait bonus on Will saves vs fear LONELY = AbilitySpec( name="Lonely", category="trait", effects=(StatModifier(target="will", value=1, condition="fear"),), ) # Larger Than Life: +1 trait bonus on Intimidate LARGER_THAN_LIFE = AbilitySpec( name="Larger Than Life", category="trait", effects=(StatModifier(target="intimidate", value=1),), ) # Focused Study: +2 on one skill FOCUSED_STUDY = AbilitySpec(name="Focused Study", category="trait")