feat(loaders): map 8-PC feats
This commit is contained in:
@@ -147,3 +147,267 @@ POWER_ATTACK = AbilitySpec(
|
||||
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")
|
||||
|
||||
@@ -255,7 +255,7 @@ class Action:
|
||||
]
|
||||
target_id: str | None = None
|
||||
spell_name: str | None = None
|
||||
maneuver: str | None = None # "trip", "disarm", "bull_rush", "sunder"
|
||||
maneuver: str | None = None # "trip", "disarm", "bull_rush", "sunder", "grapple", "escape"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -915,7 +915,7 @@ class CombatEngine:
|
||||
if self.has_condition_flag(state, "cant_attack"):
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: wait (cannot attack)")
|
||||
return
|
||||
if maneuver not in ("trip", "disarm", "bull_rush", "sunder"):
|
||||
if maneuver not in ("trip", "disarm", "bull_rush", "sunder", "grapple", "escape"):
|
||||
return
|
||||
|
||||
# Calculate CMB and CMD
|
||||
@@ -974,11 +974,47 @@ class CombatEngine:
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: sunder vs {target.combatant.id} d20={roll}+{attacker_cmb}={total} CMD {target_cmd} -> SUCCESS ({dmg} damage to weapon)")
|
||||
else:
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: sunder vs {target.combatant.id} d20={roll}+{attacker_cmb}={total} CMD {target_cmd} -> FAILED")
|
||||
elif maneuver == "grapple":
|
||||
if success:
|
||||
# Initiate grapple
|
||||
target.active_conditions.append(
|
||||
ActiveCondition(
|
||||
condition=CONDITIONS_BY_NAME["grappled"],
|
||||
rounds_left=10, # Lasts until broken
|
||||
source=f"maneuver: grapple by {state.combatant.id}",
|
||||
)
|
||||
)
|
||||
target.conditions.append(CONDITIONS_BY_NAME["grappled"])
|
||||
target.grappled_with = state.combatant.id
|
||||
state.grappled_with = target.combatant.id
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: grapple vs {target.combatant.id} d20={roll}+{attacker_cmb}={total} CMD {target_cmd} -> SUCCESS (grappled)")
|
||||
else:
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: grapple vs {target.combatant.id} d20={roll}+{attacker_cmb}={total} CMD {target_cmd} -> FAILED")
|
||||
elif maneuver == "escape":
|
||||
# Escape uses CMB or Escape Artist skill
|
||||
escape_bonus = max(
|
||||
state.combatant.cmb or (state.combatant.bab + state.combatant.abilities.str_mod + self._size_mod(state)),
|
||||
self.resolve_skill(state, "escape_artist", 0).total if hasattr(state.combatant, 'skills') and 'escape_artist' in state.combatant.skills else 0
|
||||
)
|
||||
target_cmb = target.combatant.cmb or (target.combatant.bab + target.combatant.abilities.str_mod + self._size_mod(target))
|
||||
target_cmd = 10 + target_cmb + target.combatant.abilities.dex_mod
|
||||
roll = self._rng.d20()
|
||||
total = roll + escape_bonus
|
||||
success = roll != 1 and total >= target_cmd
|
||||
if success:
|
||||
# Remove grappled condition from both
|
||||
target.active_conditions = [ac for ac in target.active_conditions if ac.condition.name != "grappled"]
|
||||
target.conditions = [c for c in target.conditions if c.name != "grappled"]
|
||||
state.active_conditions = [ac for ac in state.active_conditions if ac.condition.name != "grappled"]
|
||||
state.conditions = [c for c in state.conditions if c.name != "grappled"]
|
||||
target.grappled_with = None
|
||||
state.grappled_with = None
|
||||
else:
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: escape vs {target.combatant.id} d20={roll}+{escape_bonus}={total} CMD {target_cmd} -> FAILED")
|
||||
|
||||
def _execute_attack(
|
||||
self, state: CombatantState, target: CombatantState | None, kind: str
|
||||
) -> None:
|
||||
if target is None or not target.active:
|
||||
return
|
||||
if self.has_condition_flag(state, "cant_attack"):
|
||||
self._log(f"round {self._current_round} {state.combatant.id}: wait (cannot attack)")
|
||||
return
|
||||
|
||||
@@ -204,6 +204,50 @@ DAZED = Condition(
|
||||
cant_act=True,
|
||||
)
|
||||
|
||||
# Asleep/Unconscious: helpless, can't act, -4 AC vs melee, no Dex to AC (CRB)
|
||||
ASLEEP = Condition(
|
||||
name="asleep",
|
||||
modifiers=(
|
||||
StatModifier(target="ac", value=-4),
|
||||
),
|
||||
denies_dex_to_ac=True,
|
||||
cant_act=True,
|
||||
cant_move=True,
|
||||
)
|
||||
|
||||
|
||||
# Grappled: -4 Dex, -2 attack, no movement, no AoO, can't cast without concentration (CRB)
|
||||
GRAPPLED = Condition(
|
||||
name="grappled",
|
||||
modifiers=(
|
||||
StatModifier(target="attack", value=-2),
|
||||
StatModifier(target="ac", value=-2), # -4 Dex = -2 AC
|
||||
StatModifier(target="ref", value=-2), # -4 Dex = -2 Ref
|
||||
StatModifier(target="dex_skills", value=-4), # -4 to Dex skills
|
||||
),
|
||||
cant_move=True,
|
||||
cant_charge=True,
|
||||
cant_aoo=True,
|
||||
denies_dex_to_ac=False, # Doesn't lose Dex to AC, but -4 Dex penalty applies
|
||||
)
|
||||
|
||||
|
||||
# Pinned: -4 Dex, -2 attack, no movement, can't cast, -4 AC vs melee (CRB)
|
||||
PINNED = Condition(
|
||||
name="pinned",
|
||||
modifiers=(
|
||||
StatModifier(target="attack", value=-2),
|
||||
StatModifier(target="ac", value=-4),
|
||||
StatModifier(target="ref", value=-2),
|
||||
StatModifier(target="dex_skills", value=-4),
|
||||
),
|
||||
cant_move=True,
|
||||
cant_charge=True,
|
||||
cant_aoo=True,
|
||||
denies_dex_to_ac=True,
|
||||
)
|
||||
|
||||
|
||||
# Staggered: only single move or standard action, no full-round (CRB)
|
||||
STAGGERED = Condition(
|
||||
name="staggered",
|
||||
@@ -216,5 +260,6 @@ CONDITIONS_BY_NAME: dict[str, Condition] = {
|
||||
SHAKEN, SICKENED, FATIGUED, EXHAUSTED, ENTANGLED, DAZZLED,
|
||||
BLINDED, DEAFENED, FLAT_FOOTED, PRONE, STUNNED, PARALYZED,
|
||||
NAUSEATED, DAZED, STAGGERED, FRIGHTENED, PANICKED, COWERING,
|
||||
ASLEEP, GRAPPLED, PINNED,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -26,14 +26,56 @@ if TYPE_CHECKING:
|
||||
from pf1e_simulator.loaders.foundry import Json
|
||||
|
||||
from pf1e_simulator.abilities import (
|
||||
ADOPTED,
|
||||
ALERTNESS,
|
||||
ARMOR_PROFICIENCY_LIGHT,
|
||||
ARMOR_PROFICIENCY_MEDIUM,
|
||||
AXE_TO_GRIND,
|
||||
BITTER,
|
||||
BORN_DAMNED,
|
||||
BREEZE_KISSED,
|
||||
BREW_POTION,
|
||||
COMBAT_REFLEXES,
|
||||
CRAFT_WONDROUS_ITEM,
|
||||
DAZZLING_DISPLAY,
|
||||
DODGE,
|
||||
EXTRA_HEX,
|
||||
ESCHEW_MATERIALS,
|
||||
EXALTED_SOCIETY,
|
||||
EXTREMELY_FASHIONABLE,
|
||||
FATES_FAVORED,
|
||||
FEY_TOUCHED,
|
||||
FOCUSED_STUDY,
|
||||
FORTIFIED_DRINKER,
|
||||
FRIENDLY_FACE,
|
||||
GIFTED_ADEPT,
|
||||
GREAT_FORTITUDE,
|
||||
GUNSMITHING,
|
||||
HARMONIC_SPELL,
|
||||
IRON_WILL,
|
||||
LIKE_THE_WIND,
|
||||
LIGHTNING_REFLEXES,
|
||||
LONELY,
|
||||
LARGER_THAN_LIFE,
|
||||
MAW_OR_CLAW,
|
||||
MOUNTED_COMBAT,
|
||||
POINT_BLANK_SHOT,
|
||||
POWER_ATTACK,
|
||||
PRECISE_SHOT,
|
||||
PREHENSILE_TAIL,
|
||||
RAPID_RELOAD,
|
||||
SACRED_SUMS,
|
||||
SELECTIVE_CHANNELING,
|
||||
SKILL_FOCUS_PERCEPTION,
|
||||
SPELL_VULNERABILITY_DIVINATION,
|
||||
STEALTH_SYNERGY,
|
||||
SUMMON_GOOD_MONSTER,
|
||||
THOROUGHBRED,
|
||||
VAGABOND_CHILD,
|
||||
WHISPERING_WIND,
|
||||
VAGABOND_CHILD,
|
||||
WHISPERING_WIND,
|
||||
WEAPON_FINESSE,
|
||||
AbilitySpec,
|
||||
toughness,
|
||||
weapon_focus,
|
||||
|
||||
Reference in New Issue
Block a user