Phase 7: maps & VTT

- BattleMap entity (image dataURL, grid, tokens, fog) — Dexie v5, repo, hook,
  cascade-deleted with campaign
- Maps page: upload image (8MB cap), per-campaign list
- Map editor canvas: grid overlay (toggle + cell size), fog of war (canvas) with
  reveal/hide painting + reveal-all/hide-all, draggable tokens that snap to the
  grid (pointer events, touch+mouse), add/remove tokens; debounced autosave
- Dashboard + routes get a Maps entry

New maps e2e (upload, add token, toggle fog).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 02:01:29 +02:00
parent 522ff8abce
commit 1632018ce9
9 changed files with 354 additions and 5 deletions
+26 -1
View File
@@ -9,6 +9,7 @@ import {
npcSchema,
questSchema,
calendarSchema,
battleMapSchema,
type Campaign,
type CampaignDraft,
type Character,
@@ -18,6 +19,7 @@ import {
type Npc,
type Quest,
type Calendar,
type BattleMap,
} from '@/lib/schemas';
import { getSystem } from '@/lib/rules';
@@ -57,7 +59,7 @@ export const campaignsRepo = {
async remove(id: string): Promise<void> {
await db.transaction(
'rw',
[db.campaigns, db.characters, db.encounters, db.diceRolls, db.notes, db.npcs, db.quests, db.calendars],
[db.campaigns, db.characters, db.encounters, db.diceRolls, db.notes, db.npcs, db.quests, db.calendars, db.maps],
async () => {
await db.characters.where('campaignId').equals(id).delete();
await db.encounters.where('campaignId').equals(id).delete();
@@ -65,6 +67,7 @@ export const campaignsRepo = {
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.calendars.delete(id);
await db.campaigns.delete(id);
},
@@ -264,3 +267,25 @@ export const calendarRepo = {
await db.calendars.put(calendarSchema.parse(calendar));
},
};
// ---------------- Battle maps ----------------
export const mapsRepo = {
listByCampaign(campaignId: string): Promise<BattleMap[]> {
return db.maps.where('campaignId').equals(campaignId).toArray();
},
async create(campaignId: string, name: string, image: string): Promise<BattleMap> {
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<void> {
await db.maps.put(battleMapSchema.parse({ ...map, updatedAt: now() }));
},
async remove(id: string): Promise<void> {
await db.maps.delete(id);
},
};