Fixes + UX: combat HP, char delete, dice animation, compendium filters, import/export
- Combat: relabel HP controls Dmg/Heal, disabled at 0 (were silent no-ops) - Characters: per-card Delete (confirm) + Export; header Import from JSON - Dice: tumbling roll animation that settles on the result (a11y-safe) - Compendium: per-category filters (monster type/CR, spell level/school, item rarity/type) + Clear - Character import/export to portable JSON (reassigns id, validates on import) - New: 4 io unit tests, e2e fixes spec (HP damage, delete, CR filter) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { characterSchema, type Character } from '@/lib/schemas';
|
||||
import { newId } from '@/lib/ids';
|
||||
import { downloadJson, safeFilename } from './file';
|
||||
|
||||
const FORMAT = 'ttrpg-manager:character';
|
||||
const VERSION = 1;
|
||||
|
||||
interface CharacterFile {
|
||||
format: typeof FORMAT;
|
||||
version: number;
|
||||
character: Character;
|
||||
}
|
||||
|
||||
/** Download a character as a portable JSON file. */
|
||||
export function exportCharacter(c: Character): void {
|
||||
const payload: CharacterFile = { format: FORMAT, version: VERSION, character: c };
|
||||
downloadJson(`${safeFilename(c.name)}.character.json`, payload);
|
||||
}
|
||||
|
||||
export class CharacterImportError extends Error {}
|
||||
|
||||
/**
|
||||
* Parse imported JSON into a valid Character bound to the given campaign. Accepts
|
||||
* both our wrapped export format and a bare character object. Always assigns a
|
||||
* fresh id + timestamps so importing never collides with or overwrites existing
|
||||
* data.
|
||||
*/
|
||||
export function parseCharacterImport(text: string, campaignId: string): Character {
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = JSON.parse(text);
|
||||
} catch {
|
||||
throw new CharacterImportError('That file is not valid JSON.');
|
||||
}
|
||||
|
||||
const candidate =
|
||||
raw && typeof raw === 'object' && 'character' in raw
|
||||
? (raw as { character: unknown }).character
|
||||
: raw;
|
||||
|
||||
if (!candidate || typeof candidate !== 'object') {
|
||||
throw new CharacterImportError('No character data found in that file.');
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const result = characterSchema.safeParse({
|
||||
...(candidate as Record<string, unknown>),
|
||||
id: newId(),
|
||||
campaignId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new CharacterImportError(
|
||||
`That file isn't a valid character: ${result.error.issues[0]?.message ?? 'unknown error'}`,
|
||||
);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
Reference in New Issue
Block a user