Files
ttrpg_manager/src/lib/mechanics/normalize/armor.ts
T
NilsBriggen b3914b1dda Bug hunt + 5e/PF2e parity + character-build UX overhaul
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>
2026-06-10 14:14:01 +02:00

43 lines
1.4 KiB
TypeScript

import type { Armor5e } from '@/lib/compendium/types';
import type { ArmorMechanics } from '../types';
/**
* 5e equipment armor → unified mechanics.
*
* The source `ac` is a string ("11 + Dex", "14 + Dex (max 2)", "18", "+2" for a
* shield) and `dexCap` is null (light/unlimited), a number (medium cap), or 0
* (heavy). We trust `dexCap` and parse the leading number out of `ac`.
*/
export function normalizeArmor5e(raw: Armor5e): ArmorMechanics | null {
if (!raw.name) return null;
const type = (raw.type ?? '').toLowerCase();
const isShield = type === 'shield';
const acStr = raw.ac ?? '';
// Shields read "+2"; armor reads "11 + Dex" or "18".
const baseAc = Number(/-?\d+/.exec(acStr)?.[0] ?? (isShield ? 2 : 10));
const category: ArmorMechanics['category'] = isShield
? 'shield'
: type === 'light' ? 'light'
: type === 'medium' ? 'medium'
: type === 'heavy' ? 'heavy'
: 'unarmored';
// Shields don't cap Dex (they're additive); honor the source dexCap otherwise.
// When the source omits dexCap, fall back by category: heavy ignores Dex (0),
// medium caps at +2, light/unarmored is uncapped (null).
const dexCap = isShield
? null
: raw.dexCap !== undefined && raw.dexCap !== null
? raw.dexCap
: category === 'heavy' ? 0 : category === 'medium' ? 2 : null;
return {
name: raw.name,
category,
baseAc,
dexCap,
stealthDisadvantage: !!raw.stealthPenalty,
};
}