Polish sprint 1/2: restore crash fix, pf2e parity, full glyph purge

Crash fix:
- restoreBackup (file + cloud pull) now validates each row through its Zod
  schema, backfilling new fields — an older backup no longer lands characters
  missing feats/concentration/etc and crashing the sheet. + test.

pf2e parity (closing feature gaps vs 5e):
- +9 missing classes (Animist, Exemplar, Gunslinger, Inventor, Kineticist,
  Magus, Psychic, Summoner, Thaumaturge) with data + class tips.
- the wizard now enriches pf2e classes with their caster ability, so pf2e
  spellcasters get the Spells step + "Caster" tag like 5e.
- editable Perception rank and spellcasting proficiency on the pf2e sheet
  (fixes wrong initiative / spell DC at higher levels); rank-10 spell slots.
- pf2e monster resistances/weaknesses/immunities now apply in combat (flat
  amounts: resistance subtracts, weakness adds, 'all' matches any type). + tests.

Glyph purge (finishing the emoji→Lucide migration): replaced ~40 leftover
text-glyphs (✕ − + ✦ 🤫 ⬆ ⬇ ☑ ☐ ▶ ◀ ‹ › ★ ↻ ▸ ↑ ●) with Lucide icons across
Modal (every dialog), the sheet sections, dice, settings, player panels, world
pages, map editor, roll tray, and session UI.

Gate: 293 unit + e2e green; tsc + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 11:29:36 +02:00
parent f75b50adb8
commit dd694477b2
38 changed files with 355 additions and 84 deletions
@@ -40,6 +40,49 @@ function parseConditionList(raw: string | undefined): string[] {
.filter(Boolean);
}
/** Normalize a pf2e damage/condition word ("non-magical attacks", "cold_iron"). */
function pf2eType(s: string): DamageType | undefined {
return asDamageType(s.replace(/_/g, ' ').trim());
}
/** pf2e creature flat-defense shape, ready to attach to a combatant. */
export interface Pf2eDefenses {
immune: DamageType[];
conditionImmune: string[];
resistFlat: { type: string; amount: number }[];
weakness: { type: string; amount: number }[];
notes: string[];
}
/**
* pf2e creature → flat resistances/weaknesses + immunities. pf2e uses flat
* amounts (resistance 5, weakness 5) rather than 5e's halve/double, and 'all'
* applies to every type. Immunities mix damage types and conditions.
*/
export function normalizeMonsterDefensesPf2e(raw: {
immunity?: unknown; resistance?: unknown; weakness?: unknown;
}): Pf2eDefenses {
const immune: DamageType[] = [];
const conditionImmune: string[] = [];
for (const i of Array.isArray(raw.immunity) ? (raw.immunity as string[]) : []) {
const t = pf2eType(String(i));
if (t) immune.push(t);
else conditionImmune.push(String(i).trim().toLowerCase());
}
const flat = (obj: unknown): { type: string; amount: number }[] => {
if (!obj || typeof obj !== 'object') return [];
const out: { type: string; amount: number }[] = [];
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
const amount = Number(v);
if (!Number.isFinite(amount) || amount <= 0) continue;
const key = k.toLowerCase() === 'all' ? 'all' : pf2eType(k);
if (key) out.push({ type: key, amount });
}
return out;
};
return { immune, conditionImmune, resistFlat: flat(raw.resistance), weakness: flat(raw.weakness), notes: [] };
}
/** Open5e monster → structured damage/condition defenses. */
export function normalizeMonsterDefenses(raw: Monster): DamageDefenses {
const resist = parseDamageList(raw.damage_resistances);