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:
@@ -423,7 +423,7 @@ function CombatantRow({
|
||||
const [dmgType, setDmgType] = useState('');
|
||||
// Only show the damage-type picker when this creature actually has typed defenses.
|
||||
const def = c.damageDefenses;
|
||||
const hasDefenses = !!def && (def.resist.length > 0 || def.immune.length > 0 || def.vulnerable.length > 0);
|
||||
const hasDefenses = !!def && (def.resist.length > 0 || def.immune.length > 0 || def.vulnerable.length > 0 || (def.resistFlat?.length ?? 0) > 0 || (def.weakness?.length ?? 0) > 0);
|
||||
const dead = c.hp.current <= 0;
|
||||
const foe = c.kind === 'monster';
|
||||
const ratio = c.hp.max > 0 ? c.hp.current / c.hp.max : 0;
|
||||
@@ -520,9 +520,16 @@ function CombatantRow({
|
||||
{hasDefenses && (
|
||||
<Select className="w-auto py-1 text-xs" value={dmgType} onChange={(e) => setDmgType(e.target.value)} aria-label={`${c.name} damage type`}>
|
||||
<option value="">untyped</option>
|
||||
{DAMAGE_TYPES.filter((t) => t !== 'untyped').map((t) => (
|
||||
<option key={t} value={t}>{def!.immune.includes(t) ? `${t} (immune)` : def!.resist.includes(t) ? `${t} (resist)` : def!.vulnerable.includes(t) ? `${t} (vuln)` : t}</option>
|
||||
))}
|
||||
{DAMAGE_TYPES.filter((t) => t !== 'untyped').map((t) => {
|
||||
const rf = def!.resistFlat?.find((r) => r.type === t);
|
||||
const wk = def!.weakness?.find((w) => w.type === t);
|
||||
const tag = def!.immune.includes(t) ? ' (immune)'
|
||||
: def!.vulnerable.includes(t) ? ' (vuln)'
|
||||
: def!.resist.includes(t) ? ' (resist)'
|
||||
: rf ? ` (resist ${rf.amount})`
|
||||
: wk ? ` (weak ${wk.amount})` : '';
|
||||
return <option key={t} value={t}>{t}{tag}</option>;
|
||||
})}
|
||||
</Select>
|
||||
)}
|
||||
<Button size="sm" variant="danger" disabled={delta <= 0} onClick={() => { onDamage(delta, dmgType || undefined); setDelta(0); }} title="Apply damage">
|
||||
|
||||
Reference in New Issue
Block a user