|
|
import { |
|
|
createContext, |
|
|
useContext, |
|
|
useState, |
|
|
useCallback, |
|
|
useEffect, |
|
|
} from "react"; |
|
|
import { storyApi } from "../utils/api"; |
|
|
import { getNextLayoutType, LAYOUTS } from "../layouts/config"; |
|
|
|
|
|
|
|
|
const DISABLE_NARRATOR = true; |
|
|
|
|
|
const GameContext = createContext(null); |
|
|
|
|
|
export function GameProvider({ children }) { |
|
|
const [segments, setSegments] = useState([]); |
|
|
const [choices, setChoices] = useState([]); |
|
|
const [isLoading, setIsLoading] = useState(false); |
|
|
const [isNarratorSpeaking, setIsNarratorSpeaking] = useState(false); |
|
|
const [heroName, setHeroName] = useState(""); |
|
|
const [loadedPages, setLoadedPages] = useState(new Set()); |
|
|
const [showChoices, setShowChoices] = useState(true); |
|
|
const [showTransitionSpinner, setShowTransitionSpinner] = useState(false); |
|
|
const [error, setError] = useState(null); |
|
|
const [gameState, setGameState] = useState(null); |
|
|
const [currentStory, setCurrentStory] = useState(null); |
|
|
const [universe, setUniverse] = useState(null); |
|
|
const [slotMachineState, setSlotMachineState] = useState({ |
|
|
style: null, |
|
|
genre: null, |
|
|
epoch: null, |
|
|
activeIndex: -1, |
|
|
}); |
|
|
const [showSlotMachine, setShowSlotMachine] = useState(() => { |
|
|
return !localStorage.getItem("game_initialized"); |
|
|
}); |
|
|
const [isInitialLoading, setIsInitialLoading] = useState(true); |
|
|
const [showLoadingMessages, setShowLoadingMessages] = useState(false); |
|
|
const [isTransitionLoading, setIsTransitionLoading] = useState(false); |
|
|
const [layoutCounter, setLayoutCounter] = useState(0); |
|
|
|
|
|
|
|
|
const stopNarration = useCallback(() => { |
|
|
storyApi.stopNarration(); |
|
|
setIsNarratorSpeaking(false); |
|
|
}, []); |
|
|
|
|
|
const playNarration = useCallback(async (text) => { |
|
|
if (DISABLE_NARRATOR) return; |
|
|
|
|
|
try { |
|
|
setIsNarratorSpeaking(true); |
|
|
await storyApi.playNarration(text); |
|
|
} catch (error) { |
|
|
console.error("Error playing narration:", error); |
|
|
} finally { |
|
|
setIsNarratorSpeaking(false); |
|
|
} |
|
|
}, []); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
return () => { |
|
|
stopNarration(); |
|
|
}; |
|
|
}, [stopNarration]); |
|
|
|
|
|
|
|
|
const handlePageLoaded = useCallback((pageIndex) => { |
|
|
setLoadedPages((prev) => { |
|
|
const newSet = new Set(prev); |
|
|
newSet.add(pageIndex); |
|
|
return newSet; |
|
|
}); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const generateImagesForStory = useCallback( |
|
|
async (imagePrompts, segmentIndex, currentSegments) => { |
|
|
try { |
|
|
let localSegments = [...currentSegments]; |
|
|
const images = Array(imagePrompts.length).fill(null); |
|
|
|
|
|
|
|
|
const session_id = localSegments[segmentIndex].session_id; |
|
|
if (!session_id) { |
|
|
throw new Error("No session_id available for image generation"); |
|
|
} |
|
|
|
|
|
|
|
|
const layoutType = getNextLayoutType( |
|
|
layoutCounter, |
|
|
imagePrompts.length |
|
|
); |
|
|
setLayoutCounter((prev) => prev + 1); |
|
|
|
|
|
|
|
|
localSegments[segmentIndex] = { |
|
|
...localSegments[segmentIndex], |
|
|
layoutType, |
|
|
images: Array(imagePrompts.length).fill(null), |
|
|
imagePrompts, |
|
|
isLoading: true, |
|
|
}; |
|
|
|
|
|
|
|
|
setSegments([...localSegments]); |
|
|
setShowTransitionSpinner(false); |
|
|
|
|
|
|
|
|
for ( |
|
|
let promptIndex = 0; |
|
|
promptIndex < imagePrompts.length; |
|
|
promptIndex++ |
|
|
) { |
|
|
let retryCount = 0; |
|
|
const maxRetries = 3; |
|
|
let success = false; |
|
|
|
|
|
|
|
|
const panelDimensions = LAYOUTS[layoutType].panels[promptIndex]; |
|
|
if (!panelDimensions) { |
|
|
console.error( |
|
|
`No panel dimensions found for index ${promptIndex} in layout ${layoutType}` |
|
|
); |
|
|
continue; |
|
|
} |
|
|
|
|
|
while (retryCount < maxRetries && !success) { |
|
|
try { |
|
|
const result = await storyApi.generateImage( |
|
|
imagePrompts[promptIndex], |
|
|
panelDimensions.width, |
|
|
panelDimensions.height, |
|
|
session_id |
|
|
); |
|
|
|
|
|
if (!result) { |
|
|
throw new Error("Pas de résultat de génération d'image"); |
|
|
} |
|
|
|
|
|
if (result.success) { |
|
|
images[promptIndex] = result.image_base64; |
|
|
|
|
|
|
|
|
localSegments[segmentIndex] = { |
|
|
...localSegments[segmentIndex], |
|
|
images: [...images], |
|
|
isLoading: true, |
|
|
}; |
|
|
setSegments([...localSegments]); |
|
|
|
|
|
success = true; |
|
|
} else { |
|
|
console.warn( |
|
|
`Failed to generate image ${promptIndex + 1}, attempt ${ |
|
|
retryCount + 1 |
|
|
}` |
|
|
); |
|
|
retryCount++; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error( |
|
|
`Error generating image ${promptIndex + 1}:`, |
|
|
error |
|
|
); |
|
|
retryCount++; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
localSegments[segmentIndex] = { |
|
|
...localSegments[segmentIndex], |
|
|
isLoading: false, |
|
|
}; |
|
|
setSegments([...localSegments]); |
|
|
} catch (error) { |
|
|
console.error("Error in generateImagesForStory:", error); |
|
|
} |
|
|
}, |
|
|
[layoutCounter, setLayoutCounter, setSegments] |
|
|
); |
|
|
|
|
|
const regenerateImage = async (prompt, session_id) => { |
|
|
try { |
|
|
if (!session_id) { |
|
|
console.error("No session_id provided for image regeneration"); |
|
|
return null; |
|
|
} |
|
|
|
|
|
const response = await storyApi.generateImage( |
|
|
prompt, |
|
|
512, |
|
|
512, |
|
|
session_id |
|
|
); |
|
|
if (response.success && response.image_base64) { |
|
|
return response.image_base64; |
|
|
} |
|
|
return null; |
|
|
} catch (error) { |
|
|
console.error("Error regenerating image:", error); |
|
|
return null; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const handleChoice = useCallback( |
|
|
async (choiceId, customText) => { |
|
|
if (isLoading) return; |
|
|
|
|
|
|
|
|
stopNarration(); |
|
|
|
|
|
|
|
|
if (segments.length > 0) { |
|
|
setShowTransitionSpinner(true); |
|
|
} |
|
|
setIsLoading(true); |
|
|
setShowChoices(false); |
|
|
|
|
|
try { |
|
|
let response; |
|
|
if (choiceId === "custom") { |
|
|
response = await storyApi.makeCustomChoice( |
|
|
customText, |
|
|
universe?.session_id |
|
|
); |
|
|
} else { |
|
|
response = await storyApi.makeChoice(choiceId, universe?.session_id); |
|
|
} |
|
|
|
|
|
|
|
|
setChoices(response.choices); |
|
|
|
|
|
|
|
|
const formattedSegment = { |
|
|
text: response.story_text, |
|
|
rawText: response.story_text, |
|
|
choices: response.choices || [], |
|
|
isLoading: true, |
|
|
images: [], |
|
|
isDeath: response.is_death || false, |
|
|
isVictory: response.is_victory || false, |
|
|
time: response.time, |
|
|
location: response.location, |
|
|
session_id: universe?.session_id, |
|
|
is_last_step: response.is_last_step, |
|
|
hasBeenRead: false, |
|
|
}; |
|
|
|
|
|
|
|
|
if (!response.image_prompts || response.image_prompts.length === 0) { |
|
|
formattedSegment.isLoading = false; |
|
|
setSegments((prev) => [...prev, formattedSegment]); |
|
|
setIsLoading(false); |
|
|
setShowChoices(true); |
|
|
setShowTransitionSpinner(false); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const currentSegments = [...segments]; |
|
|
const newSegmentIndex = currentSegments.length; |
|
|
|
|
|
await generateImagesForStory(response.image_prompts, newSegmentIndex, [ |
|
|
...currentSegments, |
|
|
formattedSegment, |
|
|
]); |
|
|
|
|
|
|
|
|
setIsLoading(false); |
|
|
setShowChoices(true); |
|
|
} catch (error) { |
|
|
console.error("Error making choice:", error); |
|
|
setError(error); |
|
|
setIsLoading(false); |
|
|
setShowTransitionSpinner(false); |
|
|
setShowChoices(true); |
|
|
} |
|
|
}, |
|
|
[ |
|
|
isLoading, |
|
|
universe?.session_id, |
|
|
generateImagesForStory, |
|
|
segments, |
|
|
stopNarration, |
|
|
] |
|
|
); |
|
|
|
|
|
|
|
|
const resetGame = useCallback(() => { |
|
|
setSegments([]); |
|
|
setChoices([]); |
|
|
setIsLoading(false); |
|
|
setIsNarratorSpeaking(false); |
|
|
setLoadedPages(new Set()); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const getLastSegment = useCallback(() => { |
|
|
if (!segments || segments.length === 0) return null; |
|
|
return segments[segments.length - 1]; |
|
|
}, [segments]); |
|
|
|
|
|
|
|
|
const isGameOver = useCallback(() => { |
|
|
const lastSegment = getLastSegment(); |
|
|
return lastSegment?.isDeath || lastSegment?.isVictory; |
|
|
}, [getLastSegment]); |
|
|
|
|
|
const value = { |
|
|
|
|
|
segments, |
|
|
setSegments, |
|
|
choices, |
|
|
setChoices, |
|
|
isLoading, |
|
|
setIsLoading, |
|
|
isNarratorSpeaking, |
|
|
setIsNarratorSpeaking, |
|
|
heroName, |
|
|
setHeroName, |
|
|
loadedPages, |
|
|
showChoices, |
|
|
setShowChoices, |
|
|
showTransitionSpinner, |
|
|
error, |
|
|
setError, |
|
|
gameState, |
|
|
setGameState, |
|
|
currentStory, |
|
|
setCurrentStory, |
|
|
universe, |
|
|
setUniverse, |
|
|
slotMachineState, |
|
|
setSlotMachineState, |
|
|
showSlotMachine, |
|
|
setShowSlotMachine, |
|
|
isInitialLoading, |
|
|
setIsInitialLoading, |
|
|
showLoadingMessages, |
|
|
setShowLoadingMessages, |
|
|
isTransitionLoading, |
|
|
setIsTransitionLoading, |
|
|
layoutCounter, |
|
|
setLayoutCounter, |
|
|
|
|
|
|
|
|
handlePageLoaded, |
|
|
onChoice: handleChoice, |
|
|
stopNarration, |
|
|
playNarration, |
|
|
resetGame, |
|
|
getLastSegment, |
|
|
isGameOver, |
|
|
|
|
|
|
|
|
isPageLoaded: (pageIndex) => loadedPages.has(pageIndex), |
|
|
areAllPagesLoaded: (totalPages) => loadedPages.size === totalPages, |
|
|
generateImagesForStory, |
|
|
regenerateImage, |
|
|
}; |
|
|
|
|
|
return <GameContext.Provider value={value}>{children}</GameContext.Provider>; |
|
|
} |
|
|
|
|
|
export const useGame = () => { |
|
|
const context = useContext(GameContext); |
|
|
if (!context) { |
|
|
throw new Error("useGame must be used within a GameProvider"); |
|
|
} |
|
|
return context; |
|
|
}; |
|
|
|