From a1798aecb30500ce06f762e148b9309c053b1d21 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Mon, 1 Jun 2026 21:43:58 +0200 Subject: [PATCH] refactor(ui): split talkie dialogue overlay --- src/components/ui/TalkieDialogueOverlay.tsx | 114 +++--------------- src/components/ui/talkie/TalkieModel.tsx | 82 +++++++++++++ .../ui/talkie/TalkieSignalLines.tsx | 19 +++ src/hooks/ui/useTalkieDialogueOverlayState.ts | 27 +++++ src/index.css | 32 ++--- 5 files changed, 158 insertions(+), 116 deletions(-) create mode 100644 src/components/ui/talkie/TalkieModel.tsx create mode 100644 src/components/ui/talkie/TalkieSignalLines.tsx create mode 100644 src/hooks/ui/useTalkieDialogueOverlayState.ts diff --git a/src/components/ui/TalkieDialogueOverlay.tsx b/src/components/ui/TalkieDialogueOverlay.tsx index ce7100b..21a7f4b 100644 --- a/src/components/ui/TalkieDialogueOverlay.tsx +++ b/src/components/ui/TalkieDialogueOverlay.tsx @@ -1,108 +1,24 @@ -import { Suspense, useEffect, useMemo, useRef } from "react"; -import { Canvas, useFrame } from "@react-three/fiber"; -import { useGLTF } from "@react-three/drei"; -import * as THREE from "three"; -import { useGameStore } from "@/managers/stores/useGameStore"; -import { useSubtitleStore } from "@/managers/stores/useSubtitleStore"; - -const TALKIE_MODEL_PATH = "/models/talkie/model.gltf"; -const TALKIE_REVEAL_STEPS = new Set([ - "reveal", - "await-ebike-mount", - "ebike-intro-ride", - "ebike-breakdown", - "completed", -]); - -const TALKIE_REST_Y = -0.55; -const TALKIE_ACTIVE_Y = -0.18; -const TALKIE_FLOAT_Y_AMPLITUDE = 0.025; -const TALKIE_FLOAT_ROTATION_AMPLITUDE = THREE.MathUtils.degToRad(1.6); - -interface TalkieModelProps { - active: boolean; -} - -function TalkieModel({ active }: TalkieModelProps): React.JSX.Element { - const { scene } = useGLTF(TALKIE_MODEL_PATH); - const model = useMemo(() => scene.clone(true), [scene]); - const groupRef = useRef(null); - - useEffect(() => { - model.traverse((child) => { - if (child instanceof THREE.Mesh) { - child.castShadow = false; - child.receiveShadow = false; - child.frustumCulled = false; - } - }); - }, [model]); - - useFrame(({ clock }) => { - if (!groupRef.current) return; - - const t = clock.getElapsedTime(); - const floatY = Math.sin(t * 1.4) * TALKIE_FLOAT_Y_AMPLITUDE; - const targetY = (active ? TALKIE_ACTIVE_Y : TALKIE_REST_Y) + floatY; - groupRef.current.position.y = THREE.MathUtils.lerp( - groupRef.current.position.y, - targetY, - 0.14, - ); - - if (active) { - groupRef.current.rotation.z = Math.sin(t * 22) * 0.025; - } else { - groupRef.current.rotation.z = - Math.sin(t * 0.8) * TALKIE_FLOAT_ROTATION_AMPLITUDE; - } - }); - - return ( - - - - ); -} - -function TalkieSignalLines(): React.JSX.Element { - return ( - - ); -} +import { Suspense } from "react"; +import { Canvas } from "@react-three/fiber"; +import { TalkieModel } from "@/components/ui/talkie/TalkieModel"; +import { TalkieSignalLines } from "@/components/ui/talkie/TalkieSignalLines"; +import { useTalkieDialogueOverlayState } from "@/hooks/ui/useTalkieDialogueOverlayState"; export function TalkieDialogueOverlay(): React.JSX.Element | null { - const activeSubtitle = useSubtitleStore((state) => state.activeSubtitle); - const mainState = useGameStore((state) => state.mainState); - const introStep = useGameStore((state) => state.intro.currentStep); - const isAfterReveal = - mainState !== "intro" || TALKIE_REVEAL_STEPS.has(introStep); - const isNarratorDialogue = activeSubtitle?.speaker === "Narrateur"; + const { isNarratorDialogue, isVisible } = useTalkieDialogueOverlayState(); - if (!isAfterReveal) return null; - - const overlayClassName = isNarratorDialogue - ? "talkie-dialogue-overlay talkie-dialogue-overlay--active talkie-dialogue-overlay--raised" - : "talkie-dialogue-overlay"; + if (!isVisible) return null; return ( -