diff --git a/client/src/templates/Challenges/components/scene/character.tsx b/client/src/templates/Challenges/components/scene/character.tsx index 788db7e8914..133c2235c13 100644 --- a/client/src/templates/Challenges/components/scene/character.tsx +++ b/client/src/templates/Challenges/components/scene/character.tsx @@ -3,13 +3,14 @@ import { Characters, CharacterPosition } from '../../../../redux/prop-types'; import { characterAssets } from './scene-assets'; import './character.css'; +import { SceneSubject } from './scene-subject'; interface CharacterProps { position: CharacterPosition; opacity: number; name: Characters; - isBlinking: boolean; isTalking: boolean; + sceneSubject: SceneSubject; } interface CharacterStyles { @@ -27,29 +28,43 @@ export function Character({ position, opacity, name, - isBlinking, - isTalking + isTalking, + sceneSubject }: CharacterProps): JSX.Element { const [eyesAreOpen, setEyesAreOpen] = useState(true); const [mouthIsOpen, setMouthIsOpen] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + + const onNotify = (eventType: 'play' | 'stop') => { + if (eventType === 'play') { + setIsPlaying(true); + } else { + setIsPlaying(false); + } + }; useEffect(() => { - let blinkIntervalId: NodeJS.Timeout; + sceneSubject.attach(onNotify); + return () => { + sceneSubject.detach(onNotify); + }; + }, [sceneSubject]); + + useEffect(() => { + if (!isPlaying) return; let blinkTimeoutId: NodeJS.Timeout; - if (isBlinking) { - const blinkPeriod = getRandomInt(2000, 5000); - blinkIntervalId = setInterval(() => { - const blinkJitter = getRandomInt(0, 1000); - blinkTimeoutId = setTimeout(() => { - setEyesAreOpen(false); + const blinkPeriod = getRandomInt(2000, 5000); + const blinkIntervalId = setInterval(() => { + const blinkJitter = getRandomInt(0, 1000); + blinkTimeoutId = setTimeout(() => { + setEyesAreOpen(false); - blinkTimeoutId = setTimeout(() => { - setEyesAreOpen(true); - }, 30); // always unblink after 30ms - }, blinkJitter); - }, blinkPeriod); - } + blinkTimeoutId = setTimeout(() => { + setEyesAreOpen(true); + }, 30); // always unblink after 30ms + }, blinkJitter); + }, blinkPeriod); // Clear intervals when component is unmounted or conditions change return () => { @@ -57,9 +72,10 @@ export function Character({ clearInterval(blinkIntervalId); clearTimeout(blinkTimeoutId); }; - }, [isBlinking]); + }, [isPlaying]); useEffect(() => { + if (!isPlaying) return; let talkIntervalId: NodeJS.Timeout; let mouthOpenTimeoutId: NodeJS.Timeout; let mouthCloseTimeoutId: NodeJS.Timeout; @@ -91,7 +107,7 @@ export function Character({ clearTimeout(mouthOpenTimeoutId); clearTimeout(mouthCloseTimeoutId); }; - }, [isTalking]); + }, [isTalking, isPlaying]); const characterWrapStyles: CharacterStyles = { opacity diff --git a/client/src/templates/Challenges/components/scene/scene-subject.tsx b/client/src/templates/Challenges/components/scene/scene-subject.tsx index 81ecbcef608..24e04b06994 100644 --- a/client/src/templates/Challenges/components/scene/scene-subject.tsx +++ b/client/src/templates/Challenges/components/scene/scene-subject.tsx @@ -1,4 +1,4 @@ -type Observer = () => void; +type Observer = (eventType: 'play' | 'stop') => void; export class SceneSubject { #observers: Observer[]; @@ -16,7 +16,7 @@ export class SceneSubject { // For now, we don't need to pass any data to the observers, so notify() // doesn't take any arguments. - notify() { - this.#observers.forEach(observer => observer()); + notify(eventType: 'play' | 'stop') { + this.#observers.forEach(observer => observer(eventType)); } } diff --git a/client/src/templates/Challenges/components/scene/scene.tsx b/client/src/templates/Challenges/components/scene/scene.tsx index 78991eb1baf..34bda2298ab 100644 --- a/client/src/templates/Challenges/components/scene/scene.tsx +++ b/client/src/templates/Challenges/components/scene/scene.tsx @@ -153,7 +153,7 @@ export function Scene({ canPauseRef.current = false; }; - const playScene = useCallback(() => { + const handlePlay = useCallback(() => { const updateCurrentTime = () => { const time = Date.now() - startRef.current; setCurrentTime(time); @@ -207,9 +207,9 @@ export function Scene({ }, duration + sToMs(audio.startTime) ); - }, [isPlaying, sceneIsReady, audio, duration]); + }, [audio, duration, isPlaying, sceneIsReady]); - const resetScene = useCallback(() => { + const handleStop = useCallback(() => { usedCommandsRef.current.clear(); pause(); audioRef.current.currentTime = audio.startTimestamp || 0; @@ -222,12 +222,27 @@ export function Scene({ setBackground(initBackground); }, [audio, initCharacters, initBackground]); + const onNotify = useCallback( + (eventType: 'play' | 'stop') => { + if (eventType === 'play') { + handlePlay(); + } else { + handleStop(); + } + }, + [handlePlay, handleStop] + ); + + const resetScene = useCallback(() => { + sceneSubject.notify('stop'); + }, [sceneSubject]); + useEffect(() => { - sceneSubject.attach(playScene); + sceneSubject.attach(onNotify); return () => { - sceneSubject.detach(playScene); + sceneSubject.detach(onNotify); }; - }, [playScene, sceneSubject]); + }, [onNotify, sceneSubject]); useEffect(() => { if (isEmpty(sortedCommands)) return; @@ -306,8 +321,8 @@ export function Scene({ name={character} position={position} opacity={opacity} + sceneSubject={sceneSubject} isTalking={isTalking} - isBlinking={isPlaying} /> ); } @@ -328,7 +343,7 @@ export function Scene({