323 lines
10 KiB
Python
323 lines
10 KiB
Python
"""PF1e conditions: predefined stat modifiers and behavioral flags.
|
|
|
|
Each condition maps to a set of StatModifier effects (applied via
|
|
resolve_modifiers with PF1e stacking rules) and optional behavioral flags
|
|
that the engine checks at relevant resolution points.
|
|
|
|
Behavioral flags are declared here but only stat modifiers are integrated
|
|
into resolve_attack/resolve_save in this step. Flag enforcement (cant_act,
|
|
must_flee, etc.) will be added incrementally with spell/condition application.
|
|
|
|
Sources: PRPG Core Rulebook, Conditions chapter.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from pf1e_simulator.effects import StatModifier
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Condition:
|
|
"""A PF1e condition: stat modifiers + behavioral flags.
|
|
|
|
``modifiers`` are collected alongside transient ``effects`` by
|
|
``resolve_modifiers``. Behavioral flags are checked by the engine at
|
|
relevant points (e.g. ``cant_act`` in ``_take_turn``).
|
|
"""
|
|
|
|
name: str
|
|
modifiers: tuple[StatModifier, ...] = ()
|
|
denies_dex_to_ac: bool = False
|
|
cant_act: bool = False
|
|
cant_move: bool = False
|
|
cant_attack: bool = False
|
|
cant_charge: bool = False
|
|
cant_aoo: bool = False
|
|
must_flee: bool = False
|
|
speed_mult: float = 1.0
|
|
miss_chance: int = 0
|
|
|
|
|
|
# ── Fear conditions ──────────────────────────────────────────────────────────
|
|
# Shaken: -2 attack rolls, saving throws, skill checks, ability checks (CRB)
|
|
SHAKEN = Condition(
|
|
name="shaken",
|
|
modifiers=(
|
|
StatModifier(target="attack", value=-2),
|
|
StatModifier(target="fort", value=-2),
|
|
StatModifier(target="ref", value=-2),
|
|
StatModifier(target="will", value=-2),
|
|
),
|
|
)
|
|
|
|
# Frightened: shaken penalties + must flee from source of fear (CRB)
|
|
FRIGHTENED = Condition(
|
|
name="frightened",
|
|
modifiers=(
|
|
StatModifier(target="attack", value=-2),
|
|
StatModifier(target="fort", value=-2),
|
|
StatModifier(target="ref", value=-2),
|
|
StatModifier(target="will", value=-2),
|
|
),
|
|
must_flee=True,
|
|
)
|
|
|
|
# Panicked: drop items + flee at top speed + -2 saves (CRB)
|
|
PANICKED = Condition(
|
|
name="panicked",
|
|
modifiers=(
|
|
StatModifier(target="fort", value=-2),
|
|
StatModifier(target="ref", value=-2),
|
|
StatModifier(target="will", value=-2),
|
|
),
|
|
must_flee=True,
|
|
)
|
|
|
|
# Cowering: frozen in fear, can't act, -2 AC, loses Dex (CRB)
|
|
COWERING = Condition(
|
|
name="cowering",
|
|
modifiers=(
|
|
StatModifier(target="ac", value=-2),
|
|
),
|
|
denies_dex_to_ac=True,
|
|
cant_act=True,
|
|
)
|
|
|
|
# ── Physical conditions ─────────────────────────────────────────────────────
|
|
# Sickened: -2 attack, weapon damage, saving throws, skill/ability checks (CRB)
|
|
SICKENED = Condition(
|
|
name="sickened",
|
|
modifiers=(
|
|
StatModifier(target="attack", value=-2),
|
|
StatModifier(target="damage", value=-2),
|
|
StatModifier(target="fort", value=-2),
|
|
StatModifier(target="ref", value=-2),
|
|
StatModifier(target="will", value=-2),
|
|
),
|
|
)
|
|
|
|
# Fatigued: -2 STR, -2 DEX; can't run or charge (CRB)
|
|
# Approximation: -1 attack (STR/DEX), -1 damage (STR)
|
|
FATIGUED = Condition(
|
|
name="fatigued",
|
|
modifiers=(
|
|
StatModifier(target="attack", value=-1),
|
|
StatModifier(target="damage", value=-1),
|
|
),
|
|
cant_charge=True,
|
|
)
|
|
|
|
# Exhausted: -6 STR, -6 DEX; half speed; can't run or charge (CRB)
|
|
# Approximation: -3 attack, -3 damage
|
|
EXHAUSTED = Condition(
|
|
name="exhausted",
|
|
modifiers=(
|
|
StatModifier(target="attack", value=-3),
|
|
StatModifier(target="damage", value=-3),
|
|
),
|
|
cant_charge=True,
|
|
speed_mult=0.5,
|
|
)
|
|
|
|
# Entangled: -2 attack, -4 DEX; half speed; can't run or charge (CRB)
|
|
# Approximation: -2 attack, -2 AC (from -4 DEX), -2 ref (from -4 DEX)
|
|
ENTANGLED = Condition(
|
|
name="entangled",
|
|
modifiers=(
|
|
StatModifier(target="attack", value=-2),
|
|
StatModifier(target="ac", value=-2),
|
|
StatModifier(target="ref", value=-2),
|
|
),
|
|
cant_charge=True,
|
|
speed_mult=0.5,
|
|
)
|
|
|
|
# ── Sensory conditions ──────────────────────────────────────────────────────
|
|
# Dazzled: -1 attack rolls (CRB)
|
|
DAZZLED = Condition(
|
|
name="dazzled",
|
|
modifiers=(
|
|
StatModifier(target="attack", value=-1),
|
|
),
|
|
)
|
|
|
|
# Blinded: -2 AC, loses Dex, -4 Dexterity skill checks; NO flat attack
|
|
# penalty (the -2 was a non-RAW double penalty on top of the engine's
|
|
# modeled 50% total-concealment miss). The Str-skill half of the -4 is not
|
|
# modeled (no str_skills target). (CRB)
|
|
BLINDED = Condition(
|
|
name="blinded",
|
|
modifiers=(
|
|
StatModifier(target="ac", value=-2),
|
|
StatModifier(target="dex_skills", value=-4),
|
|
),
|
|
denies_dex_to_ac=True,
|
|
)
|
|
|
|
# Deafened: -4 initiative, 20% spell failure (verbal) (CRB)
|
|
DEAFENED = Condition(
|
|
name="deafened",
|
|
modifiers=(
|
|
StatModifier(target="initiative", value=-4),
|
|
),
|
|
)
|
|
|
|
# ── Positional conditions ────────────────────────────────────────────────────
|
|
# Flat-footed: loses Dex to AC, can't make AoO (CRB)
|
|
FLAT_FOOTED = Condition(
|
|
name="flat_footed",
|
|
denies_dex_to_ac=True,
|
|
cant_aoo=True,
|
|
)
|
|
|
|
# Prone: -4 melee attack, +4 AC vs ranged, -4 AC vs melee (CRB)
|
|
PRONE = Condition(
|
|
name="prone",
|
|
)
|
|
|
|
# ── Incapacitating conditions ────────────────────────────────────────────────
|
|
# Stunned: can't act, -2 AC, loses Dex (CRB)
|
|
STUNNED = Condition(
|
|
name="stunned",
|
|
modifiers=(
|
|
StatModifier(target="ac", value=-2),
|
|
),
|
|
denies_dex_to_ac=True,
|
|
cant_act=True,
|
|
)
|
|
|
|
# Paralyzed: can't move or act, effective STR/DEX = 0, helpless (CRB)
|
|
PARALYZED = Condition(
|
|
name="paralyzed",
|
|
denies_dex_to_ac=True,
|
|
cant_act=True,
|
|
cant_move=True,
|
|
)
|
|
|
|
# Nauseated: can't attack/cast/concentrate; only single move action (CRB)
|
|
NAUSEATED = Condition(
|
|
name="nauseated",
|
|
cant_attack=True,
|
|
)
|
|
|
|
# Dazed: can't act, no AC penalty (CRB)
|
|
DAZED = Condition(
|
|
name="dazed",
|
|
cant_act=True,
|
|
)
|
|
|
|
# Asleep/Unconscious: helpless (CRB) — can't act, no Dex to AC. The old flat
|
|
# -4 AC approximation is superseded by the engine's Dex-0 (-5) adjustment for
|
|
# helpless targets, applied in resolve_attack (CombatEngine._is_helpless).
|
|
ASLEEP = Condition(
|
|
name="asleep",
|
|
denies_dex_to_ac=True,
|
|
cant_act=True,
|
|
cant_move=True,
|
|
)
|
|
|
|
|
|
# Helpless: helpless for all intents, treated as Dex 0 (loses Dex to AC),
|
|
# can't act; melee attackers gain +4 (engine logic in resolve_attack). The
|
|
# engine also treats paralyzed/asleep/unconscious as helpless (CRB: "helpless"
|
|
# = paralyzed, held, bound, sleeping, unconscious; stunned is NOT helpless) —
|
|
# see CombatEngine._is_helpless (CRB)
|
|
HELPLESS = Condition(
|
|
name="helpless",
|
|
denies_dex_to_ac=True,
|
|
cant_act=True,
|
|
)
|
|
|
|
# Fascinated: distracted, takes no actions other than paying attention (CRB)
|
|
FASCINATED = Condition(
|
|
name="fascinated",
|
|
cant_act=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",
|
|
)
|
|
|
|
# ── Concealment conditions ───────────────────────────────────────────────────
|
|
# Blurred (blur spell): concealment — 20% miss chance on attacks (CRB)
|
|
BLURRED = Condition(
|
|
name="blurred",
|
|
miss_chance=20,
|
|
)
|
|
|
|
# Displaced (displacement spell): total concealment — 50% miss chance (CRB)
|
|
DISPLACED = Condition(
|
|
name="displaced",
|
|
miss_chance=50,
|
|
)
|
|
|
|
# Invisible: total concealment against attacks targeting it; the attacker-side
|
|
# +2 on attack rolls and the target's lost Dex bonus are engine logic (CRB)
|
|
INVISIBLE = Condition(
|
|
name="invisible",
|
|
miss_chance=50,
|
|
)
|
|
|
|
# ── Damage-over-time and mental conditions ───────────────────────────────────
|
|
# Bleeding: takes the listed bleed damage at the beginning of its turn (CRB).
|
|
# The damage-over-time is engine-driven (CombatantState.bleed_damage); the
|
|
# condition itself carries no stat modifiers or behavioral flags.
|
|
BLEEDING = Condition(
|
|
name="bleeding",
|
|
)
|
|
|
|
# Confused: at the beginning of each turn the subject rolls d% to act randomly
|
|
# (CRB). The d% behavior is engine logic in ``_take_turn``; simplification:
|
|
# the "if attacked, attack the attacker" clause is reduced to "nearest
|
|
# creature". No stat modifiers or behavioral flags.
|
|
CONFUSED = Condition(
|
|
name="confused",
|
|
)
|
|
|
|
# ── Registry ─────────────────────────────────────────────────────────────────
|
|
CONDITIONS_BY_NAME: dict[str, Condition] = {
|
|
c.name: c
|
|
for c in [
|
|
SHAKEN, SICKENED, FATIGUED, EXHAUSTED, ENTANGLED, DAZZLED,
|
|
BLINDED, DEAFENED, FLAT_FOOTED, PRONE, STUNNED, PARALYZED,
|
|
NAUSEATED, DAZED, STAGGERED, FRIGHTENED, PANICKED, COWERING,
|
|
ASLEEP, GRAPPLED, PINNED, BLURRED, DISPLACED,
|
|
HELPLESS, FASCINATED, INVISIBLE, BLEEDING, CONFUSED,
|
|
]
|
|
}
|