import type { SystemId } from '@/lib/rules'; import type { AbilityKey, AdvState } from './types'; /** * The mechanical effect a condition imposes — a declarative table, never * hardcoded `if`s in combat code. `deriveState` (creatureState.ts) folds these * into a single query the tracker and dice roller ask. */ /** * Which statistic family a PF2e value-scaled status penalty applies to. Different * families are independent (they target different rolls) and must never be compared * against each other — clumsy hits Dex-based rolls, enfeebled hits Str-based rolls, etc. */ export type PenaltyTarget = 'all' | 'str' | 'dex' | 'con' | 'mental'; export interface ConditionEffect { /** sets effective speed to 0 */ speedZero?: boolean; /** this creature's attack rolls */ attackRolls?: AdvState; /** attack rolls made AGAINST this creature */ attacksAgainst?: AdvState; /** disadvantage on ability checks (5e) */ abilityChecks?: boolean; /** per-ability save penalty (auto-fail dominates disadvantage) */ saves?: Partial>; /** cannot take actions or reactions */ incapacitated?: boolean; /** * pf2e: a value-scaled status penalty applied to this statistic family. The * penalty magnitude is the condition's value; family decides which rolls it hits. */ statusPenalty?: PenaltyTarget; } const ALL_PHYS: Partial> = { str: 'auto-fail', dex: 'auto-fail' }; /** D&D 5e condition mechanics (keys lowercased to match stored names). */ export const CONDITION_EFFECTS_5E: Record = { blinded: { attackRolls: 'disadvantage', attacksAgainst: 'advantage' }, frightened: { attackRolls: 'disadvantage', abilityChecks: true }, grappled: { speedZero: true }, incapacitated: { incapacitated: true }, invisible: { attackRolls: 'advantage', attacksAgainst: 'disadvantage' }, paralyzed: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage', saves: ALL_PHYS }, petrified: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage', saves: ALL_PHYS }, poisoned: { attackRolls: 'disadvantage', abilityChecks: true }, prone: { attackRolls: 'disadvantage', attacksAgainst: 'advantage' }, restrained: { speedZero: true, attackRolls: 'disadvantage', attacksAgainst: 'advantage', saves: { dex: 'disadvantage' } }, stunned: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage', saves: ALL_PHYS }, unconscious: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage', saves: ALL_PHYS }, }; /** * Pathfinder 2e condition mechanics. PF2e uses flat numeric penalties rather * than advantage/disadvantage; valued conditions (frightened N, clumsy N, …) * contribute a status penalty aggregated by `deriveState`. */ export const CONDITION_EFFECTS_PF2E: Record = { blinded: { attacksAgainst: 'advantage' }, // Each valued condition penalises a DIFFERENT statistic family (never compared): clumsy: { statusPenalty: 'dex' }, // AC, Reflex, Dex attacks/skills drained: { statusPenalty: 'con' }, // Fortitude (also reduces HP — applied separately) enfeebled: { statusPenalty: 'str' }, // Str attacks/damage, Athletics stupefied: { statusPenalty: 'mental' }, // spell attacks/DCs, mental checks frightened: { statusPenalty: 'all' }, // every check and DC; decays 1/turn sickened: { statusPenalty: 'all' }, // every check and DC // Dazzled imposes concealment on the dazzled creature's OWN targets (a flat check on // its attacks) — it does NOT lower its AC. The app can't model the flat-check, so we // record no advantage to attackers here rather than wrongly making it easier to hit. dazzled: {}, // fatigued (flat -1 AC/saves) and slowed (action loss) are NOT value-scaled — see deriveState. fatigued: {}, slowed: {}, grabbed: { speedZero: true, attacksAgainst: 'advantage' }, immobilized: { speedZero: true }, paralyzed: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage' }, petrified: { incapacitated: true, speedZero: true }, prone: { attacksAgainst: 'advantage' }, restrained: { speedZero: true, incapacitated: false, attacksAgainst: 'advantage' }, stunned: { incapacitated: true }, unconscious: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage' }, 'off-guard': { attacksAgainst: 'advantage' }, }; export function conditionEffects(system: SystemId): Record { return system === 'pf2e' ? CONDITION_EFFECTS_PF2E : CONDITION_EFFECTS_5E; }