414 lines
13 KiB
Python
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")
|