From 70346de362423401a84f2e73dc6c01a2a8b872a8 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Sun, 10 May 2026 00:10:16 +0100 Subject: [PATCH] add: trigger dialogue with timecode --- src/world/GameDialogues.tsx | 59 +++++++++++++++++++++++++++++++++++++ src/world/World.tsx | 2 ++ 2 files changed, 61 insertions(+) create mode 100644 src/world/GameDialogues.tsx diff --git a/src/world/GameDialogues.tsx b/src/world/GameDialogues.tsx new file mode 100644 index 0000000..845cd20 --- /dev/null +++ b/src/world/GameDialogues.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef, useState } from "react"; +import { useFrame } from "@react-three/fiber"; +import type { DialogueManifest } from "@/types/dialogues/dialogues"; +import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest"; +import { playDialogueById } from "@/utils/dialogues/playDialogue"; +import { logger } from "@/utils/core/logger"; + +export function GameDialogues(): null { + const [manifest, setManifest] = useState(null); + const playedDialoguesRef = useRef(new Set()); + const activeAudiosRef = useRef(new Set()); + + useEffect(() => { + let mounted = true; + const activeAudios = activeAudiosRef.current; + + void loadDialogueManifest() + .then((loadedManifest) => { + if (mounted) setManifest(loadedManifest); + }) + .catch((error: unknown) => { + logger.error("GameDialogues", "Failed to load dialogue manifest", { + error: error instanceof Error ? error : String(error), + }); + }); + + return () => { + mounted = false; + activeAudios.forEach((audio) => audio.pause()); + activeAudios.clear(); + }; + }, []); + + useFrame(({ clock }) => { + if (!manifest) return; + + const elapsedTime = clock.getElapsedTime(); + + manifest.dialogues.forEach((dialogue) => { + if (dialogue.timecode === undefined) return; + if (dialogue.timecode > elapsedTime) return; + if (playedDialoguesRef.current.has(dialogue.id)) return; + + playedDialoguesRef.current.add(dialogue.id); + + void playDialogueById(manifest, dialogue.id).then((audio) => { + if (!audio) return; + activeAudiosRef.current.add(audio); + audio.addEventListener( + "ended", + () => activeAudiosRef.current.delete(audio), + { once: true }, + ); + }); + }); + }); + + return null; +} diff --git a/src/world/World.tsx b/src/world/World.tsx index 4737abb..eb22071 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -10,6 +10,7 @@ import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControl import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove"; import { Environment } from "@/world/Environment"; +import { GameDialogues } from "@/world/GameDialogues"; import { GameMusic } from "@/world/GameMusic"; import { Lighting } from "@/world/Lighting"; import { GameMap } from "@/world/GameMap"; @@ -42,6 +43,7 @@ export function World(): React.JSX.Element { {sceneMode === "game" ? ( <> +