b3914b1dda
Audited every calculation (26-agent adversarial workflow + hand-verification). Core math was sound; fixed real bugs, closed parity gaps, and overhauled the character-build UX. 396 -> 424 tests; lint clean; build green. Correctness fixes: - Heavy armor no longer applies a negative DEX penalty to AC (3 paths + default) - 5e Exhaustion 4 (HP-max halved) implemented via deriveEffectiveMaxHp + badge - PF2e dazzled no longer makes a creature easier to hit - PF2e immunity 'all' zeroes all damage (was misfiled as a condition immunity) - Director schema bounds; PF2e spell-save basis from the `basic` flag; 5e disadvantage-only save guard; remove bogus Concentrating, add Broken/Quickened - Fix 3 lint errors (useCloud hooks naming, useless escape, explicit any) PF2e survival subsystem (parity with 5e death saves): - mechanics/dying.ts (maxDying/isDead/knockOut/applyRecovery/pf2eRestDecay) - DefensesSection knock-out + recovery-check + death UI; rest-decay in applyRest; drained HP reduction Character-build UX: - Persisted abilityBuild (base + per-source contributions); sheet & wizard show every ability source; higher-level PF2e gains L5/10/15/20 boosts - Creation surfaces granted skills (incl. new PF2e background skill parsing) - LevelUpModal enumerates features gained + prompts every owed choice (subclass, feats, fighting style, ...) via collectChoices/collectFeatures/subclassPrompt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
4.5 KiB
TypeScript
89 lines
4.5 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.
|
|
*/
|
|
/**
|
|
* 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<Record<AbilityKey, 'disadvantage' | 'auto-fail'>>;
|
|
/** 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<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' },
|
|
// 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<string, ConditionEffect> {
|
|
return system === 'pf2e' ? CONDITION_EFFECTS_PF2E : CONDITION_EFFECTS_5E;
|
|
}
|