Files
ttrpg_manager/src/lib/mechanics/conditionEffects.ts
T
NilsBriggen 895807d6c9 Polish sprint 2/2: layouts, UX consistency, pf2e conditions
Layout:
- CombatantRow: action controls (damage/AC/move/remove) grouped into one
  right-aligned wrapper so they wrap together instead of scattering on narrow
  widths.
- top bar: the campaign switcher now shrinks/truncates instead of overflowing
  on tablet widths.
- character sheet header: stat coins go full-width grid on mobile; the name
  truncates and scales down on small screens.

UX consistency:
- player Cast and resource Spend/Regain no longer fail silently — Cast shows the
  reason, resource −/+ disable at their bounds.
- the AI encounter builder now catches load failures and reports them; level-up
  advisor shows the human error message, not the error-kind enum.
- section headers standardized on the .smallcaps design token across 16 files
  (replacing an ad-hoc uppercase class).
- player HP bar uses the shared Meter primitive.

pf2e depth: condition-effects table gains drained, dazzled, enfeebled, fatigued.
FeatCard: dropped a dead text-xl wrapper left from the emoji purge.

Left as documented (functional, off-theme, refactor-risky): the native
confirm/prompt in Settings cloud-conflict and the session-password flow.

Gate: 293 unit + 35 e2e + 2 realtime; tsc + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:38:24 +02:00

74 lines
3.4 KiB
TypeScript

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.
*/
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<Record<AbilityKey, 'disadvantage' | 'auto-fail'>>;
/** cannot take actions or reactions */
incapacitated?: boolean;
/** pf2e: a numeric status penalty scaling with the condition's value */
statusPenalty?: boolean;
}
const ALL_PHYS: Partial<Record<AbilityKey, 'auto-fail'>> = { str: 'auto-fail', dex: 'auto-fail' };
/** D&D 5e condition mechanics (keys lowercased to match stored names). */
export const CONDITION_EFFECTS_5E: Record<string, ConditionEffect> = {
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<string, ConditionEffect> = {
blinded: { attacksAgainst: 'advantage' },
clumsy: { statusPenalty: true },
drained: { statusPenalty: true },
dazzled: { attacksAgainst: 'advantage' },
enfeebled: { statusPenalty: true },
fatigued: { statusPenalty: true },
frightened: { statusPenalty: true },
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' },
sickened: { statusPenalty: true },
slowed: { statusPenalty: true },
stunned: { incapacitated: true },
stupefied: { statusPenalty: true },
unconscious: { incapacitated: true, speedZero: true, attacksAgainst: 'advantage' },
'off-guard': { attacksAgainst: 'advantage' },
};
export function conditionEffects(system: SystemId): Record<string, ConditionEffect> {
return system === 'pf2e' ? CONDITION_EFFECTS_PF2E : CONDITION_EFFECTS_5E;
}