Files
pf1e-simulator/src/pf1e_simulator/conditions.py
T
2026-08-30 09:39:24 +02:00

266 lines
8.0 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
# ── 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 STR/DEX skill checks (CRB)
BLINDED = Condition(
name="blinded",
modifiers=(
StatModifier(target="ac", value=-2),
StatModifier(target="attack", value=-2),
),
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, 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",
)
# ── 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,
]
}