import {useCallback, useEffect, useMemo, useState} from 'react'; import {getLiveGamePlayTimeMs} from '../../data/runtimeStats'; import {getWorldCampScenePreset} from '../../data/scenePresets'; import type {StoryOption} from '../../types'; import {UI_CHROME} from '../../uiAssets'; import {GameShellCanvasStage} from './GameShellCanvasStage'; import {GameShellMainContent} from './GameShellMainContent'; import {GameShellOverlays} from './GameShellOverlays'; import {type GameShellProps} from './types'; import {useGameShellViewModel} from './useGameShellViewModel'; import {SCENE_TRANSITION_FUNCTION_MODES, useSceneTransitionModel} from './useSceneTransitionModel'; export function GameShellRuntime({session, story, entry, companions, audio}: GameShellProps) { const { gameState, currentStory, isLoading, aiError, bottomTab, setBottomTab, isMapOpen, setIsMapOpen, } = session; const { displayedOptions, canRefreshOptions, handleRefreshOptions, handleChoice, handleMapTravelToScene, npcUi, characterChatUi, inventoryUi, battleRewardUi, questUi, } = story; const { hasSavedGame, handleContinueGame, handleStartNewGame, handleSaveAndExit, handleWorldSelect, handleBackToWorldSelect, handleCharacterSelect, } = entry; const {companionRenderStates, onBenchCompanion, onActivateRosterCompanion} = companions; const {musicVolume, onMusicVolumeChange} = audio; const [clockNow, setClockNow] = useState(() => Date.now()); const openingCampSceneId = useMemo( () => (gameState.worldType ? getWorldCampScenePreset(gameState.worldType)?.id ?? null : null), [gameState.worldType], ); const hasNpcModalOpen = Boolean(npcUi.tradeModal || npcUi.giftModal || npcUi.recruitModal); const { selectionStage, setSelectionStage, overlayPanel, openOverlayPanel, closeOverlayPanel, selectedSceneEntity, setSelectedSceneEntity, openPartyMemberDetails, closeAdventureEntityModal, showTeamModal, openCampModal, closeCampModal, resetForSaveAndExit, shouldMountAdventureEntityModal, shouldMountCampModal, shouldMountMapModal, shouldMountCharacterChatModal, shouldMountNpcModals, } = useGameShellViewModel({ gameState, isMapOpen, characterChatModalOpen: Boolean(characterChatUi.modal), hasNpcModalOpen, }); const { visibleGameState, visibleCurrentStory, sceneTransitionPhase, sceneTransitionToken, setSceneTransitionDurations, beginSceneTransition, } = useSceneTransitionModel({ gameState, currentStory, openingCampSceneId, }); const isCharacterSelectionStage = gameState.currentScene === 'Selection' && Boolean(gameState.worldType) && !gameState.playerCharacter; const shouldHideStoryOptions = sceneTransitionPhase !== 'idle'; const hideSelectionHero = gameState.currentScene === 'Selection' && selectionStage !== 'start'; const dialogueIndicator = useMemo(() => { if (!isLoading || visibleCurrentStory?.displayMode !== 'dialogue' || visibleGameState.currentEncounter?.kind !== 'npc') { return null; } const lastSpeaker = visibleCurrentStory.dialogue?.[visibleCurrentStory.dialogue.length - 1]?.speaker ?? null; return { showPlayer: true, showEncounter: true, activeSpeaker: lastSpeaker === 'player' ? 'player' : lastSpeaker ? 'npc' : null, } as const; }, [visibleCurrentStory?.dialogue, visibleCurrentStory?.displayMode, visibleGameState.currentEncounter?.kind, isLoading]); const characterChatSummaries = useMemo( () => Object.fromEntries( Object.entries(gameState.characterChats ?? {}).map(([characterId, record]) => [characterId, record.summary]), ), [gameState.characterChats], ); const canvasCompanionRenderStates = useMemo(() => { const activeEncounterNpcId = visibleGameState.currentEncounter?.kind === 'npc' ? visibleGameState.currentEncounter.id ?? null : null; if (!activeEncounterNpcId) return companionRenderStates; return companionRenderStates.filter(companion => companion.npcId !== activeEncounterNpcId); }, [companionRenderStates, visibleGameState.currentEncounter]); const livePlayTimeMs = useMemo( () => getLiveGamePlayTimeMs(gameState.runtimeStats, clockNow), [clockNow, gameState.runtimeStats], ); const adventureStatistics = useMemo( () => ({ playTimeMs: livePlayTimeMs, hostileNpcsDefeated: gameState.runtimeStats.hostileNpcsDefeated, questsAccepted: gameState.runtimeStats.questsAccepted, questsCompleted: visibleGameState.quests.filter(quest => quest.status === 'completed' || quest.status === 'turned_in').length, questsTurnedIn: visibleGameState.quests.filter(quest => quest.status === 'turned_in').length, itemsUsed: gameState.runtimeStats.itemsUsed, scenesTraveled: gameState.runtimeStats.scenesTraveled, currentSceneName: visibleGameState.currentScenePreset?.name ?? 'ε½εεΊε', playerCurrency: visibleGameState.playerCurrency, inventoryItemCount: visibleGameState.playerInventory.reduce((sum, item) => sum + item.quantity, 0), inventoryStackCount: visibleGameState.playerInventory.length, activeCompanionCount: visibleGameState.companions.length, rosterCompanionCount: visibleGameState.roster.length, }), [ gameState.runtimeStats.itemsUsed, gameState.runtimeStats.hostileNpcsDefeated, gameState.runtimeStats.questsAccepted, gameState.runtimeStats.scenesTraveled, livePlayTimeMs, visibleGameState.companions.length, visibleGameState.currentScenePreset?.name, visibleGameState.playerCurrency, visibleGameState.playerInventory, visibleGameState.quests, visibleGameState.roster.length, ], ); useEffect(() => { if (!gameState.playerCharacter || gameState.currentScene !== 'Story') { return; } setClockNow(Date.now()); const intervalId = window.setInterval(() => setClockNow(Date.now()), 1000); return () => window.clearInterval(intervalId); }, [gameState.currentScene, gameState.playerCharacter]); const handleSceneTransitionChoice = useCallback((option: StoryOption) => { const transitionMode = SCENE_TRANSITION_FUNCTION_MODES[option.functionId]; if (transitionMode) { beginSceneTransition(transitionMode); } handleChoice(option); }, [beginSceneTransition, handleChoice]); return (