"""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 / +2X dmg on all # melee attacks this turn, 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", )