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:
+5
-1
@@ -3,7 +3,7 @@ import type { Campaign } from '@/lib/schemas/campaign';
|
||||
import type { Character } from '@/lib/schemas/character';
|
||||
import type { Encounter } from '@/lib/schemas/encounter';
|
||||
import type { DiceRoll } from '@/lib/schemas/dice';
|
||||
import type { Note, Npc, Quest, Calendar } from '@/lib/schemas/world';
|
||||
import type { Note, Npc, Quest, Calendar, BattleMap } from '@/lib/schemas/world';
|
||||
import { characterDefaults } from '@/lib/schemas/character';
|
||||
|
||||
/**
|
||||
@@ -23,6 +23,7 @@ export class TtrpgDatabase extends Dexie {
|
||||
npcs!: EntityTable<Npc, 'id'>;
|
||||
quests!: EntityTable<Quest, 'id'>;
|
||||
calendars!: EntityTable<Calendar, 'campaignId'>;
|
||||
maps!: EntityTable<BattleMap, 'id'>;
|
||||
|
||||
constructor() {
|
||||
super('ttrpg-manager');
|
||||
@@ -62,6 +63,9 @@ export class TtrpgDatabase extends Dexie {
|
||||
quests: 'id, campaignId, status',
|
||||
calendars: 'campaignId',
|
||||
});
|
||||
|
||||
// v5 — Phase 7 maps/VTT.
|
||||
this.version(5).stores({ maps: 'id, campaignId' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -66,3 +66,31 @@ export const calendarSchema = z.object({
|
||||
events: z.array(calendarEventSchema).default([]),
|
||||
});
|
||||
export type Calendar = z.infer<typeof calendarSchema>;
|
||||
|
||||
// ---------------- Battle maps (VTT) ----------------
|
||||
export const mapTokenSchema = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().max(40).default(''),
|
||||
color: z.string().max(20).default('#d4af37'),
|
||||
/** grid cell coordinates */
|
||||
col: int,
|
||||
row: int,
|
||||
});
|
||||
export type MapToken = z.infer<typeof mapTokenSchema>;
|
||||
|
||||
export const battleMapSchema = z.object({
|
||||
id: z.string(),
|
||||
campaignId: z.string(),
|
||||
name: z.string().min(1).max(120),
|
||||
/** image as a data URL (stored locally) */
|
||||
image: z.string().default(''),
|
||||
gridSize: int.min(10).max(400).default(50),
|
||||
showGrid: z.boolean().default(true),
|
||||
fogEnabled: z.boolean().default(false),
|
||||
/** revealed cell keys "col,row"; everything else is fogged when enabled */
|
||||
revealed: z.array(z.string()).default([]),
|
||||
tokens: z.array(mapTokenSchema).default([]),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type BattleMap = z.infer<typeof battleMapSchema>;
|
||||
|
||||
Reference in New Issue
Block a user