import { AnimatePresence, motion } from 'motion/react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { buildCustomWorldPlayableCharacters, } from '../../data/characterPresets'; import { readSavedCustomWorldProfiles, upsertSavedCustomWorldProfile, } from '../../data/customWorldLibrary'; import { getScenePreset } from '../../data/scenePresets'; import { type CustomWorldGenerationProgress, generateCustomWorldProfile, } from '../../services/ai'; import { buildCustomWorldCreatorIntentDisplayText, buildCustomWorldCreatorIntentGenerationText, createEmptyCustomWorldCreatorIntent, } from '../../services/customWorldCreatorIntent'; import { type CustomWorldCreatorIntent, type CustomWorldGenerationMode, type CustomWorldProfile, type GameState, WorldType, } from '../../types'; import { CHROME_ICONS, getNineSliceStyle, UI_CHROME, WORLD_SELECT_ICONS, } from '../../uiAssets'; import { CustomWorldGenerationView } from '../CustomWorldGenerationView'; import { CustomWorldResultView } from '../CustomWorldResultView'; import { DeveloperTeamModal } from '../DeveloperTeamModal'; import { PixelIcon } from '../PixelIcon'; import { CustomWorldCreatorModal } from '../SelectionCustomizationModals'; export type SelectionStage = | 'start' | 'world' | 'custom-world-generating' | 'custom-world-result'; type WorldOnlineCounts = Partial>; type PreGameSelectionFlowProps = { selectionStage: SelectionStage; setSelectionStage: (stage: SelectionStage) => void; gameState: GameState; hasSavedGame: boolean; handleContinueGame: () => void; handleStartNewGame: () => void; handleWorldSelect: ( type: WorldType, customWorldProfile?: GameState['customWorldProfile'], ) => void; }; const DEVELOPER_TEAM_MESSAGE = '\u7a0b\u7b56\u7f8e\uff1a\u53d9\u4e16AI \u5305\u4ef2\u822a\n\u5408\u4f5c\u8bf7\u8054\u7cfb\u5fae\u4fe1\uff1abzh253518756'; const START_SCREEN_CONTACTS = [ { label: 'QQ群', value: '1094580241' }, { label: '微信', value: 'bzh253518756' }, ] as const; const WORLD_OPTIONS = [ { id: WorldType.WUXIA, name: '武侠', subtitle: '刀剑江湖', icon: WORLD_SELECT_ICONS.wuxia, texture: UI_CHROME.worldButtonWuxia, }, { id: WorldType.XIANXIA, name: '仙侠', subtitle: '云灵仙境', icon: WORLD_SELECT_ICONS.xianxia, texture: UI_CHROME.worldButtonXianxia, }, ] as const; function generateWorldOnlineCounts(): WorldOnlineCounts { const roll = (base: number) => Math.max(100, Math.min(200, base + Math.floor(Math.random() * 19) - 9)); return { [WorldType.WUXIA]: roll(146), [WorldType.XIANXIA]: roll(173), }; } function buildLockedSeedNameSets(profile: CustomWorldProfile) { const lockedCharacterNames = new Set( profile.creatorIntent?.keyCharacters .filter((entry) => entry.locked) .map((entry) => entry.name.trim()) .filter(Boolean) ?? [], ); const lockedLandmarkNames = new Set( profile.creatorIntent?.keyLandmarks .filter((entry) => entry.locked) .map((entry) => entry.name.trim()) .filter(Boolean) ?? [], ); return { lockedCharacterNames, lockedLandmarkNames, }; } function mergeLockedProfileContent( currentProfile: CustomWorldProfile, nextProfile: CustomWorldProfile, ) { const { lockedCharacterNames, lockedLandmarkNames } = buildLockedSeedNameSets(currentProfile); const nextPlayableNpcs = nextProfile.playableNpcs.map((npc) => { if (!lockedCharacterNames.has(npc.name.trim())) { return npc; } return ( currentProfile.playableNpcs.find( (currentNpc) => currentNpc.name.trim() === npc.name.trim(), ) ?? npc ); }); const nextStoryNpcs = nextProfile.storyNpcs.map((npc) => { if (!lockedCharacterNames.has(npc.name.trim())) { return npc; } return ( currentProfile.storyNpcs.find( (currentNpc) => currentNpc.name.trim() === npc.name.trim(), ) ?? npc ); }); const nextLandmarks = nextProfile.landmarks.map((landmark) => { if (!lockedLandmarkNames.has(landmark.name.trim())) { return landmark; } return ( currentProfile.landmarks.find( (currentLandmark) => currentLandmark.name.trim() === landmark.name.trim(), ) ?? landmark ); }); return { ...nextProfile, playableNpcs: nextPlayableNpcs, storyNpcs: nextStoryNpcs, landmarks: nextLandmarks, } satisfies CustomWorldProfile; } export function PreGameSelectionFlow({ selectionStage, setSelectionStage, gameState, hasSavedGame, handleContinueGame, handleStartNewGame, handleWorldSelect, }: PreGameSelectionFlowProps) { const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] = useState(null); const [savedCustomWorldProfiles, setSavedCustomWorldProfiles] = useState< CustomWorldProfile[] >(() => readSavedCustomWorldProfiles()); const [showDeveloperTeamModal, setShowDeveloperTeamModal] = useState(false); const [worldOnlineCounts, setWorldOnlineCounts] = useState( () => generateWorldOnlineCounts(), ); const [showCustomWorldModal, setShowCustomWorldModal] = useState(false); const [customWorldCreatorIntent, setCustomWorldCreatorIntent] = useState(() => createEmptyCustomWorldCreatorIntent('freeform'), ); const [customWorldGenerationMode, setCustomWorldGenerationMode] = useState('fast'); const [customWorldError, setCustomWorldError] = useState(null); const [isGeneratingCustomWorld, setIsGeneratingCustomWorld] = useState(false); const [customWorldProgress, setCustomWorldProgress] = useState(null); const customWorldAbortControllerRef = useRef(null); const previewCustomWorldCharacters = useMemo( () => generatedCustomWorldProfile ? buildCustomWorldPlayableCharacters(generatedCustomWorldProfile) : [], [generatedCustomWorldProfile], ); const worldCards = useMemo( () => WORLD_OPTIONS.map((world, index) => ({ ...world, sceneImage: getScenePreset(world.id, index + 1)?.imageSrc ?? getScenePreset(world.id, 0)?.imageSrc ?? '', featureIcon: world.id === WorldType.WUXIA ? '/Icons/03_Torch.png' : '/Icons/19_Mana_potion.png', onlineCount: worldOnlineCounts[world.id] ?? 0, })), [worldOnlineCounts], ); const savedCustomWorldCards = useMemo( () => savedCustomWorldProfiles.map((profile, index) => { const anchorWorldType = profile.templateWorldType; const leadCharacter = buildCustomWorldPlayableCharacters(profile)[0] ?? null; return { id: profile.id, profile, texture: anchorWorldType === WorldType.WUXIA ? UI_CHROME.worldButtonWuxia : UI_CHROME.worldButtonXianxia, sceneImage: profile.landmarks[0]?.imageSrc ?? getScenePreset(anchorWorldType, (index % 3) + 1)?.imageSrc ?? getScenePreset(anchorWorldType, 0)?.imageSrc ?? '', featurePortrait: leadCharacter?.portrait ?? '', featureIcon: anchorWorldType === WorldType.WUXIA ? WORLD_SELECT_ICONS.wuxia : WORLD_SELECT_ICONS.xianxia, accentLabel: anchorWorldType === WorldType.WUXIA ? '武侠基础' : '仙侠基础', }; }), [savedCustomWorldProfiles], ); const customWorldSettingPreview = useMemo(() => { if (customWorldCreatorIntent.sourceMode === 'freeform') { return customWorldCreatorIntent.rawSettingText.trim(); } const intentSummary = buildCustomWorldCreatorIntentDisplayText( customWorldCreatorIntent, ).trim(); if (intentSummary) { return intentSummary; } return customWorldCreatorIntent.rawSettingText.trim(); }, [customWorldCreatorIntent]); useEffect(() => { if (!gameState.worldType && selectionStage === 'world') { setWorldOnlineCounts(generateWorldOnlineCounts()); } }, [gameState.worldType, selectionStage]); useEffect(() => { if ( selectionStage === 'custom-world-result' && !generatedCustomWorldProfile ) { setSelectionStage('world'); } }, [generatedCustomWorldProfile, selectionStage, setSelectionStage]); useEffect( () => () => { customWorldAbortControllerRef.current?.abort(); }, [], ); const leaveCustomWorldResult = () => { setGeneratedCustomWorldProfile(null); setCustomWorldError(null); setCustomWorldProgress(null); setSelectionStage('world'); }; const leaveCustomWorldGeneration = () => { if (isGeneratingCustomWorld) { return; } setCustomWorldError(null); setCustomWorldProgress(null); setSelectionStage('world'); }; const openCustomWorldCreator = () => { if (isGeneratingCustomWorld) { return; } setCustomWorldError(null); setCustomWorldProgress(null); setShowCustomWorldModal(true); }; const editCustomWorldSetting = () => { if (isGeneratingCustomWorld) { return; } if (generatedCustomWorldProfile) { setCustomWorldCreatorIntent( generatedCustomWorldProfile.creatorIntent ?? ({ ...createEmptyCustomWorldCreatorIntent('freeform'), rawSettingText: generatedCustomWorldProfile.settingText, } satisfies CustomWorldCreatorIntent), ); setCustomWorldGenerationMode( generatedCustomWorldProfile.generationMode ?? 'full', ); } setCustomWorldError(null); setCustomWorldProgress(null); setSelectionStage('world'); setShowCustomWorldModal(true); }; const saveGeneratedCustomWorld = () => { if (!generatedCustomWorldProfile) { return; } try { setSavedCustomWorldProfiles( upsertSavedCustomWorldProfile(generatedCustomWorldProfile), ); } catch (error) { setCustomWorldError( error instanceof Error ? error.message : '本地保存自定义世界失败。', ); return; } handleWorldSelect(WorldType.CUSTOM, generatedCustomWorldProfile); setGeneratedCustomWorldProfile(null); setCustomWorldError(null); setCustomWorldProgress(null); setSelectionStage('world'); }; const openSavedCustomWorldEditor = (profile: CustomWorldProfile) => { if (isGeneratingCustomWorld) { return; } setGeneratedCustomWorldProfile(profile); setCustomWorldCreatorIntent( profile.creatorIntent ?? ({ ...createEmptyCustomWorldCreatorIntent('freeform'), rawSettingText: profile.settingText, } satisfies CustomWorldCreatorIntent), ); setCustomWorldGenerationMode(profile.generationMode ?? 'full'); setCustomWorldError(null); setCustomWorldProgress(null); setSelectionStage('custom-world-result'); }; const regenerateFromCurrentProfile = async ( applyProfile: ( currentProfile: CustomWorldProfile, regeneratedProfile: CustomWorldProfile, ) => CustomWorldProfile, options: { confirmMessage: string; generationMode?: CustomWorldGenerationMode; }, ) => { if (!generatedCustomWorldProfile || isGeneratingCustomWorld) { return; } const confirmed = window.confirm(options.confirmMessage); if (!confirmed) { return; } const abortController = new AbortController(); customWorldAbortControllerRef.current?.abort(); customWorldAbortControllerRef.current = abortController; setIsGeneratingCustomWorld(true); setCustomWorldError(null); try { const regeneratedProfile = await generateCustomWorldProfile( { settingText: generatedCustomWorldProfile.settingText.trim() || customWorldSettingPreview, creatorIntent: generatedCustomWorldProfile.creatorIntent, generationMode: options.generationMode ?? generatedCustomWorldProfile.generationMode ?? 'full', }, { signal: abortController.signal, onProgress: setCustomWorldProgress, }, ); if (abortController.signal.aborted) { return; } const mergedProfile = applyProfile( generatedCustomWorldProfile, mergeLockedProfileContent(generatedCustomWorldProfile, regeneratedProfile), ); setGeneratedCustomWorldProfile(mergedProfile); setCustomWorldProgress(null); setCustomWorldError(null); } catch (error) { if (abortController.signal.aborted) { setCustomWorldError('世界生成已中断。你可以重新尝试本次操作。'); return; } setCustomWorldError( error instanceof Error ? error.message : '局部重生成失败。', ); } finally { if (customWorldAbortControllerRef.current === abortController) { customWorldAbortControllerRef.current = null; } setIsGeneratingCustomWorld(false); } }; const continueExpandCustomWorld = async () => { await regenerateFromCurrentProfile( (_currentProfile, regeneratedProfile) => ({ ...regeneratedProfile, generationMode: 'full', generationStatus: 'complete', }), { confirmMessage: '确认继续补全当前世界吗?系统会在保留已锁定锚点的前提下,继续生成长尾角色和场景网络。', generationMode: 'full', }, ); }; const regeneratePlayableNpc = async (id: string) => { await regenerateFromCurrentProfile( (currentProfile, regeneratedProfile) => { const targetIndex = currentProfile.playableNpcs.findIndex( (entry) => entry.id === id, ); if (targetIndex < 0) { return currentProfile; } const nextNpc = regeneratedProfile.playableNpcs[targetIndex] ?? regeneratedProfile.playableNpcs.find( (entry) => entry.name === currentProfile.playableNpcs[targetIndex]?.name, ); if (!nextNpc) { return currentProfile; } return { ...currentProfile, playableNpcs: currentProfile.playableNpcs.map((entry, index) => index === targetIndex ? nextNpc : entry, ), }; }, { confirmMessage: '确认重新生成这个可扮演角色吗?当前角色的 AI 生成内容会被替换。', generationMode: 'full', }, ); }; const regenerateStoryNpc = async (id: string) => { await regenerateFromCurrentProfile( (currentProfile, regeneratedProfile) => { const targetIndex = currentProfile.storyNpcs.findIndex( (entry) => entry.id === id, ); if (targetIndex < 0) { return currentProfile; } const nextNpc = regeneratedProfile.storyNpcs[targetIndex] ?? regeneratedProfile.storyNpcs.find( (entry) => entry.name === currentProfile.storyNpcs[targetIndex]?.name, ); if (!nextNpc) { return currentProfile; } const nextStoryNpcs = currentProfile.storyNpcs.map((entry, index) => index === targetIndex ? nextNpc : entry, ); return { ...currentProfile, storyNpcs: nextStoryNpcs, landmarks: currentProfile.landmarks.map((landmark) => ({ ...landmark, sceneNpcIds: landmark.sceneNpcIds.map((npcId) => npcId === id ? nextNpc.id : npcId, ), })), }; }, { confirmMessage: '确认重新生成这个场景角色吗?当前角色的 AI 生成内容会被替换。', generationMode: 'full', }, ); }; const regenerateLandmark = async (id: string) => { await regenerateFromCurrentProfile( (currentProfile, regeneratedProfile) => { const targetIndex = currentProfile.landmarks.findIndex( (entry) => entry.id === id, ); if (targetIndex < 0) { return currentProfile; } const nextLandmark = regeneratedProfile.landmarks[targetIndex] ?? regeneratedProfile.landmarks.find( (entry) => entry.name === currentProfile.landmarks[targetIndex]?.name, ); if (!nextLandmark) { return currentProfile; } return { ...currentProfile, landmarks: currentProfile.landmarks.map((entry, index) => index === targetIndex ? nextLandmark : entry, ), }; }, { confirmMessage: '确认重新生成这个关键地点吗?当前场景的 AI 生成内容会被替换。', generationMode: 'full', }, ); }; const regenerateStoryExpansion = async () => { await regenerateFromCurrentProfile( (currentProfile, regeneratedProfile) => ({ ...currentProfile, storyNpcs: regeneratedProfile.storyNpcs, }), { confirmMessage: '确认重新生成长尾场景角色吗?已锁定锚点会保留,其余场景角色会被新的生成结果替换。', generationMode: 'full', }, ); }; const regenerateLandmarkNetwork = async () => { await regenerateFromCurrentProfile( (currentProfile, regeneratedProfile) => ({ ...currentProfile, landmarks: currentProfile.landmarks.map((landmark, index) => ({ ...landmark, sceneNpcIds: regeneratedProfile.landmarks[index]?.sceneNpcIds ?? landmark.sceneNpcIds, connections: regeneratedProfile.landmarks[index]?.connections ?? landmark.connections, })), }), { confirmMessage: '确认重新生成场景网络吗?已锁定场景名称与描述会保留,但 NPC 分布和连接关系会按最新结果刷新。', generationMode: 'full', }, ); }; const createCustomWorld = async () => { if (isGeneratingCustomWorld) { return; } const generationText = buildCustomWorldCreatorIntentGenerationText( customWorldCreatorIntent, ).trim() || customWorldCreatorIntent.rawSettingText.trim(); const settingText = customWorldSettingPreview.trim() || generationText; if (!generationText) { setCustomWorldError( customWorldCreatorIntent.sourceMode === 'card' ? '请至少填写一个世界锚点。' : '请先输入世界设置。', ); return; } const abortController = new AbortController(); customWorldAbortControllerRef.current?.abort(); customWorldAbortControllerRef.current = abortController; setCustomWorldError(null); setGeneratedCustomWorldProfile(null); setCustomWorldProgress(null); setShowCustomWorldModal(false); setSelectionStage('custom-world-generating'); setIsGeneratingCustomWorld(true); try { const profile = await generateCustomWorldProfile( { settingText, creatorIntent: customWorldCreatorIntent, generationMode: customWorldGenerationMode, }, { signal: abortController.signal, onProgress: setCustomWorldProgress, }, ); if (abortController.signal.aborted) { return; } const persistedProfile = generatedCustomWorldProfile ? { ...profile, id: generatedCustomWorldProfile.id, } : profile; const savedProfiles = upsertSavedCustomWorldProfile(persistedProfile); setSavedCustomWorldProfiles(savedProfiles); setGeneratedCustomWorldProfile(null); setCustomWorldError(null); setCustomWorldProgress(null); setSelectionStage('world'); } catch (error) { if (abortController.signal.aborted) { setCustomWorldError('世界生成已中断。你可以返回修改设定,或重新开始。'); return; } setCustomWorldError( error instanceof Error ? error.message : '生成自定义世界失败。', ); } finally { if (customWorldAbortControllerRef.current === abortController) { customWorldAbortControllerRef.current = null; } setIsGeneratingCustomWorld(false); } }; const interruptCustomWorldGeneration = () => { if (!isGeneratingCustomWorld || !customWorldAbortControllerRef.current) { return; } const confirmed = window.confirm( '确认中断当前世界生成吗?本轮未完成的内容不会保留。', ); if (!confirmed) { return; } customWorldAbortControllerRef.current.abort(new Error('世界生成已中断。')); }; return ( <> {!gameState.worldType && selectionStage === 'start' && (
{hasSavedGame && ( )}
联系方式
{START_SCREEN_CONTACTS.map((contact) => (
{contact.label} {contact.value}
))}
)} {!gameState.worldType && selectionStage === 'world' && (
选择世界
{worldCards.map((world) => ( ))} {savedCustomWorldCards.map((world) => (
))}
)} {!gameState.worldType && selectionStage === 'custom-world-generating' && ( { void createCustomWorld(); }} onInterrupt={interruptCustomWorldGeneration} /> )} {!gameState.worldType && selectionStage === 'custom-world-result' && generatedCustomWorldProfile && ( { void createCustomWorld(); }} onContinueExpand={() => { void continueExpandCustomWorld(); }} onRegeneratePlayableNpc={(id) => { void regeneratePlayableNpc(id); }} onRegenerateStoryNpc={(id) => { void regenerateStoryNpc(id); }} onRegenerateLandmark={(id) => { void regenerateLandmark(id); }} onRegenerateStoryExpansion={() => { void regenerateStoryExpansion(); }} onRegenerateLandmarkNetwork={() => { void regenerateLandmarkNetwork(); }} onSave={saveGeneratedCustomWorld} /> )}
{ setCustomWorldCreatorIntent(value); if (customWorldError) setCustomWorldError(null); }} generationMode={customWorldGenerationMode} onGenerationModeChange={setCustomWorldGenerationMode} onClose={() => { if (isGeneratingCustomWorld) return; setShowCustomWorldModal(false); }} onSubmit={() => { void createCustomWorld(); }} isGenerating={isGeneratingCustomWorld} progress={customWorldProgress?.overallProgress ?? 0} progressLabel={customWorldProgress?.phaseLabel ?? '正在准备生成'} error={customWorldError} /> setShowDeveloperTeamModal(false)} /> ); }