This commit is contained in:
2026-04-10 15:37:02 +08:00
parent 161cd32277
commit f19e482c8f
233 changed files with 43987 additions and 5127 deletions

View File

@@ -1,20 +1,13 @@
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';
import { UI_CHROME } from '../../uiAssets';
import { GameShellCanvasStage } from './GameShellCanvasStage';
import { GameShellMainContent } from './GameShellMainContent';
import { GameShellOverlays } from './GameShellOverlays';
import type { GameShellProps } from './types';
import { useGameShellRuntimeViewModel } from './useGameShellRuntimeViewModel';
export function GameShellRuntime({session, story, entry, companions, audio}: GameShellProps) {
const {
gameState,
currentStory,
isLoading,
aiError,
bottomTab,
@@ -26,7 +19,6 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
displayedOptions,
canRefreshOptions,
handleRefreshOptions,
handleChoice,
handleMapTravelToScene,
npcUi,
characterChatUi,
@@ -46,18 +38,10 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
} = entry;
const {
companionRenderStates,
buildCompanionRenderStates,
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,
@@ -77,120 +61,24 @@ export function GameShellRuntime({session, story, entry, companions, audio}: Gam
shouldMountMapModal,
shouldMountCharacterChatModal,
shouldMountNpcModals,
} = useGameShellViewModel({
gameState,
isMapOpen,
characterChatModalOpen: Boolean(characterChatUi.modal),
hasNpcModalOpen,
});
const {
visibleGameState,
visibleCurrentStory,
sceneTransitionPhase,
sceneTransitionToken,
setSceneTransitionDurations,
beginSceneTransition,
} = useSceneTransitionModel({
gameState,
currentStory,
openingCampSceneId,
isCharacterSelectionStage,
shouldHideStoryOptions,
hideSelectionHero,
dialogueIndicator,
characterChatSummaries,
canvasCompanionRenderStates,
adventureStatistics,
handleSceneTransitionChoice,
} = useGameShellRuntimeViewModel({
session,
story,
companions,
});
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 visibleCompanionRenderStates = useMemo(
() => buildCompanionRenderStates(visibleGameState),
[buildCompanionRenderStates, visibleGameState],
);
const canvasCompanionRenderStates = useMemo(() => {
const activeEncounterNpcId = visibleGameState.currentEncounter?.kind === 'npc'
? visibleGameState.currentEncounter.id ?? null
: null;
if (!activeEncounterNpcId) return visibleCompanionRenderStates;
return visibleCompanionRenderStates.filter(companion => companion.npcId !== activeEncounterNpcId);
}, [visibleCompanionRenderStates, 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 (
<div