From 4fa02fd49315e72e09a041c7f3113bbdff673a89 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 14 Apr 2026 19:55:49 +0200 Subject: [PATCH] Add dialogue management to RuntimeHost --- src/runtime-three/runtime-host.ts | 126 +++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/src/runtime-three/runtime-host.ts b/src/runtime-three/runtime-host.ts index 0a7fbc34..19411d22 100644 --- a/src/runtime-three/runtime-host.ts +++ b/src/runtime-three/runtime-host.ts @@ -183,6 +183,16 @@ export interface RuntimeSceneExitTransitionRequest { targetEntryEntityId: string; } +export interface RuntimeDialogueState { + dialogueId: string; + title: string; + lineId: string; + lineIndex: number; + lineCount: number; + speakerName: string | null; + text: string; +} + export class RuntimeHost { private readonly scene = new Scene(); private readonly worldBackgroundRenderer = new WorldBackgroundRenderer(); @@ -256,6 +266,9 @@ export class RuntimeHost { private interactionPromptHandler: | ((prompt: RuntimeInteractionPrompt | null) => void) | null = null; + private runtimeDialogueHandler: + | ((dialogue: RuntimeDialogueState | null) => void) + | null = null; private sceneLoadStateHandler: | ((state: RuntimeSceneLoadState) => void) | null = null; @@ -266,6 +279,7 @@ export class RuntimeHost { private currentPlayerControllerTelemetry: PlayerControllerTelemetry | null = null; private currentInteractionPrompt: RuntimeInteractionPrompt | null = null; + private currentDialogue: RuntimeDialogueState | null = null; private currentSceneLoadState: RuntimeSceneLoadState | null = null; private currentClockState: RuntimeClockState | null = null; private lastPublishedClockState: RuntimeClockState | null = null; @@ -470,6 +484,7 @@ export class RuntimeHost { this.thirdPersonController.resetSceneState(); this.interactionSystem.reset(); this.setInteractionPrompt(null); + this.setRuntimeDialogue(null); this.currentPlayerControllerTelemetry = null; this.currentPlayerAudioHooks = null; this.playerControllerTelemetryHandler?.(null); @@ -555,6 +570,42 @@ export class RuntimeHost { this.interactionPromptHandler = handler; } + setRuntimeDialogueHandler( + handler: ((dialogue: RuntimeDialogueState | null) => void) | null + ) { + this.runtimeDialogueHandler = handler; + } + + advanceRuntimeDialogue() { + if (this.runtimeScene === null || this.currentDialogue === null) { + return; + } + + const dialogue = + this.runtimeScene.dialogues.dialogues[this.currentDialogue.dialogueId] ?? + null; + + if (dialogue === null) { + this.setRuntimeDialogue(null); + return; + } + + const nextLineIndex = this.currentDialogue.lineIndex + 1; + + if (nextLineIndex >= dialogue.lines.length) { + this.setRuntimeDialogue(null); + return; + } + + this.setRuntimeDialogue( + this.createRuntimeDialogueState(dialogue.id, nextLineIndex) + ); + } + + closeRuntimeDialogue() { + this.setRuntimeDialogue(null); + } + setSceneLoadStateHandler( handler: ((state: RuntimeSceneLoadState) => void) | null ) { @@ -2946,6 +2997,9 @@ export class RuntimeHost { stopSound: (soundEmitterId) => { this.audioSystem.stopSound(soundEmitterId); }, + startDialogue: (dialogueId) => { + this.openRuntimeDialogue(dialogueId); + }, dispatchControlEffect: (effect, link) => { this.applyControlEffect(effect, link); } @@ -2967,6 +3021,65 @@ export class RuntimeHost { this.interactionPromptHandler?.(prompt); } + private createRuntimeDialogueState( + dialogueId: string, + lineIndex: number + ): RuntimeDialogueState | null { + if (this.runtimeScene === null) { + return null; + } + + const dialogue = this.runtimeScene.dialogues.dialogues[dialogueId]; + + if (dialogue === undefined) { + return null; + } + + const line = dialogue.lines[lineIndex]; + + if (line === undefined) { + return null; + } + + return { + dialogueId, + title: dialogue.title, + lineId: line.id, + lineIndex, + lineCount: dialogue.lines.length, + speakerName: line.speakerName, + text: line.text + }; + } + + private setRuntimeDialogue(dialogue: RuntimeDialogueState | null) { + if ( + this.currentDialogue?.dialogueId === dialogue?.dialogueId && + this.currentDialogue?.lineId === dialogue?.lineId && + this.currentDialogue?.lineIndex === dialogue?.lineIndex && + this.currentDialogue?.lineCount === dialogue?.lineCount && + this.currentDialogue?.speakerName === dialogue?.speakerName && + this.currentDialogue?.text === dialogue?.text && + this.currentDialogue?.title === dialogue?.title + ) { + return; + } + + this.currentDialogue = dialogue; + this.runtimeDialogueHandler?.(dialogue); + } + + private openRuntimeDialogue(dialogueId: string) { + const dialogue = this.createRuntimeDialogueState(dialogueId, 0); + + if (dialogue === null) { + console.warn(`dialogue: missing dialogue ${dialogueId}`); + return; + } + + this.setRuntimeDialogue(dialogue); + } + private resolveInteractionPrompt(): RuntimeInteractionPrompt | null { if ( this.runtimeScene === null || @@ -3006,13 +3119,22 @@ export class RuntimeHost { !this.sceneReady || this.runtimeScene === null || (this.activeController !== this.firstPersonController && - this.activeController !== this.thirdPersonController) || - this.currentInteractionPrompt === null + this.activeController !== this.thirdPersonController) ) { return; } this.audioSystem.handleUserGesture(); + + if (this.currentDialogue !== null) { + this.advanceRuntimeDialogue(); + return; + } + + if (this.currentInteractionPrompt === null) { + return; + } + this.interactionSystem.dispatchClickInteraction( this.currentInteractionPrompt.sourceEntityId, this.runtimeScene,