Files
pf1e-simulator/src/pf1e_simulator/abilities.py
T
2026-09-04 23:02:18 +02:00

414 lines
13 KiB
Python

"""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",
)
# ── 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")