diff --git a/src/features/assistant/director/DirectorPage.tsx b/src/features/assistant/director/DirectorPage.tsx index 014996b..85c9598 100644 --- a/src/features/assistant/director/DirectorPage.tsx +++ b/src/features/assistant/director/DirectorPage.tsx @@ -21,6 +21,7 @@ function Director({ campaign }: { campaign: Campaign }) { useDirectorSession(campaign); const characters = useCharacters(campaign.id); const setConfig = useDirectorStore((s) => s.setConfig); + const shareWithPlayers = useDirectorStore((s) => s.shareWithPlayers); const entries = session?.transcript ?? []; const started = entries.length > 0; @@ -89,8 +90,10 @@ function Director({ campaign }: { campaign: Campaign }) { persona={persona} controlledCharacterId={controlledCharacterId ?? ''} characters={characters} + shareWithPlayers={shareWithPlayers} onPersona={(p) => setConfig({ persona: p })} onSeat={(id) => setConfig({ controlledCharacterId: id })} + onShare={(v) => setConfig({ shareWithPlayers: v })} />
diff --git a/src/features/assistant/director/SceneControls.tsx b/src/features/assistant/director/SceneControls.tsx index 922d6fc..72d18bf 100644 --- a/src/features/assistant/director/SceneControls.tsx +++ b/src/features/assistant/director/SceneControls.tsx @@ -7,14 +7,18 @@ export function SceneControls({ persona, controlledCharacterId, characters, + shareWithPlayers, onPersona, onSeat, + onShare, }: { persona: DirectorPersona; controlledCharacterId: string; characters: Character[]; + shareWithPlayers: boolean; onPersona: (p: DirectorPersona) => void; onSeat: (id: string) => void; + onShare: (v: boolean) => void; }) { const pcs = characters.filter((c) => c.kind === 'pc'); @@ -45,6 +49,11 @@ export function SceneControls({ )} + +

{persona === 'dm' ? 'The AI narrates the world and runs NPCs and monsters.' diff --git a/src/features/assistant/director/useDirectorSession.ts b/src/features/assistant/director/useDirectorSession.ts index 039ea73..16403be 100644 --- a/src/features/assistant/director/useDirectorSession.ts +++ b/src/features/assistant/director/useDirectorSession.ts @@ -16,12 +16,23 @@ import { type RollRequest, } from '@/lib/assistant/director'; import { complete } from '@/lib/llm/client'; +import { sendChat } from '@/lib/sync/wsSync'; +import { useSessionStore } from '@/stores/sessionStore'; import type { Degree } from '@/lib/dice/check'; import { useLatestAiSession } from './hooks'; import { useDirectorAction, type ApplyResult } from './useDirectorAction'; const nowIso = (): string => new Date().toISOString(); +/** When hosting a live session and sharing is on, push the director's narration to + * the players' screens via the table chat channel. A no-op when not hosting. */ +function broadcastNarration(text: string): void { + const { role, status } = useSessionStore.getState(); + if (role === 'gm' && status === 'connected' && useDirectorStore.getState().shareWithPlayers && text.trim()) { + sendChat(text.trim()); + } +} + /** * Fold older transcript turns into the session's rolling summary when the live * window has grown past its budget. Uses the LLM when configured, else a @@ -122,6 +133,7 @@ export function useDirectorSession(campaign: Campaign) { text: res.turn.narration, ts: nowIso(), }]); + broadcastNarration(res.turn.speaker ? `${res.turn.speaker}: ${res.turn.narration}` : res.turn.narration); } setLastTurn({ rollRequests: res.turn.rollRequests, actions: res.turn.actions, suggestions: res.turn.suggestions }); diff --git a/src/stores/directorStore.ts b/src/stores/directorStore.ts index ed46136..add6747 100644 --- a/src/stores/directorStore.ts +++ b/src/stores/directorStore.ts @@ -11,7 +11,9 @@ interface DirectorState { narrationStyle: string; /** how many recent transcript entries to send each turn (token budget) */ windowSize: number; - setConfig: (patch: Partial>) => void; + /** when hosting a live session, push the director's narration to players' screens */ + shareWithPlayers: boolean; + setConfig: (patch: Partial>) => void; } export const useDirectorStore = create()( @@ -21,6 +23,7 @@ export const useDirectorStore = create()( controlledCharacterId: '', narrationStyle: '', windowSize: 16, + shareWithPlayers: true, setConfig: (patch) => set(patch), }), { name: 'ttrpg-director' },