Add dialogue management to RuntimeHost

This commit is contained in:
2026-04-14 19:55:49 +02:00
parent 65cdd4529b
commit 4fa02fd493

View File

@@ -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,