Files
ttrpg_manager/src/lib/db/db.ts
T
NilsBriggen fb459ad92c 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>
2026-06-08 10:31:26 +02:00

103 lines
3.8 KiB
TypeScript

import Dexie, { type EntityTable } from 'dexie';
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, BattleMap, Homebrew } from '@/lib/schemas/world';
import { characterDefaults } from '@/lib/schemas/character';
/**
* Single Dexie instance for the whole app. Dexie wraps IndexedDB transactions
* correctly (commits before resolving), which removes the silent-data-loss
* class of bugs the old hand-rolled adapter shipped with.
*
* Schema versions are additive: bump `version(n)` and add a `.upgrade()` when
* the shape changes — never edit a past version in place.
*/
export class TtrpgDatabase extends Dexie {
campaigns!: EntityTable<Campaign, 'id'>;
characters!: EntityTable<Character, 'id'>;
encounters!: EntityTable<Encounter, 'id'>;
diceRolls!: EntityTable<DiceRoll, 'id'>;
notes!: EntityTable<Note, 'id'>;
npcs!: EntityTable<Npc, 'id'>;
quests!: EntityTable<Quest, 'id'>;
calendars!: EntityTable<Calendar, 'campaignId'>;
maps!: EntityTable<BattleMap, 'id'>;
homebrew!: EntityTable<Homebrew, 'id'>;
constructor() {
super('ttrpg-manager');
this.version(1).stores({
campaigns: 'id, updatedAt',
characters: 'id, campaignId, kind',
encounters: 'id, campaignId, status',
diceRolls: 'id, campaignId, createdAt',
});
// v2 — Phase 1 character depth: backfill new fields on existing characters.
this.version(2).stores({}).upgrade(async (tx) => {
await tx
.table('characters')
.toCollection()
.modify((c: Record<string, unknown>) => {
for (const [key, value] of Object.entries(characterDefaults())) {
if (c[key] === undefined) c[key] = value;
}
});
});
// v3 — Phase 3 combat depth: backfill the encounter event log.
this.version(3).stores({}).upgrade(async (tx) => {
await tx
.table('encounters')
.toCollection()
.modify((e: Record<string, unknown>) => {
if (e.log === undefined) e.log = [];
});
});
// v4 — Phase 5 worldbuilding: notes, NPCs, quests, calendar.
this.version(4).stores({
notes: 'id, campaignId',
npcs: 'id, campaignId',
quests: 'id, campaignId, status',
calendars: 'campaignId',
});
// v5 — Phase 7 maps/VTT.
this.version(5).stores({ maps: 'id, campaignId' });
// v6 — Phase 9 homebrew content.
this.version(6).stores({ homebrew: 'id, campaignId, [campaignId+kind]' });
// 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;
}
});
});
}
}
export const db = new TtrpgDatabase();