From 2f0700f8d3ef8eeadfc819f36f6f9c0ef180a19f Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Wed, 10 Jun 2026 03:13:47 +0200 Subject: [PATCH] =?UTF-8?q?D3:=20AI=20Player=20=E2=80=94=20pilot=20a=20rea?= =?UTF-8?q?l=20PC,=20with=20caster=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The director can now play a party character, not just DM. A Mode switch (Dungeon Master / Player) plus an AI-seat picker choose which PC the AI pilots. - Scene injection: when seated, the controlled PC's real sheet detail is fed to the director — attacks with derived to-hit (sys.weaponAttack) + damage expressions, spells (level/concentration), resources, and spell slots — so the AI player acts from true numbers, in first person. - Persona-aware prompt + cue already route DM→runs monsters / player→"it is your turn"; the seat sets controlledName. - Caster actions (castSpell/spendResource) flow through the D2 approve-each apply bridge unchanged — slot/resource spend is enforced by the kernel, concentration mirrors to the combatant. - UI: SceneControls (mode + seat), seat-gating ("pick a character"), and player-flavored buttons (Take turn). directorStore gains controlledCharacterId. Verified in-app: switching to Player mode, seating "Lia the Brave · Fighter 3", and taking a turn produced first-person narration. 388 unit tests green (incl. new player-persona scene/ prompt tests); lint clean; build OK. Co-Authored-By: Claude Opus 4.8 --- .../assistant/director/DirectorPage.tsx | 23 ++++++-- .../assistant/director/SceneControls.tsx | 55 +++++++++++++++++++ .../assistant/director/useDirectorSession.ts | 14 +++-- src/lib/assistant/director/context.ts | 33 ++++++++++- src/lib/assistant/director/director.test.ts | 35 +++++++++++- src/stores/directorStore.ts | 7 ++- tsconfig.app.tsbuildinfo | 2 +- 7 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 src/features/assistant/director/SceneControls.tsx diff --git a/src/features/assistant/director/DirectorPage.tsx b/src/features/assistant/director/DirectorPage.tsx index 8ca83c4..014996b 100644 --- a/src/features/assistant/director/DirectorPage.tsx +++ b/src/features/assistant/director/DirectorPage.tsx @@ -3,7 +3,10 @@ import type { Campaign } from '@/lib/schemas'; import { Page, PageHeader, RequireCampaign } from '@/components/ui/Page'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Codex'; +import { useDirectorStore } from '@/stores/directorStore'; +import { useCharacters } from '@/features/characters/hooks'; import { useDirectorSession } from './useDirectorSession'; +import { SceneControls } from './SceneControls'; import { TranscriptPane } from './TranscriptPane'; import { RollRequestCard } from './RollRequestCard'; import { ActionCard } from './ActionCard'; @@ -14,8 +17,10 @@ export function DirectorPage() { } function Director({ campaign }: { campaign: Campaign }) { - const { persona, session, busy, source, message, lastTurn, llmReady, activeEnc, run, recordRoll, applyAction, restart } = + const { persona, controlledCharacterId, needsSeat, session, busy, source, message, lastTurn, llmReady, activeEnc, run, recordRoll, applyAction, restart } = useDirectorSession(campaign); + const characters = useCharacters(campaign.id); + const setConfig = useDirectorStore((s) => s.setConfig); const entries = session?.transcript ?? []; const started = entries.length > 0; @@ -80,16 +85,26 @@ function Director({ campaign }: { campaign: Campaign }) {