Phase 17: map & fog VTT overhaul + player-facing projection
- Pure geometry library src/lib/map/* (grid, distance 5e/pf2e/euclid, AoE circle/cone/line/square, polygon rasterize + point-in-polygon, brush, fog set algebra, player projection) with unit tests. - Schema: map tokens gain size/kind/characterId/hp/conditions/gmOnly; maps gain drawings/gridUnit(feet)/gridType/fogVersion + point/drawing sub-schemas. Dexie v8 additive migration; mapsRepo.get + transactional mutate. - Editor split into a shared MapCanvas renderer + MapEditor tool state machine: fog via brush/rectangle/polygon (+ size, reveal-all/hide-all), measurement (system-aware distance in feet), AoE templates (circle/cone/line/square preview), drawing annotations (freehand/line/arrow/rect/circle/text, GM-only), pings, and richer tokens (NxN size, character link with live HP ring, condition dots, edit popover). - Player-facing projection: toPlayerProjection strips GM-only content; PlayerMapView renders it read-only with opaque fog; uiStore.activeMapId + 'Show to players'; the player view now shows the live battle map. enemyStatus extracted to src/lib/combat/playerProjection.ts (reused, and for Phase 18). 9 geometry unit tests; maps e2e covers upload→token→reveal-all→player projection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,28 @@ export class TtrpgDatabase extends Dexie {
|
||||
// v7 — Phase 13 assistant: encounters gain optional partyLevelsSnapshot +
|
||||
// outcome. Both optional, so existing rows stay valid; no backfill needed.
|
||||
this.version(7).stores({});
|
||||
|
||||
// v8 — Phase 17 VTT overhaul: maps gain drawings/gridUnit/gridType/fogVersion;
|
||||
// tokens gain size/kind/conditions/gmOnly. All defaulted, so backfill is
|
||||
// belt-and-suspenders (repo re-parses on save too).
|
||||
this.version(8).stores({}).upgrade(async (tx) => {
|
||||
await tx
|
||||
.table('maps')
|
||||
.toCollection()
|
||||
.modify((m: Record<string, unknown>) => {
|
||||
if (m.drawings === undefined) m.drawings = [];
|
||||
if (m.gridUnit === undefined) m.gridUnit = { feet: 5 };
|
||||
if (m.gridType === undefined) m.gridType = 'square';
|
||||
if (m.fogVersion === undefined) m.fogVersion = 1;
|
||||
const tokens = Array.isArray(m.tokens) ? (m.tokens as Record<string, unknown>[]) : [];
|
||||
for (const t of tokens) {
|
||||
if (t.size === undefined) t.size = 1;
|
||||
if (t.kind === undefined) t.kind = 'npc';
|
||||
if (t.conditions === undefined) t.conditions = [];
|
||||
if (t.gmOnly === undefined) t.gmOnly = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -299,9 +299,20 @@ export const mapsRepo = {
|
||||
await db.maps.add(map);
|
||||
return map;
|
||||
},
|
||||
get(id: string): Promise<BattleMap | undefined> {
|
||||
return db.maps.get(id);
|
||||
},
|
||||
async save(map: BattleMap): Promise<void> {
|
||||
await db.maps.put(battleMapSchema.parse({ ...map, updatedAt: now() }));
|
||||
},
|
||||
/** Transactional read-modify-write (mirrors encountersRepo.mutate). */
|
||||
async mutate(id: string, fn: (m: BattleMap) => BattleMap): Promise<void> {
|
||||
await db.transaction('rw', db.maps, async () => {
|
||||
const current = await db.maps.get(id);
|
||||
if (!current) return;
|
||||
await db.maps.put(battleMapSchema.parse({ ...fn(current), updatedAt: now() }));
|
||||
});
|
||||
},
|
||||
async remove(id: string): Promise<void> {
|
||||
await db.maps.delete(id);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user