docs: queue dialogue

This commit is contained in:
Tom Boullay
2026-05-11 09:43:40 +02:00
parent dd41d2cbb2
commit c5cc6f685a
2 changed files with 94 additions and 3 deletions
+88 -1
View File
@@ -4,6 +4,32 @@ import { useSubtitleStore } from "@/managers/stores/useSubtitleStore";
import type { DialogueManifest } from "@/types/dialogues/dialogues"; import type { DialogueManifest } from "@/types/dialogues/dialogues";
import { loadDialogueSubtitleCue } from "@/utils/dialogues/loadDialogueManifest"; import { loadDialogueSubtitleCue } from "@/utils/dialogues/loadDialogueManifest";
interface QueuedDialogueRequest {
manifest: DialogueManifest;
dialogueId: string;
resolve: (audio: HTMLAudioElement | null) => void;
}
const DIALOGUE_PLAY_START_TIMEOUT_MS = 800;
const dialogueQueue: QueuedDialogueRequest[] = [];
let isDialogueQueuePlaying = false;
export function queueDialogueById(
manifest: DialogueManifest,
dialogueId: string,
): Promise<HTMLAudioElement | null> {
return new Promise((resolve) => {
dialogueQueue.push({ manifest, dialogueId, resolve });
void playNextQueuedDialogue();
});
}
export function clearQueuedDialogues(): void {
while (dialogueQueue.length > 0) {
dialogueQueue.shift()?.resolve(null);
}
}
export async function playDialogueById( export async function playDialogueById(
manifest: DialogueManifest, manifest: DialogueManifest,
dialogueId: string, dialogueId: string,
@@ -28,6 +54,7 @@ export async function playDialogueById(
}; };
const cleanup = (): void => { const cleanup = (): void => {
audio.removeEventListener("play", syncSubtitle);
audio.removeEventListener("timeupdate", syncSubtitle); audio.removeEventListener("timeupdate", syncSubtitle);
audio.removeEventListener("ended", cleanup); audio.removeEventListener("ended", cleanup);
audio.removeEventListener("pause", cleanup); audio.removeEventListener("pause", cleanup);
@@ -51,10 +78,70 @@ export async function playDialogueById(
clearSubtitle(); clearSubtitle();
}; };
audio.addEventListener("play", syncSubtitle);
audio.addEventListener("timeupdate", syncSubtitle); audio.addEventListener("timeupdate", syncSubtitle);
audio.addEventListener("ended", cleanup); audio.addEventListener("ended", cleanup);
audio.addEventListener("pause", cleanup); audio.addEventListener("pause", cleanup);
syncSubtitle();
return audio; return audio;
} }
async function playNextQueuedDialogue(): Promise<void> {
if (isDialogueQueuePlaying) return;
isDialogueQueuePlaying = true;
while (dialogueQueue.length > 0) {
const request = dialogueQueue.shift();
if (!request) continue;
try {
const audio = await playDialogueById(
request.manifest,
request.dialogueId,
);
request.resolve(audio);
if (audio) await waitForDialogueToFinish(audio);
} catch {
request.resolve(null);
}
}
isDialogueQueuePlaying = false;
}
function waitForDialogueToFinish(audio: HTMLAudioElement): Promise<void> {
if (audio.ended) return Promise.resolve();
return new Promise((resolve) => {
let hasStarted = !audio.paused;
let startTimeout: ReturnType<typeof setTimeout> | null = null;
function cleanup(): void {
if (startTimeout) clearTimeout(startTimeout);
audio.removeEventListener("play", handlePlay);
audio.removeEventListener("ended", finish);
audio.removeEventListener("pause", finish);
audio.removeEventListener("error", finish);
}
function finish(): void {
cleanup();
resolve();
}
function handlePlay(): void {
hasStarted = true;
if (startTimeout) clearTimeout(startTimeout);
}
audio.addEventListener("play", handlePlay);
audio.addEventListener("ended", finish);
audio.addEventListener("pause", finish);
audio.addEventListener("error", finish);
startTimeout = setTimeout(() => {
if (!hasStarted && audio.paused) finish();
}, DIALOGUE_PLAY_START_TIMEOUT_MS);
});
}
+6 -2
View File
@@ -2,7 +2,10 @@ import { useEffect, useRef, useState } from "react";
import { useFrame } from "@react-three/fiber"; import { useFrame } from "@react-three/fiber";
import type { DialogueManifest } from "@/types/dialogues/dialogues"; import type { DialogueManifest } from "@/types/dialogues/dialogues";
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest"; import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
import { playDialogueById } from "@/utils/dialogues/playDialogue"; import {
clearQueuedDialogues,
queueDialogueById,
} from "@/utils/dialogues/playDialogue";
import { logger } from "@/utils/core/logger"; import { logger } from "@/utils/core/logger";
export function GameDialogues(): null { export function GameDialogues(): null {
@@ -26,6 +29,7 @@ export function GameDialogues(): null {
return () => { return () => {
mounted = false; mounted = false;
clearQueuedDialogues();
activeAudios.forEach((audio) => audio.pause()); activeAudios.forEach((audio) => audio.pause());
activeAudios.clear(); activeAudios.clear();
}; };
@@ -43,7 +47,7 @@ export function GameDialogues(): null {
playedDialoguesRef.current.add(dialogue.id); playedDialoguesRef.current.add(dialogue.id);
void playDialogueById(manifest, dialogue.id).then((audio) => { void queueDialogueById(manifest, dialogue.id).then((audio) => {
if (!audio) return; if (!audio) return;
activeAudiosRef.current.add(audio); activeAudiosRef.current.add(audio);
audio.addEventListener( audio.addEventListener(