import { db } from './db'; import { newId } from '@/lib/ids'; import { campaignSchema, characterSchema, encounterSchema, diceRollSchema, noteSchema, npcSchema, questSchema, calendarSchema, battleMapSchema, homebrewSchema, type HomebrewKind, type Homebrew, type Campaign, type CampaignDraft, type Character, type Encounter, type DiceRoll, type Note, type Npc, type Quest, type Calendar, type BattleMap, } from '@/lib/schemas'; import { getSystem } from '@/lib/rules'; function now(): string { return new Date().toISOString(); } // ---------------- Campaigns ---------------- export const campaignsRepo = { async list(): Promise { const all = await db.campaigns.toArray(); return all.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); }, get(id: string): Promise { return db.campaigns.get(id); }, async create(draft: CampaignDraft): Promise { const ts = now(); const campaign = campaignSchema.parse({ ...draft, id: newId(), description: draft.description ?? '', createdAt: ts, updatedAt: ts, }); await db.campaigns.add(campaign); return campaign; }, async update(id: string, patch: Partial>): Promise { await db.campaigns.update(id, { ...patch, updatedAt: now() }); }, /** Delete a campaign and ALL its children atomically (real cascade). */ async remove(id: string): Promise { await db.transaction( 'rw', [db.campaigns, db.characters, db.encounters, db.diceRolls, db.notes, db.npcs, db.quests, db.calendars, db.maps, db.homebrew], async () => { await db.characters.where('campaignId').equals(id).delete(); await db.encounters.where('campaignId').equals(id).delete(); await db.diceRolls.where('campaignId').equals(id).delete(); await db.notes.where('campaignId').equals(id).delete(); await db.npcs.where('campaignId').equals(id).delete(); await db.quests.where('campaignId').equals(id).delete(); await db.maps.where('campaignId').equals(id).delete(); await db.homebrew.where('campaignId').equals(id).delete(); await db.calendars.delete(id); await db.campaigns.delete(id); }, ); }, }; // ---------------- Characters ---------------- export const charactersRepo = { listByCampaign(campaignId: string): Promise { return db.characters.where('campaignId').equals(campaignId).toArray(); }, get(id: string): Promise { return db.characters.get(id); }, async create( campaignId: string, draft: Pick & { system: Character['system']; }, ): Promise { const ts = now(); const sys = getSystem(draft.system); const character = characterSchema.parse({ id: newId(), campaignId, system: draft.system, kind: draft.kind, name: draft.name, ancestry: draft.ancestry, className: draft.className, level: draft.level, abilities: sys.defaultAbilityScores(), hp: { current: 1, max: 1, temp: 0 }, speed: 30, armorBonus: 0, skillRanks: {}, saveRanks: {}, perceptionRank: 'trained', notes: '', createdAt: ts, updatedAt: ts, }); await db.characters.add(character); return character; }, async update(id: string, patch: Partial>): Promise { await db.characters.update(id, { ...patch, updatedAt: now() }); }, /** Insert a fully-formed character (e.g. from an import). Validates on write. */ async insert(character: Character): Promise { await db.characters.add(characterSchema.parse(character)); }, async remove(id: string): Promise { await db.characters.delete(id); }, }; // ---------------- Encounters ---------------- export const encountersRepo = { listByCampaign(campaignId: string): Promise { return db.encounters.where('campaignId').equals(campaignId).toArray(); }, get(id: string): Promise { return db.encounters.get(id); }, async create(campaignId: string, name: string): Promise { const ts = now(); const encounter = encounterSchema.parse({ id: newId(), campaignId, name, status: 'planning', round: 0, turnIndex: 0, combatants: [], createdAt: ts, updatedAt: ts, }); await db.encounters.add(encounter); return encounter; }, /** Persist a full encounter (combat state lives as one document). */ async save(encounter: Encounter): Promise { const validated = encounterSchema.parse({ ...encounter, updatedAt: now() }); await db.encounters.put(validated); }, /** * Apply a transition by reading the freshest committed state inside a * transaction, so two rapid mutations can't overwrite each other (the * read-modify-write race the old app shipped — bug class C19). */ async mutate(id: string, fn: (e: Encounter) => Encounter): Promise { await db.transaction('rw', db.encounters, async () => { const current = await db.encounters.get(id); if (!current) return; await db.encounters.put(encounterSchema.parse({ ...fn(current), updatedAt: now() })); }); }, async remove(id: string): Promise { await db.encounters.delete(id); }, }; // ---------------- Dice rolls ---------------- export const diceRepo = { async recent(campaignId: string | undefined, limit = 50): Promise { const coll = campaignId ? db.diceRolls.where('campaignId').equals(campaignId) : db.diceRolls.toCollection(); const all = await coll.toArray(); return all.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit); }, async add(roll: Omit): Promise { const record = diceRollSchema.parse({ ...roll, id: newId(), createdAt: now() }); await db.diceRolls.add(record); return record; }, async clear(campaignId?: string): Promise { if (campaignId) await db.diceRolls.where('campaignId').equals(campaignId).delete(); else await db.diceRolls.clear(); }, }; // ---------------- Notes ---------------- export const notesRepo = { listByCampaign(campaignId: string): Promise { return db.notes.where('campaignId').equals(campaignId).toArray(); }, get(id: string): Promise { return db.notes.get(id); }, async create(campaignId: string, title: string): Promise { const ts = now(); const note = noteSchema.parse({ id: newId(), campaignId, title, body: '', tags: [], createdAt: ts, updatedAt: ts }); await db.notes.add(note); return note; }, async update(id: string, patch: Partial>): Promise { await db.notes.update(id, { ...patch, updatedAt: now() }); }, async remove(id: string): Promise { await db.notes.delete(id); }, }; // ---------------- NPCs ---------------- export const npcsRepo = { listByCampaign(campaignId: string): Promise { return db.npcs.where('campaignId').equals(campaignId).toArray(); }, async create(campaignId: string, name: string): Promise { const ts = now(); const npc = npcSchema.parse({ id: newId(), campaignId, name, role: '', location: '', faction: '', status: 'alive', description: '', createdAt: ts, updatedAt: ts, }); await db.npcs.add(npc); return npc; }, async update(id: string, patch: Partial>): Promise { await db.npcs.update(id, { ...patch, updatedAt: now() }); }, async remove(id: string): Promise { await db.npcs.delete(id); }, }; // ---------------- Quests ---------------- export const questsRepo = { listByCampaign(campaignId: string): Promise { return db.quests.where('campaignId').equals(campaignId).toArray(); }, async create(campaignId: string, title: string): Promise { const ts = now(); const quest = questSchema.parse({ id: newId(), campaignId, title, status: 'active', description: '', reward: '', objectives: [], createdAt: ts, updatedAt: ts, }); await db.quests.add(quest); return quest; }, async update(id: string, patch: Partial>): Promise { await db.quests.update(id, { ...patch, updatedAt: now() }); }, async remove(id: string): Promise { await db.quests.delete(id); }, }; // ---------------- Calendar (one per campaign) ---------------- export const calendarRepo = { /** Read-only — safe to call inside a Dexie liveQuery. Missing = undefined. */ get(campaignId: string): Promise { return db.calendars.get(campaignId); }, async save(calendar: Calendar): Promise { await db.calendars.put(calendarSchema.parse(calendar)); }, }; // ---------------- Battle maps ---------------- export const mapsRepo = { listByCampaign(campaignId: string): Promise { return db.maps.where('campaignId').equals(campaignId).toArray(); }, async create(campaignId: string, name: string, image: string): Promise { const ts = now(); const map = battleMapSchema.parse({ id: newId(), campaignId, name, image, gridSize: 50, showGrid: true, fogEnabled: false, revealed: [], tokens: [], createdAt: ts, updatedAt: ts, }); await db.maps.add(map); return map; }, async save(map: BattleMap): Promise { await db.maps.put(battleMapSchema.parse({ ...map, updatedAt: now() })); }, async remove(id: string): Promise { await db.maps.delete(id); }, }; // ---------------- Homebrew ---------------- export const homebrewRepo = { listByCampaign(campaignId: string): Promise { return db.homebrew.where('campaignId').equals(campaignId).toArray(); }, byKind(campaignId: string, kind: HomebrewKind): Promise { return db.homebrew.where('[campaignId+kind]').equals([campaignId, kind]).toArray(); }, async create(campaignId: string, system: Homebrew['system'], kind: HomebrewKind, name: string): Promise { const ts = now(); const hb = homebrewSchema.parse({ id: newId(), campaignId, system, kind, name, fields: {}, description: '', createdAt: ts, updatedAt: ts }); await db.homebrew.add(hb); return hb; }, async update(id: string, patch: Partial>): Promise { await db.homebrew.update(id, { ...patch, updatedAt: now() }); }, async remove(id: string): Promise { await db.homebrew.delete(id); }, /** Insert a batch (from a content pack import), reassigning ids + campaign. */ async importMany(campaignId: string, entries: Homebrew[]): Promise { const ts = now(); const records = entries.map((e) => homebrewSchema.parse({ ...e, id: newId(), campaignId, createdAt: ts, updatedAt: ts }), ); await db.homebrew.bulkAdd(records); return records.length; }, };