The first user-visible slice of the AI DM / AI Player "director": a new
/director page where the AI narrates the scene, voices NPCs/monsters, and runs
enemies — grounded entirely in the campaign's real party and active encounter,
and degrading to a deterministic narrator when no AI key is set.
Engine (pure, framework-free) in src/lib/assistant/director/:
- schema.ts: DeepSeek-tolerant directorTurnSchema (z.preprocess structural
repair + z.coerce + .catch() enums + per-element safeParse-drop) → a single
validated turn { narration, rollRequests[], actions[], suggestions[] }.
- context.ts: buildDirectorScene assembles a CLOSED roster from the active
encounter (or the party) with deriveState badges — the only entities the
director may name.
- prompt.ts: persona-aware system prompt (DM/player) leading with the
systemConstraint; multi-turn message mapping from the transcript.
- engine.ts: runDirectorTurn + sanitizeTurn — the anti-hallucination gate that
drops any action referencing an off-roster entity (and ungrounded
addCombatant), mirroring the encounter advisor's candidate filter.
- fallback.ts: deterministic director that still surfaces roll buttons.
Hard rules, enforced:
- NEVER auto-roll: a roll fires only from RollRequestCard's onClick (rollAndShow).
A unit test asserts the engine layer never imports the roll seam, making
auto-roll structurally impossible.
- Approve-each: D1 is read-only (actions render as previews; the Apply bridge
lands in D2). The director never writes game state.
- Always-on fallback: works with no API key.
Also: Dexie v18 `aiSessions` table + aiSessionsRepo (+ cascade delete), a small
directorStore, a Settings card, and the /director route + nav entry.
Verified in-app on the sample 5e campaign: grounded narration named the real
current combatant; on an enemy's turn a "Aller Rosk attack vs Pip Underbough"
roll card appeared with diceRolls UNCHANGED until the button was clicked, which
then fired exactly one roll and recorded the result back into the transcript.
381 unit tests green; lint clean (3 pre-existing); build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CreationWizard: add sys.skills to the loader effect's deps (stable singleton).
- Extract NotFound to src/app/NotFound.tsx so router.tsx no longer trips
react-refresh/only-export-components.
Branch is now warning-clean (the 3 remaining eslint errors are pre-existing on master,
in files this branch does not touch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New themed useConfirm() hook (no native dialogs). World deletes (notes, quests,
homebrew, maps) now confirm before removing.
- Dice 'Clear history' is disabled with no active campaign (it used to wipe EVERY
campaign's rolls) and now confirms; only ever clears the active campaign.
- sessionLog is included in backup/restore and the campaign cascade-delete — it was
silently dropped on backup and orphaned on campaign delete.
- Unknown URLs render a themed 'Page not found' with a link home instead of a blank shell.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Players claim a seat (GM approves) and drive their own character live —
HP/slots/resources/conditions/dice broadcast to the table — via new seat
protocol messages and per-seat ownership enforced in RoomHub. GM persists
player edits (charactersRepo.update → liveQuery → rebroadcast). Offline dev:
"Share with player" encodes a claim link (src/lib/sync/playerLink.ts) → /player
imports the character locally for full-sheet editing, bundled back as an
offlineSnapshot on seat claim for GM review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Deterministic advisors (src/lib/assistant): party resources (downed/bloodied/
spent slots/rest nudges), session prep (dangling [[wiki links]], active quests,
empty journal), live combat hints (turn, downed combatants) — pure + tested
- Data-driven encounter builder: greedily assembles level/CR-appropriate monsters
to a target difficulty via the Phase-3 budget; one click builds + opens combat
- Assistant page: suggestion cards with one-click actions (goto, long rest,
create note); wired into routes, dashboard, command palette
- 8 advisor/builder unit tests + assistant e2e
Conversational LLM layer intentionally deferred (no provider/key in this env);
the deterministic advisor is the offline default per the plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Homebrew entity (monster/spell/item/feat/condition) — Dexie v6, repo, hook,
cascade-deleted with campaign
- Homebrew editor page: per-kind fields + description, create/edit/delete
- Compendium merges campaign homebrew of the matching kind (HB badge, generic
HomebrewDetail) with the same cross-links (add monster->combat, spell/item->character)
- Content packs: export/import homebrew as JSON (validated, ids reassigned)
- System extensibility documented via the existing RulesSystem registry seam
- Fix: card editors (homebrew/npc/quest/note) debounce-save the FULL snapshot, not
partial patches — editing two fields fast no longer drops an edit
New homebrew e2e + cascade test coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Player View (/play): read-only projector screen — party HP bars + conditions,
active-encounter initiative with current turn; enemy HP hidden behind a
Healthy/Bloodied/Down status band; Fullscreen button
- SyncAdapter seam (src/lib/sync): local-first default via Dexie liveQuery;
documents where a future networked (WebSocket/CRDT) backend plugs in
- Dashboard + routes get a Player View entry
- Robustness: encountersRepo.mutate() does transactional read-modify-write so
rapid combat mutations can't overwrite each other (old C19 race); tracker uses it
New player-view e2e. Networked multiplayer backend intentionally deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>