import { useCallback, useEffect, useMemo, useState } from 'react'; import { getLiveGamePlayTimeMs } from '../../data/runtimeStats'; import { getWorldCampScenePreset } from '../../data/scenePresets'; import type { CharacterChatRecord, CompanionRenderState, GameState, StoryMoment, StoryOption, } from '../../types'; import type { GameShellAdventureStatistics, GameShellDialogueIndicator, GameShellProps, } from './types'; import { useGameShellViewModel } from './useGameShellViewModel'; import { SCENE_TRANSITION_FUNCTION_MODES, useSceneTransitionModel, } from './useSceneTransitionModel'; export function buildGameShellDialogueIndicator(params: { isLoading: boolean; visibleGameState: GameState; visibleCurrentStory: StoryMoment | null; }): GameShellDialogueIndicator | null { const { isLoading, visibleGameState, visibleCurrentStory } = params; 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, }; } export function buildCharacterChatSummaries( characterChats: Record | undefined, ) { return Object.fromEntries( Object.entries(characterChats ?? {}).map(([characterId, record]) => [ characterId, record.summary, ]), ); } export function buildCanvasCompanionRenderStates(params: { visibleCompanionRenderStates: CompanionRenderState[]; visibleGameState: GameState; }) { const activeEncounterNpcId = params.visibleGameState.currentEncounter?.kind === 'npc' ? params.visibleGameState.currentEncounter.id ?? null : null; if (!activeEncounterNpcId) { return params.visibleCompanionRenderStates; } return params.visibleCompanionRenderStates.filter( (companion) => companion.npcId !== activeEncounterNpcId, ); } export function buildAdventureStatistics(params: { gameState: GameState; visibleGameState: GameState; livePlayTimeMs: number; }): GameShellAdventureStatistics { const { gameState, visibleGameState, livePlayTimeMs } = params; return { 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, }; } export function useGameShellRuntimeViewModel(params: Pick< GameShellProps, 'session' | 'story' | 'companions' >) { const { session, story, companions } = params; const { gameState, currentStory, isLoading, isMapOpen, } = session; const { npcUi, characterChatUi, handleChoice } = story; const { buildCompanionRenderStates } = companions; 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 shellViewModel = useGameShellViewModel({ gameState, isMapOpen, characterChatModalOpen: Boolean(characterChatUi.modal), hasNpcModalOpen, }); const sceneTransitionModel = useSceneTransitionModel({ gameState, currentStory, openingCampSceneId, }); const { visibleGameState, visibleCurrentStory, sceneTransitionPhase, beginSceneTransition, } = sceneTransitionModel; const isCharacterSelectionStage = gameState.currentScene === 'Selection' && Boolean(gameState.worldType) && !gameState.playerCharacter; const shouldHideStoryOptions = sceneTransitionPhase !== 'idle'; const hideSelectionHero = gameState.currentScene === 'Selection' && shellViewModel.selectionStage !== 'start'; const dialogueIndicator = useMemo( () => buildGameShellDialogueIndicator({ isLoading, visibleGameState, visibleCurrentStory, }), [isLoading, visibleCurrentStory, visibleGameState], ); const characterChatSummaries = useMemo( () => buildCharacterChatSummaries(gameState.characterChats), [gameState.characterChats], ); const visibleCompanionRenderStates = useMemo( () => buildCompanionRenderStates(visibleGameState), [buildCompanionRenderStates, visibleGameState], ); const canvasCompanionRenderStates = useMemo( () => buildCanvasCompanionRenderStates({ visibleCompanionRenderStates, visibleGameState, }), [visibleCompanionRenderStates, visibleGameState], ); const livePlayTimeMs = useMemo( () => getLiveGamePlayTimeMs(gameState.runtimeStats, clockNow), [clockNow, gameState.runtimeStats], ); const adventureStatistics = useMemo( () => buildAdventureStatistics({ gameState, visibleGameState, livePlayTimeMs, }), [gameState, livePlayTimeMs, visibleGameState], ); 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 { ...shellViewModel, ...sceneTransitionModel, isCharacterSelectionStage, shouldHideStoryOptions, hideSelectionHero, dialogueIndicator, characterChatSummaries, canvasCompanionRenderStates, adventureStatistics, handleSceneTransitionChoice, }; }