| import { Box, IconButton, LinearProgress, Tooltip } from "@mui/material"; |
| import { motion } from "framer-motion"; |
| import { useEffect, useRef, useState } from "react"; |
| import { useNavigate, useParams } from "react-router-dom"; |
|
|
| import { ErrorDisplay } from "../components/ErrorDisplay"; |
| import { LoadingScreen } from "../components/LoadingScreen"; |
| import { TalkWithSarah } from "../components/TalkWithSarah"; |
| import { GameDebugPanel } from "../components/GameDebugPanel"; |
| import { UniverseSlotMachine } from "../components/UniverseSlotMachine"; |
| import { useGameSession } from "../hooks/useGameSession"; |
| import { useStoryCapture } from "../hooks/useStoryCapture"; |
| import { ComicLayout } from "../layouts/ComicLayout"; |
| import { storyApi, universeApi } from "../utils/api"; |
| import { GameProvider, useGame } from "../contexts/GameContext"; |
| import { StoryChoices } from "../components/StoryChoices"; |
| import { useSoundSystem } from "../contexts/SoundContext"; |
| import { GameNavigation } from "../components/GameNavigation"; |
| import { RotatingMessage } from "../components/RotatingMessage"; |
|
|
| |
| const SOUND_ENABLED_KEY = "sound_enabled"; |
| const GAME_INITIALIZED_KEY = "game_initialized"; |
|
|
| const TRANSITION_MESSAGES = [ |
| "Opening the portal...", |
| "Take a deep breath...", |
| "Let's start...", |
| ]; |
|
|
| const SESSION_LOADING_MESSAGES = [ |
| "Waking up sleepy AI...", |
| "Calibrating the multiverse...", |
| "Gathering comic book inspiration...", |
| ]; |
|
|
| function GameContent() { |
| const navigate = useNavigate(); |
| const { universeId } = useParams(); |
| const { |
| segments, |
| setSegments, |
| choices, |
| setChoices, |
| isLoading, |
| setIsLoading, |
| heroName, |
| setHeroName, |
| showChoices, |
| setShowChoices, |
| error, |
| setError, |
| gameState, |
| setGameState, |
| currentStory, |
| setCurrentStory, |
| universe, |
| setUniverse, |
| slotMachineState, |
| setSlotMachineState, |
| showSlotMachine, |
| setShowSlotMachine, |
| isInitialLoading, |
| setIsInitialLoading, |
| showLoadingMessages, |
| setShowLoadingMessages, |
| isTransitionLoading, |
| setIsTransitionLoading, |
| layoutCounter, |
| setLayoutCounter, |
| resetGame, |
| generateImagesForStory, |
| isNarratorSpeaking, |
| playNarration, |
| stopNarration, |
| } = useGame(); |
|
|
| const storyContainerRef = useRef(null); |
| const { downloadStoryImage } = useStoryCapture(); |
| const [audioInitialized, setAudioInitialized] = useState(false); |
| const { isSoundEnabled, setIsSoundEnabled, playSound } = useSoundSystem(); |
| const [loadingMessage, setLoadingMessage] = useState(0); |
| const [isDebugVisible, setIsDebugVisible] = useState(false); |
| const [isSlotMachineVisible, setIsSlotMachineVisible] = useState(true); |
|
|
| const { |
| sessionId, |
| universe: gameUniverse, |
| isLoading: isSessionLoading, |
| error: sessionError, |
| } = useGameSession(); |
|
|
| |
| useEffect(() => { |
| const handleUserInteraction = () => { |
| if (!audioInitialized) { |
| |
| const AudioContext = window.AudioContext || window.webkitAudioContext; |
| const audioCtx = new AudioContext(); |
| audioCtx.resume().then(() => { |
| setAudioInitialized(true); |
| |
| window.removeEventListener("click", handleUserInteraction); |
| window.removeEventListener("keydown", handleUserInteraction); |
| window.removeEventListener("touchstart", handleUserInteraction); |
| }); |
| } |
| }; |
|
|
| window.addEventListener("click", handleUserInteraction); |
| window.addEventListener("keydown", handleUserInteraction); |
| window.addEventListener("touchstart", handleUserInteraction); |
|
|
| return () => { |
| window.removeEventListener("click", handleUserInteraction); |
| window.removeEventListener("keydown", handleUserInteraction); |
| window.removeEventListener("touchstart", handleUserInteraction); |
| }; |
| }, [audioInitialized]); |
|
|
| |
| useEffect(() => { |
| if (!isInitialLoading && audioInitialized) { |
| playSound("transition"); |
| } |
| }, [isInitialLoading, audioInitialized]); |
|
|
| |
| useEffect(() => { |
| localStorage.setItem(SOUND_ENABLED_KEY, isSoundEnabled); |
| storyApi.setSoundEnabled(isSoundEnabled); |
| }, [isSoundEnabled]); |
|
|
| |
| useEffect(() => { |
| const handleKeyPress = (event) => { |
| if (event.key.toLowerCase() === "d") { |
| setIsDebugVisible((prev) => !prev); |
| } |
| }; |
|
|
| window.addEventListener("keydown", handleKeyPress); |
| return () => window.removeEventListener("keydown", handleKeyPress); |
| }, []); |
|
|
| |
| useEffect(() => { |
| if (segments.length > 0) { |
| const lastSegment = segments[segments.length - 1]; |
| setCurrentStory(lastSegment); |
| setGameState((prev) => ({ |
| ...prev, |
| story_beat: segments.length - 1, |
| story_history: segments, |
| })); |
| } |
| }, [segments]); |
|
|
| |
| useEffect(() => { |
| if (gameUniverse) { |
| setGameState({ |
| universe_style: gameUniverse.style, |
| universe_genre: gameUniverse.genre, |
| universe_epoch: gameUniverse.epoch, |
| universe_macguffin: gameUniverse.macguffin, |
| hero_name: gameUniverse.hero_name || "the hero", |
| story_beat: 0, |
| story_history: [], |
| }); |
| } |
| }, [gameUniverse]); |
|
|
| |
| useEffect(() => { |
| const loadUniverse = async () => { |
| setIsLoading(true); |
| try { |
| const universeData = await universeApi.generate(); |
| console.log("Universe Data:", universeData); |
|
|
| |
| setSlotMachineState({ |
| style: universeData.style.name, |
| genre: universeData.genre, |
| epoch: universeData.epoch, |
| activeIndex: 3, |
| }); |
|
|
| setHeroName(universeData.hero_name); |
| setUniverse(universeData); |
|
|
| |
| const response = await storyApi.start(universeData.session_id); |
| console.log("Initial Story Response:", response); |
|
|
| |
| const formattedSegment = { |
| text: response.story_text, |
| rawText: response.story_text, |
| choices: response.choices || [], |
| isLoading: false, |
| images: [], |
| isDeath: response.is_death || false, |
| isVictory: response.is_victory || false, |
| time: response.time, |
| location: response.location, |
| session_id: universeData.session_id, |
| }; |
|
|
| setSegments([formattedSegment]); |
| setChoices(response.choices); |
|
|
| |
| if (response.image_prompts && response.image_prompts.length > 0) { |
| await generateImagesForStory(response.image_prompts, 0, [ |
| formattedSegment, |
| ]); |
| } |
|
|
| |
| setShowSlotMachine(false); |
| } catch (error) { |
| console.error("Error loading universe:", error); |
| setError(error); |
| } finally { |
| setIsLoading(false); |
| } |
| }; |
|
|
| loadUniverse(); |
| return () => resetGame(); |
| }, [universeId]); |
|
|
| |
| useEffect(() => { |
| if (isTransitionLoading) { |
| |
| const isGameReady = |
| segments.length > 0 && |
| segments[0].images && |
| segments[0].images.length > 0; |
|
|
| if (isGameReady) { |
| setIsTransitionLoading(false); |
| } |
| } |
| }, [isTransitionLoading, segments]); |
|
|
| |
| useEffect(() => { |
| const loadedSegments = segments.filter((segment) => !segment.isLoading); |
| const lastSegment = loadedSegments[loadedSegments.length - 1]; |
| const hasNewSegment = lastSegment && !lastSegment.hasBeenRead; |
|
|
| if (storyContainerRef.current && hasNewSegment && !isNarratorSpeaking) { |
| |
| if (isSoundEnabled) { |
| stopNarration(); |
| } |
|
|
| |
| storyContainerRef.current.scrollTo({ |
| left: storyContainerRef.current.scrollWidth, |
| behavior: "smooth", |
| }); |
|
|
| let isCleanedUp = false; |
|
|
| |
| const timeoutId = setTimeout(() => { |
| if (isCleanedUp) return; |
|
|
| |
| playSound("writing"); |
|
|
| |
| setTimeout(() => { |
| if (isCleanedUp) return; |
|
|
| if ( |
| lastSegment && |
| lastSegment.text && |
| isSoundEnabled && |
| !isNarratorSpeaking |
| ) { |
| playNarration(lastSegment.text); |
| } |
| |
| lastSegment.hasBeenRead = true; |
| }, 500); |
| }, 500); |
|
|
| return () => { |
| isCleanedUp = true; |
| clearTimeout(timeoutId); |
| if (isSoundEnabled) { |
| stopNarration(); |
| } |
| }; |
| } |
| }, [ |
| segments, |
| playNarration, |
| stopNarration, |
| isSoundEnabled, |
| playSound, |
| isNarratorSpeaking, |
| ]); |
|
|
| |
| useEffect(() => { |
| if (!isSoundEnabled) { |
| stopNarration(); |
| } |
| }, [isSoundEnabled, stopNarration]); |
|
|
| const handleBack = () => { |
| playSound("page"); |
| localStorage.removeItem("slot_machine_shown"); |
| window.location.href = "/tutorial"; |
| }; |
|
|
| const handleCaptureStory = async () => { |
| await downloadStoryImage(storyContainerRef, `your-story-${Date.now()}.png`); |
| }; |
|
|
| |
| if (sessionError) { |
| return ( |
| <ErrorDisplay |
| message="Impossible d'initialiser la session de jeu. Veuillez rafraîchir la page." |
| error={sessionError} |
| /> |
| ); |
| } |
|
|
| |
| if (isSessionLoading) { |
| return ( |
| <Box |
| sx={{ |
| width: "100%", |
| height: "100vh", |
| display: "flex", |
| alignItems: "center", |
| justifyContent: "center", |
| position: "relative", |
| }} |
| > |
| <LinearProgress |
| sx={{ |
| position: "absolute", |
| top: 0, |
| left: 0, |
| right: 0, |
| }} |
| /> |
| <RotatingMessage messages={SESSION_LOADING_MESSAGES} isVisible={true} /> |
| </Box> |
| ); |
| } |
|
|
| |
| if ( |
| isTransitionLoading || |
| !segments.length || |
| !segments[0].images || |
| segments[0].images.length === 0 |
| ) { |
| return ( |
| <Box |
| sx={{ |
| width: "100%", |
| height: "100vh", |
| display: "flex", |
| alignItems: "center", |
| justifyContent: "center", |
| position: "relative", |
| }} |
| > |
| <LinearProgress |
| sx={{ |
| position: "absolute", |
| top: 0, |
| left: 0, |
| right: 0, |
| }} |
| /> |
| <RotatingMessage messages={TRANSITION_MESSAGES} isVisible={true} /> |
| </Box> |
| ); |
| } |
|
|
| |
| if ( |
| gameUniverse && |
| slotMachineState.style && |
| isInitialLoading && |
| isSlotMachineVisible |
| ) { |
| return ( |
| <Box sx={{ width: "100%", height: "100vh" }}> |
| <UniverseSlotMachine |
| style={slotMachineState.style} |
| genre={slotMachineState.genre} |
| epoch={slotMachineState.epoch} |
| activeIndex={slotMachineState.activeIndex} |
| onComplete={() => { |
| setIsInitialLoading(false); |
| setIsSlotMachineVisible(false); |
| setIsTransitionLoading(true); |
| }} |
| /> |
| </Box> |
| ); |
| } |
|
|
| return ( |
| <motion.div |
| initial={{ opacity: 0 }} |
| animate={{ opacity: 1 }} |
| exit={{ opacity: 0 }} |
| transition={{ duration: 0.3, ease: "easeInOut" }} |
| style={{ width: "100%" }} |
| > |
| <Box |
| sx={{ |
| height: "100vh", |
| width: "100vw", |
| position: "relative", |
| overflow: "hidden", |
| }} |
| > |
| <GameNavigation /> |
| {/* Main content */} |
| <Box |
| ref={storyContainerRef} |
| sx={{ |
| height: "100%", |
| width: "100%", |
| position: "relative", |
| }} |
| > |
| {error ? ( |
| <ErrorDisplay error={error} onRetry={resetGame} /> |
| ) : showSlotMachine ? ( |
| <UniverseSlotMachine state={slotMachineState} /> |
| ) : ( |
| <Box |
| sx={{ |
| height: "100%", |
| width: "100%", |
| display: "flex", |
| flexDirection: "column", |
| position: "relative", |
| }} |
| > |
| <Box |
| sx={{ |
| height: "85%", |
| width: "100%", |
| overflow: "auto", |
| }} |
| > |
| <ComicLayout /> |
| </Box> |
| <Box |
| sx={{ |
| height: "15%", |
| width: "100%", |
| display: "flex", |
| justifyContent: "center", |
| alignItems: "center", |
| px: 2, |
| }} |
| > |
| <Box sx={{ width: "100%", maxWidth: "800px" }}> |
| <StoryChoices /> |
| </Box> |
| </Box> |
| </Box> |
| )} |
| </Box> |
| |
| {isDebugVisible && ( |
| <GameDebugPanel |
| gameState={gameState} |
| storySegments={segments} |
| currentChoices={choices} |
| showChoices={showChoices} |
| isLoading={isLoading} |
| /> |
| )} |
| |
| {/* Sarah chat interface */} |
| <TalkWithSarah /> |
| </Box> |
| </motion.div> |
| ); |
| } |
|
|
| export function Game() { |
| return ( |
| <GameProvider> |
| <GameContent /> |
| </GameProvider> |
| ); |
| } |
|
|
| export default Game; |
|
|