"""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, ) # Bane (SRD spells/bane.md): "Each affected creature takes a -1 penalty on # attack rolls and a -1 penalty on saving throws against fear effects." The # save penalties are conditioned on the "fear" descriptor so they apply only # to saves against fear effects (the engine's descriptor machinery, same # convention as inspire courage's conditioned morale). BANE = Condition( name="bane", modifiers=( StatModifier(target="attack", value=-1), StatModifier(target="fort", value=-1, condition="fear"), StatModifier(target="ref", value=-1, condition="fear"), StatModifier(target="will", value=-1, condition="fear"), ), ) # ── 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, -1 damage (STR/DEX), -1 AC and -1 Ref (the -2 DEX # = -1 to each, same modelling as exhausted/entangled). FATIGUED = Condition( name="fatigued", modifiers=( StatModifier(target="attack", value=-1), StatModifier(target="damage", value=-1), StatModifier(target="ac", value=-1), StatModifier(target="ref", value=-1), ), cant_charge=True, ) # Exhausted: -6 STR, -6 DEX; half speed; can't run or charge (CRB) # Approximation: -3 attack, -3 damage (STR/DEX), -3 AC and -3 Ref (the -6 DEX # = -3 to each, mirroring how ENTANGLED models its -4 DEX as -2 AC/-2 Ref). EXHAUSTED = Condition( name="exhausted", modifiers=( StatModifier(target="attack", value=-3), StatModifier(target="damage", value=-3), StatModifier(target="ac", value=-3), StatModifier(target="ref", 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), # -4 to Dexterity skills (mirrors grappled/pinned). Casting while entangled # requires a DC 15 + spell level concentration check — enforced in # ``CombatEngine._spell_cast_interrupted`` (SRD Table 9-1). ENTANGLED = Condition( name="entangled", modifiers=( StatModifier(target="attack", value=-2), StatModifier(target="ac", value=-2), StatModifier(target="ref", value=-2), StatModifier(target="dex_skills", value=-4), # -4 to Dex skills ), 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) # SRD "Conditions—Nauseated": "The character is unable to attack, cast # spells... " — being unable to attack also denies attacks of opportunity. NAUSEATED = Condition( name="nauseated", cant_attack=True, cant_aoo=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) # SRD "Conditions—Pinned": a pinned creature is tightly bound and can only # attempt to escape (verbal/mental actions + the escape check) — it cannot # attack, so cant_attack applies (the engine's move-only filter then blocks # attacks, maneuvers and casts). Documented gap: the engine never APPLIES # pinned (no grapple-maintain loop), so this definition is exercised only # when the condition is injected directly. 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_attack=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". SRD "Conditions—Confused": a confused creature "will not make # attacks of opportunity against anything that it is not already devoted to # attacking" — simplified to NO AoOs at all (cant_aoo), since the engine has # no "already devoted" concept. No stat modifiers. CONFUSED = Condition( name="confused", cant_aoo=True, ) # ── 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, BANE, ] }