596
src/components/game-shell/PreGameSelectionFlow.tsx
Normal file
596
src/components/game-shell/PreGameSelectionFlow.tsx
Normal file
@@ -0,0 +1,596 @@
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||
import {
|
||||
readSavedCustomWorldProfiles,
|
||||
upsertSavedCustomWorldProfile,
|
||||
} from '../../data/customWorldLibrary';
|
||||
import { getScenePreset } from '../../data/scenePresets';
|
||||
import { generateCustomWorldProfile } from '../../services/ai';
|
||||
import {
|
||||
type CustomWorldProfile,
|
||||
type GameState,
|
||||
WorldType,
|
||||
} from '../../types';
|
||||
import {
|
||||
CHROME_ICONS,
|
||||
getNineSliceStyle,
|
||||
UI_CHROME,
|
||||
WORLD_SELECT_ICONS,
|
||||
} from '../../uiAssets';
|
||||
import { CustomWorldResultView } from '../CustomWorldResultView';
|
||||
import { DeveloperTeamModal } from '../DeveloperTeamModal';
|
||||
import { PixelIcon } from '../PixelIcon';
|
||||
import { CustomWorldCreatorModal } from '../SelectionCustomizationModals';
|
||||
|
||||
export type SelectionStage = 'start' | 'world' | 'custom-world-result';
|
||||
|
||||
type WorldOnlineCounts = Partial<Record<WorldType, number>>;
|
||||
|
||||
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 getCustomWorldGenerationLabel(progress: number) {
|
||||
if (progress >= 96) return '正在完成世界归档...';
|
||||
if (progress >= 78) return '正在关联地标和关键物品...';
|
||||
if (progress >= 52) return '正在生成核心角色...';
|
||||
if (progress >= 28) return '正在生成可玩角色...';
|
||||
return '正在解析世界设置...';
|
||||
}
|
||||
|
||||
function getCustomWorldProgressLabel(progress: number) {
|
||||
if (progress >= 96) return '正在完成世界归档...';
|
||||
if (progress >= 78) return '正在组合场景和视觉效果...';
|
||||
if (progress >= 52) return '正在生成核心角色...';
|
||||
if (progress >= 28) return '正在生成可玩角色...';
|
||||
return '正在解析世界设置...';
|
||||
}
|
||||
|
||||
export function PreGameSelectionFlow({
|
||||
selectionStage,
|
||||
setSelectionStage,
|
||||
gameState,
|
||||
hasSavedGame,
|
||||
handleContinueGame,
|
||||
handleStartNewGame,
|
||||
handleWorldSelect,
|
||||
}: PreGameSelectionFlowProps) {
|
||||
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
|
||||
useState<GameState['customWorldProfile']>(null);
|
||||
const [savedCustomWorldProfiles, setSavedCustomWorldProfiles] = useState<
|
||||
CustomWorldProfile[]
|
||||
>(() => readSavedCustomWorldProfiles());
|
||||
const [showDeveloperTeamModal, setShowDeveloperTeamModal] = useState(false);
|
||||
const [worldOnlineCounts, setWorldOnlineCounts] = useState<WorldOnlineCounts>(
|
||||
() => generateWorldOnlineCounts(),
|
||||
);
|
||||
const [showCustomWorldModal, setShowCustomWorldModal] = useState(false);
|
||||
const [customWorldDraft, setCustomWorldDraft] = useState('');
|
||||
const [customWorldError, setCustomWorldError] = useState<string | null>(null);
|
||||
const [isGeneratingCustomWorld, setIsGeneratingCustomWorld] = useState(false);
|
||||
const [customWorldProgress, setCustomWorldProgress] = useState(0);
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gameState.worldType && selectionStage === 'world') {
|
||||
setWorldOnlineCounts(generateWorldOnlineCounts());
|
||||
}
|
||||
}, [gameState.worldType, selectionStage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectionStage === 'custom-world-result' &&
|
||||
!generatedCustomWorldProfile
|
||||
) {
|
||||
setSelectionStage('world');
|
||||
}
|
||||
}, [generatedCustomWorldProfile, selectionStage, setSelectionStage]);
|
||||
|
||||
const leaveCustomWorldResult = () => {
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(0);
|
||||
setSelectionStage('world');
|
||||
};
|
||||
|
||||
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(0);
|
||||
setSelectionStage('world');
|
||||
};
|
||||
|
||||
const createCustomWorld = async () => {
|
||||
const settingText = customWorldDraft.trim();
|
||||
if (!settingText) {
|
||||
setCustomWorldError('请先输入世界设置。');
|
||||
return;
|
||||
}
|
||||
|
||||
setCustomWorldError(null);
|
||||
setIsGeneratingCustomWorld(true);
|
||||
setCustomWorldProgress(8);
|
||||
|
||||
const progressTimer = window.setInterval(() => {
|
||||
setCustomWorldProgress((current) => {
|
||||
if (current >= 92) return current;
|
||||
return Math.min(
|
||||
92,
|
||||
current + Math.max(3, Math.round((96 - current) / 5)),
|
||||
);
|
||||
});
|
||||
}, 260);
|
||||
|
||||
try {
|
||||
const profile = await generateCustomWorldProfile(settingText);
|
||||
window.clearInterval(progressTimer);
|
||||
setCustomWorldProgress(100);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 180));
|
||||
setGeneratedCustomWorldProfile(profile);
|
||||
setShowCustomWorldModal(false);
|
||||
setCustomWorldError(null);
|
||||
setSelectionStage('custom-world-result');
|
||||
} catch (error) {
|
||||
window.clearInterval(progressTimer);
|
||||
setCustomWorldProgress(0);
|
||||
setCustomWorldError(
|
||||
error instanceof Error ? error.message : '生成自定义世界失败。',
|
||||
);
|
||||
} finally {
|
||||
setIsGeneratingCustomWorld(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence mode="wait">
|
||||
{!gameState.worldType && selectionStage === 'start' && (
|
||||
<motion.div
|
||||
key="start-screen"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<div className="flex h-full w-full max-w-sm flex-col gap-5 py-4 sm:py-6">
|
||||
<div className="flex min-h-0 flex-1 items-center">
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
{hasSavedGame && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleContinueGame}
|
||||
className="pixel-nine-slice pixel-pressable w-full text-left"
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, {
|
||||
paddingX: 18,
|
||||
paddingY: 13,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-base font-semibold text-white">
|
||||
继续游戏
|
||||
</span>
|
||||
<span className="text-white/60">开始</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleStartNewGame();
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldDraft('');
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(0);
|
||||
setShowCustomWorldModal(false);
|
||||
setSelectionStage('world');
|
||||
}}
|
||||
className="pixel-nine-slice pixel-pressable w-full text-left"
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, {
|
||||
paddingX: 18,
|
||||
paddingY: 13,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-base font-semibold text-white">
|
||||
{hasSavedGame ? '新游戏' : '开始游戏'}
|
||||
</span>
|
||||
<span className="text-white/60">开始</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDeveloperTeamModal(true)}
|
||||
className="pixel-nine-slice pixel-pressable w-full text-left"
|
||||
style={getNineSliceStyle(UI_CHROME.choiceButton, {
|
||||
paddingX: 18,
|
||||
paddingY: 13,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-base font-semibold text-white">
|
||||
开发团队
|
||||
</span>
|
||||
<span className="text-white/60">查看</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pixel-nine-slice pixel-panel w-full"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
paddingX: 12,
|
||||
paddingY: 10,
|
||||
})}
|
||||
>
|
||||
<div className="text-[10px] font-bold tracking-[0.2em] text-emerald-200/75">
|
||||
联系方式
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{START_SCREEN_CONTACTS.map((contact) => (
|
||||
<div
|
||||
key={contact.label}
|
||||
className="flex items-center justify-between gap-3 rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-sm text-zinc-200"
|
||||
>
|
||||
<span className="text-zinc-400">
|
||||
{contact.label}
|
||||
</span>
|
||||
<span className="font-semibold text-white">
|
||||
{contact.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{!gameState.worldType && selectionStage === 'world' && (
|
||||
<motion.div
|
||||
key="world-select"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-bold tracking-[0.2em] text-zinc-400">
|
||||
选择世界
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setSelectionStage('start');
|
||||
}}
|
||||
className="rounded-full border border-white/10 bg-black/18 px-3 py-1.5 text-[11px] text-zinc-300 transition-colors hover:text-white"
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||
<div className="grid gap-3 pb-1 md:grid-cols-2 xl:grid-cols-3">
|
||||
{worldCards.map((world) => (
|
||||
<button
|
||||
key={world.id}
|
||||
type="button"
|
||||
onClick={() => handleWorldSelect(world.id)}
|
||||
className="pixel-nine-slice pixel-pressable order-2 relative flex min-h-[12.5rem] flex-col items-start justify-between overflow-hidden text-left"
|
||||
style={getNineSliceStyle(world.texture, {
|
||||
paddingX: 18,
|
||||
paddingY: 16,
|
||||
})}
|
||||
>
|
||||
{world.sceneImage && (
|
||||
<img
|
||||
src={world.sceneImage}
|
||||
alt={world.subtitle}
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-25"
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(8,10,14,0.12),rgba(8,10,14,0.82))]" />
|
||||
<div className="absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-black/25">
|
||||
<PixelIcon
|
||||
src={world.featureIcon}
|
||||
className="h-5 w-5 opacity-95"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 flex h-full w-full flex-col">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[10px] tracking-[0.2em] text-zinc-100">
|
||||
{world.name}
|
||||
</div>
|
||||
<PixelIcon
|
||||
src={world.icon}
|
||||
className="h-10 w-10 opacity-95"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<div className="text-3xl font-black text-white">
|
||||
{world.subtitle}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="rounded-full border border-emerald-400/20 bg-emerald-500/10 px-3 py-1 text-[10px] text-emerald-100">
|
||||
在线 {world.onlineCount}
|
||||
</span>
|
||||
<span className="rounded-full border border-white/10 bg-black/24 px-2.5 py-1 text-[10px] text-zinc-100">
|
||||
推荐
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{savedCustomWorldCards.map((world) => (
|
||||
<button
|
||||
key={world.id}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleWorldSelect(WorldType.CUSTOM, world.profile)
|
||||
}
|
||||
className="pixel-nine-slice pixel-pressable order-1 relative flex min-h-[12.5rem] flex-col items-start justify-between overflow-hidden text-left"
|
||||
style={getNineSliceStyle(world.texture, {
|
||||
paddingX: 18,
|
||||
paddingY: 16,
|
||||
})}
|
||||
>
|
||||
{world.sceneImage && (
|
||||
<img
|
||||
src={world.sceneImage}
|
||||
alt={world.profile.name}
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-25"
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(8,10,14,0.14),rgba(8,10,14,0.84))]" />
|
||||
<div className="relative z-10 flex h-full w-full flex-col">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="rounded-full border border-sky-300/20 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-sky-100">
|
||||
已保存
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-black/24 px-2.5 py-1 text-[10px] text-zinc-100">
|
||||
{world.accentLabel === '武侠基础'
|
||||
? '武侠'
|
||||
: '仙侠'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<div className="text-2xl font-black text-white sm:text-[1.7rem]">
|
||||
{world.profile.name}
|
||||
</div>
|
||||
<div className="mt-2 line-clamp-2 max-w-[18rem] text-xs leading-5 text-zinc-200/90">
|
||||
{world.profile.summary}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<span className="rounded-full border border-emerald-400/20 bg-emerald-500/10 px-3 py-1 text-[10px] text-emerald-100">
|
||||
可玩角色 {world.profile.playableNpcs.length}
|
||||
</span>
|
||||
<span className="rounded-full border border-white/10 bg-black/24 px-2.5 py-1 text-[10px] text-zinc-100">
|
||||
地标 {world.profile.landmarks.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(0);
|
||||
setShowCustomWorldModal(true);
|
||||
}}
|
||||
className="pixel-nine-slice pixel-pressable order-first relative flex min-h-[12.5rem] flex-col items-start justify-between overflow-hidden text-left"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
paddingX: 18,
|
||||
paddingY: 16,
|
||||
})}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(125,211,252,0.18),transparent_42%),linear-gradient(180deg,rgba(8,10,14,0.18),rgba(8,10,14,0.82))]" />
|
||||
<div className="absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-full border border-sky-300/20 bg-sky-500/10">
|
||||
<PixelIcon
|
||||
src={CHROME_ICONS.refreshOptions}
|
||||
className="h-5 w-5 opacity-95"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 flex h-full w-full flex-col">
|
||||
<div className="rounded-full border border-sky-300/20 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-sky-100">
|
||||
自定义
|
||||
</div>
|
||||
<div className="mt-auto">
|
||||
<div className="text-3xl font-black text-white">
|
||||
创建自定义世界
|
||||
</div>
|
||||
<div className="mt-2 max-w-[16rem] text-sm leading-6 text-zinc-300">
|
||||
输入世界设置,让系统生成可玩角色、场景角色、物品和地标。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{!gameState.worldType &&
|
||||
selectionStage === 'custom-world-result' &&
|
||||
generatedCustomWorldProfile && (
|
||||
<motion.div
|
||||
key="custom-world-result"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<CustomWorldResultView
|
||||
profile={generatedCustomWorldProfile}
|
||||
previewCharacters={previewCustomWorldCharacters}
|
||||
isGenerating={isGeneratingCustomWorld}
|
||||
progress={customWorldProgress}
|
||||
progressLabel={getCustomWorldProgressLabel(customWorldProgress)}
|
||||
error={customWorldError}
|
||||
onProfileChange={setGeneratedCustomWorldProfile}
|
||||
onBack={leaveCustomWorldResult}
|
||||
onEditSetting={() => {
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(0);
|
||||
setShowCustomWorldModal(true);
|
||||
}}
|
||||
onRegenerate={() => {
|
||||
void createCustomWorld();
|
||||
}}
|
||||
onSave={saveGeneratedCustomWorld}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<CustomWorldCreatorModal
|
||||
isOpen={showCustomWorldModal}
|
||||
draft={customWorldDraft}
|
||||
onDraftChange={(value) => {
|
||||
setCustomWorldDraft(value);
|
||||
if (customWorldError) setCustomWorldError(null);
|
||||
}}
|
||||
onClose={() => {
|
||||
if (isGeneratingCustomWorld) return;
|
||||
setShowCustomWorldModal(false);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
void createCustomWorld();
|
||||
}}
|
||||
isGenerating={isGeneratingCustomWorld}
|
||||
progress={customWorldProgress}
|
||||
progressLabel={getCustomWorldGenerationLabel(customWorldProgress)}
|
||||
error={customWorldError}
|
||||
/>
|
||||
|
||||
<DeveloperTeamModal
|
||||
isOpen={showDeveloperTeamModal}
|
||||
message={DEVELOPER_TEAM_MESSAGE}
|
||||
onClose={() => setShowDeveloperTeamModal(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user