750 lines
26 KiB
TypeScript
750 lines
26 KiB
TypeScript
import { AnimatePresence, motion } from 'motion/react';
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import type {
|
|
CustomWorldAgentActionRequest,
|
|
CustomWorldAgentOperationRecord,
|
|
CustomWorldAgentSessionSnapshot,
|
|
CustomWorldWorkSummary,
|
|
SendCustomWorldAgentMessageRequest,
|
|
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
|
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
|
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
|
import {
|
|
createCustomWorldAgentSession,
|
|
executeCustomWorldAgentAction,
|
|
getCustomWorldAgentOperation,
|
|
getCustomWorldAgentSession,
|
|
listCustomWorldWorks,
|
|
sendCustomWorldAgentMessage,
|
|
} from '../../services/aiService';
|
|
import {
|
|
clearCustomWorldAgentUiState,
|
|
readCustomWorldAgentUiState,
|
|
writeCustomWorldAgentUiState,
|
|
} from '../../services/customWorldAgentUiState';
|
|
import { detectCustomWorldThemeMode } from '../../services/customWorldTheme';
|
|
import { listCustomWorldLibrary } from '../../services/storageService';
|
|
import { type CustomWorldProfile, type GameState } from '../../types';
|
|
import {
|
|
CHROME_ICONS,
|
|
CUSTOM_WORLD_THEME_ICONS,
|
|
getNineSliceStyle,
|
|
UI_CHROME,
|
|
} from '../../uiAssets';
|
|
import { CustomWorldAgentWorkspace } from '../custom-world-agent/CustomWorldAgentWorkspace';
|
|
import { CustomWorldCreationHub } from '../custom-world-home/CustomWorldCreationHub';
|
|
import { DeveloperTeamModal } from '../DeveloperTeamModal';
|
|
import { PixelIcon } from '../PixelIcon';
|
|
|
|
export type SelectionStage =
|
|
| 'start'
|
|
| 'world'
|
|
| 'custom-world-home'
|
|
| 'custom-world-agent';
|
|
|
|
type PreGameSelectionFlowProps = {
|
|
selectionStage: SelectionStage;
|
|
setSelectionStage: (stage: SelectionStage) => void;
|
|
gameState: GameState;
|
|
hasSavedGame: boolean;
|
|
handleContinueGame: () => void;
|
|
handleStartNewGame: () => void;
|
|
handleCustomWorldSelect: (customWorldProfile: 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;
|
|
|
|
function createOperationErrorBanner(
|
|
message: string,
|
|
): CustomWorldAgentOperationRecord {
|
|
return {
|
|
operationId: `operation-error-${Date.now()}`,
|
|
type: 'process_message',
|
|
status: 'failed',
|
|
phaseLabel: '处理失败',
|
|
phaseDetail: message,
|
|
progress: 100,
|
|
error: message,
|
|
};
|
|
}
|
|
|
|
export function PreGameSelectionFlow({
|
|
selectionStage,
|
|
setSelectionStage,
|
|
gameState,
|
|
hasSavedGame,
|
|
handleContinueGame,
|
|
handleStartNewGame,
|
|
handleCustomWorldSelect,
|
|
}: PreGameSelectionFlowProps) {
|
|
const [savedCustomWorldProfiles, setSavedCustomWorldProfiles] = useState<
|
|
CustomWorldProfile[]
|
|
>([]);
|
|
const [customWorldWorks, setCustomWorldWorks] = useState<
|
|
CustomWorldWorkSummary[]
|
|
>([]);
|
|
const [isLoadingCustomWorldWorks, setIsLoadingCustomWorldWorks] =
|
|
useState(false);
|
|
const [customWorldWorksError, setCustomWorldWorksError] =
|
|
useState<string | null>(null);
|
|
const [showDeveloperTeamModal, setShowDeveloperTeamModal] = useState(false);
|
|
const [isCreatingCustomWorldWork, setIsCreatingCustomWorldWork] =
|
|
useState(false);
|
|
const [isRestoringAgentSession, setIsRestoringAgentSession] = useState(false);
|
|
const [activeCustomWorldAgentSessionId, setActiveCustomWorldAgentSessionId] =
|
|
useState<string | null>(null);
|
|
const [
|
|
activeCustomWorldAgentOperationId,
|
|
setActiveCustomWorldAgentOperationId,
|
|
] = useState<string | null>(null);
|
|
const [activeCustomWorldAgentSession, setActiveCustomWorldAgentSession] =
|
|
useState<CustomWorldAgentSessionSnapshot | null>(null);
|
|
const [activeCustomWorldAgentOperation, setActiveCustomWorldAgentOperation] =
|
|
useState<CustomWorldAgentOperationRecord | null>(null);
|
|
const clearOperationTimeoutRef = useRef<number | null>(null);
|
|
|
|
const savedCustomWorldCards = useMemo(
|
|
() =>
|
|
savedCustomWorldProfiles.map((profile) => {
|
|
const themeMode = detectCustomWorldThemeMode(profile);
|
|
const leadCharacter =
|
|
buildCustomWorldPlayableCharacters(profile)[0] ?? null;
|
|
|
|
return {
|
|
id: profile.id,
|
|
profile,
|
|
texture: UI_CHROME.panel,
|
|
sceneImage: resolveCustomWorldCampSceneImage(profile) ?? '',
|
|
featurePortrait: leadCharacter?.portrait ?? '',
|
|
featureIcon:
|
|
themeMode === 'martial'
|
|
? CUSTOM_WORLD_THEME_ICONS.martial
|
|
: themeMode === 'arcane'
|
|
? CUSTOM_WORLD_THEME_ICONS.arcane
|
|
: CHROME_ICONS.refreshOptions,
|
|
accentLabel: '自定义世界',
|
|
};
|
|
}),
|
|
[savedCustomWorldProfiles],
|
|
);
|
|
|
|
const refreshCustomWorldLibrary = useCallback(async () => {
|
|
try {
|
|
setSavedCustomWorldProfiles(await listCustomWorldLibrary());
|
|
} catch (error) {
|
|
console.warn(
|
|
'[PreGameSelectionFlow] failed to load custom world library',
|
|
error,
|
|
);
|
|
}
|
|
}, []);
|
|
|
|
const refreshCustomWorldWorks = useCallback(async () => {
|
|
setIsLoadingCustomWorldWorks(true);
|
|
try {
|
|
setCustomWorldWorks(await listCustomWorldWorks());
|
|
setCustomWorldWorksError(null);
|
|
} catch (error) {
|
|
setCustomWorldWorksError(
|
|
error instanceof Error ? error.message : '读取创作作品失败。',
|
|
);
|
|
} finally {
|
|
setIsLoadingCustomWorldWorks(false);
|
|
}
|
|
}, []);
|
|
|
|
const refreshCustomWorldHomeData = useCallback(async () => {
|
|
await Promise.all([refreshCustomWorldLibrary(), refreshCustomWorldWorks()]);
|
|
}, [refreshCustomWorldLibrary, refreshCustomWorldWorks]);
|
|
|
|
const syncCustomWorldAgentUiState = useCallback(
|
|
(sessionId: string | null, operationId: string | null) => {
|
|
setActiveCustomWorldAgentSessionId(sessionId);
|
|
setActiveCustomWorldAgentOperationId(operationId);
|
|
writeCustomWorldAgentUiState({
|
|
activeSessionId: sessionId,
|
|
activeOperationId: operationId,
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const scheduleClearOperationBanner = useCallback(() => {
|
|
if (clearOperationTimeoutRef.current) {
|
|
window.clearTimeout(clearOperationTimeoutRef.current);
|
|
}
|
|
|
|
clearOperationTimeoutRef.current = window.setTimeout(() => {
|
|
setActiveCustomWorldAgentOperation(null);
|
|
setActiveCustomWorldAgentOperationId(null);
|
|
writeCustomWorldAgentUiState({
|
|
activeSessionId: activeCustomWorldAgentSessionId,
|
|
activeOperationId: null,
|
|
});
|
|
clearOperationTimeoutRef.current = null;
|
|
}, 1400);
|
|
}, [activeCustomWorldAgentSessionId]);
|
|
|
|
const refreshActiveCustomWorldAgentSession = useCallback(
|
|
async (sessionId: string) => {
|
|
const session = await getCustomWorldAgentSession(sessionId);
|
|
setActiveCustomWorldAgentSession(session);
|
|
return session;
|
|
},
|
|
[],
|
|
);
|
|
|
|
const loadCustomWorldAgentSession = useCallback(
|
|
async (sessionId: string, operationId?: string | null) => {
|
|
setIsRestoringAgentSession(true);
|
|
setActiveCustomWorldAgentSession(null);
|
|
try {
|
|
const session = await getCustomWorldAgentSession(sessionId);
|
|
setActiveCustomWorldAgentSession(session);
|
|
setActiveCustomWorldAgentOperation(null);
|
|
syncCustomWorldAgentUiState(sessionId, operationId ?? null);
|
|
setSelectionStage('custom-world-agent');
|
|
|
|
if (operationId) {
|
|
try {
|
|
const operation = await getCustomWorldAgentOperation(
|
|
sessionId,
|
|
operationId,
|
|
);
|
|
setActiveCustomWorldAgentOperation(operation);
|
|
} catch (error) {
|
|
setActiveCustomWorldAgentOperation(
|
|
createOperationErrorBanner(
|
|
error instanceof Error ? error.message : '读取操作状态失败。',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
clearCustomWorldAgentUiState();
|
|
syncCustomWorldAgentUiState(null, null);
|
|
setSelectionStage('custom-world-home');
|
|
setCustomWorldWorksError(
|
|
error instanceof Error ? error.message : '恢复共创会话失败。',
|
|
);
|
|
} finally {
|
|
setIsRestoringAgentSession(false);
|
|
}
|
|
},
|
|
[setSelectionStage, syncCustomWorldAgentUiState],
|
|
);
|
|
|
|
const leaveCustomWorldAgentWorkspace = useCallback(async () => {
|
|
clearCustomWorldAgentUiState();
|
|
syncCustomWorldAgentUiState(null, null);
|
|
setActiveCustomWorldAgentSession(null);
|
|
setActiveCustomWorldAgentOperation(null);
|
|
setSelectionStage('custom-world-home');
|
|
await refreshCustomWorldHomeData();
|
|
}, [
|
|
refreshCustomWorldHomeData,
|
|
setSelectionStage,
|
|
syncCustomWorldAgentUiState,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
void refreshCustomWorldHomeData();
|
|
const restoredState = readCustomWorldAgentUiState();
|
|
|
|
if (!gameState.worldType && restoredState.activeSessionId) {
|
|
void loadCustomWorldAgentSession(
|
|
restoredState.activeSessionId,
|
|
restoredState.activeOperationId ?? null,
|
|
);
|
|
}
|
|
}, [
|
|
gameState.worldType,
|
|
loadCustomWorldAgentSession,
|
|
refreshCustomWorldHomeData,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!gameState.worldType && selectionStage === 'custom-world-home') {
|
|
void refreshCustomWorldHomeData();
|
|
}
|
|
}, [gameState.worldType, refreshCustomWorldHomeData, selectionStage]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!activeCustomWorldAgentSessionId ||
|
|
!activeCustomWorldAgentOperationId ||
|
|
!activeCustomWorldAgentOperation ||
|
|
(activeCustomWorldAgentOperation.status !== 'queued' &&
|
|
activeCustomWorldAgentOperation.status !== 'running')
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const timeoutId = window.setTimeout(async () => {
|
|
try {
|
|
const operation = await getCustomWorldAgentOperation(
|
|
activeCustomWorldAgentSessionId,
|
|
activeCustomWorldAgentOperationId,
|
|
);
|
|
setActiveCustomWorldAgentOperation(operation);
|
|
|
|
if (
|
|
operation.status === 'completed' ||
|
|
operation.status === 'failed'
|
|
) {
|
|
await refreshActiveCustomWorldAgentSession(
|
|
activeCustomWorldAgentSessionId,
|
|
);
|
|
await refreshCustomWorldWorks();
|
|
|
|
if (operation.status === 'completed') {
|
|
scheduleClearOperationBanner();
|
|
} else {
|
|
setActiveCustomWorldAgentOperationId(null);
|
|
writeCustomWorldAgentUiState({
|
|
activeSessionId: activeCustomWorldAgentSessionId,
|
|
activeOperationId: null,
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
setActiveCustomWorldAgentOperation(
|
|
createOperationErrorBanner(
|
|
error instanceof Error ? error.message : '读取操作状态失败。',
|
|
),
|
|
);
|
|
}
|
|
}, 500);
|
|
|
|
return () => window.clearTimeout(timeoutId);
|
|
}, [
|
|
activeCustomWorldAgentOperation,
|
|
activeCustomWorldAgentOperationId,
|
|
activeCustomWorldAgentSessionId,
|
|
refreshActiveCustomWorldAgentSession,
|
|
refreshCustomWorldWorks,
|
|
scheduleClearOperationBanner,
|
|
]);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
if (clearOperationTimeoutRef.current) {
|
|
window.clearTimeout(clearOperationTimeoutRef.current);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
const openCustomWorldCreator = () => {
|
|
setSelectionStage('custom-world-home');
|
|
};
|
|
|
|
const startNewGame = () => {
|
|
handleStartNewGame();
|
|
clearCustomWorldAgentUiState();
|
|
syncCustomWorldAgentUiState(null, null);
|
|
setActiveCustomWorldAgentSession(null);
|
|
setActiveCustomWorldAgentOperation(null);
|
|
setSelectionStage('world');
|
|
};
|
|
|
|
const handleCreateNewWork = async () => {
|
|
if (isCreatingCustomWorldWork) {
|
|
return;
|
|
}
|
|
|
|
setIsCreatingCustomWorldWork(true);
|
|
setCustomWorldWorksError(null);
|
|
try {
|
|
const response = await createCustomWorldAgentSession({});
|
|
|
|
setActiveCustomWorldAgentSession(response.session);
|
|
setActiveCustomWorldAgentOperation(null);
|
|
syncCustomWorldAgentUiState(response.session.sessionId, null);
|
|
setSelectionStage('custom-world-agent');
|
|
await refreshCustomWorldWorks();
|
|
} catch (error) {
|
|
setCustomWorldWorksError(
|
|
error instanceof Error ? error.message : '创建共创会话失败。',
|
|
);
|
|
} finally {
|
|
setIsCreatingCustomWorldWork(false);
|
|
}
|
|
};
|
|
|
|
const handleResumeDraft = async (sessionId: string) => {
|
|
await loadCustomWorldAgentSession(sessionId, null);
|
|
};
|
|
|
|
const handleEnterPublished = async (profileId: string) => {
|
|
let profile =
|
|
savedCustomWorldProfiles.find((item) => item.id === profileId) ?? null;
|
|
|
|
if (!profile) {
|
|
const profiles = await listCustomWorldLibrary();
|
|
setSavedCustomWorldProfiles(profiles);
|
|
profile = profiles.find((item) => item.id === profileId) ?? null;
|
|
}
|
|
|
|
if (!profile) {
|
|
setCustomWorldWorksError('读取已发布世界失败。');
|
|
return;
|
|
}
|
|
|
|
handleCustomWorldSelect(profile);
|
|
};
|
|
|
|
const handleSubmitCustomWorldAgentMessage = async (
|
|
payload: SendCustomWorldAgentMessageRequest,
|
|
) => {
|
|
if (!activeCustomWorldAgentSessionId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await sendCustomWorldAgentMessage(
|
|
activeCustomWorldAgentSessionId,
|
|
payload,
|
|
);
|
|
setActiveCustomWorldAgentOperation(response.operation);
|
|
syncCustomWorldAgentUiState(
|
|
activeCustomWorldAgentSessionId,
|
|
response.operation.operationId,
|
|
);
|
|
await refreshActiveCustomWorldAgentSession(activeCustomWorldAgentSessionId);
|
|
} catch (error) {
|
|
setActiveCustomWorldAgentOperation(
|
|
createOperationErrorBanner(
|
|
error instanceof Error ? error.message : '发送共创消息失败。',
|
|
),
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleExecuteCustomWorldAgentAction = async (
|
|
payload: CustomWorldAgentActionRequest,
|
|
) => {
|
|
if (!activeCustomWorldAgentSessionId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await executeCustomWorldAgentAction(
|
|
activeCustomWorldAgentSessionId,
|
|
payload,
|
|
);
|
|
setActiveCustomWorldAgentOperation(response.operation);
|
|
syncCustomWorldAgentUiState(
|
|
activeCustomWorldAgentSessionId,
|
|
response.operation.operationId,
|
|
);
|
|
await refreshActiveCustomWorldAgentSession(activeCustomWorldAgentSessionId);
|
|
} catch (error) {
|
|
setActiveCustomWorldAgentOperation(
|
|
createOperationErrorBanner(
|
|
error instanceof Error ? error.message : '执行共创动作失败。',
|
|
),
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleRefreshCustomWorldAgentSession = async () => {
|
|
if (!activeCustomWorldAgentSessionId) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await refreshActiveCustomWorldAgentSession(activeCustomWorldAgentSessionId);
|
|
|
|
if (activeCustomWorldAgentOperationId) {
|
|
const operation = await getCustomWorldAgentOperation(
|
|
activeCustomWorldAgentSessionId,
|
|
activeCustomWorldAgentOperationId,
|
|
);
|
|
setActiveCustomWorldAgentOperation(operation);
|
|
}
|
|
} catch (error) {
|
|
setActiveCustomWorldAgentOperation(
|
|
createOperationErrorBanner(
|
|
error instanceof Error ? error.message : '刷新会话失败。',
|
|
),
|
|
);
|
|
}
|
|
};
|
|
|
|
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>
|
|
) : null}
|
|
|
|
<button
|
|
type="button"
|
|
onClick={startNewGame}
|
|
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={() => 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">
|
|
<button
|
|
type="button"
|
|
onClick={openCustomWorldCreator}
|
|
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>
|
|
</div>
|
|
</button>
|
|
|
|
{savedCustomWorldCards.map((world) => (
|
|
<button
|
|
key={world.id}
|
|
type="button"
|
|
onClick={() => handleCustomWorldSelect(world.profile)}
|
|
className="pixel-nine-slice pixel-pressable 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' }}
|
|
/>
|
|
) : null}
|
|
<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-emerald-300/20 bg-emerald-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-emerald-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>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{!gameState.worldType && selectionStage === 'custom-world-home' && (
|
|
<motion.div
|
|
key="custom-world-home"
|
|
initial={{ opacity: 0, y: 12 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -12 }}
|
|
className="flex h-full min-h-0 flex-col"
|
|
>
|
|
<CustomWorldCreationHub
|
|
items={customWorldWorks}
|
|
loading={isLoadingCustomWorldWorks}
|
|
error={customWorldWorksError}
|
|
onBack={() => setSelectionStage('world')}
|
|
onRetry={() => {
|
|
void refreshCustomWorldHomeData();
|
|
}}
|
|
onCreateNew={() => {
|
|
void handleCreateNewWork();
|
|
}}
|
|
onResumeDraft={(sessionId) => {
|
|
void handleResumeDraft(sessionId);
|
|
}}
|
|
onEnterPublished={(profileId) => {
|
|
void handleEnterPublished(profileId);
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
|
|
{!gameState.worldType && selectionStage === 'custom-world-agent' && (
|
|
<motion.div
|
|
key="custom-world-agent"
|
|
initial={{ opacity: 0, y: 12 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -12 }}
|
|
className="flex h-full min-h-0 flex-col"
|
|
>
|
|
<CustomWorldAgentWorkspace
|
|
session={
|
|
isRestoringAgentSession ? null : activeCustomWorldAgentSession
|
|
}
|
|
activeOperation={activeCustomWorldAgentOperation}
|
|
onBack={() => {
|
|
void leaveCustomWorldAgentWorkspace();
|
|
}}
|
|
onRefresh={() => {
|
|
void handleRefreshCustomWorldAgentSession();
|
|
}}
|
|
onSubmitMessage={(payload) => {
|
|
void handleSubmitCustomWorldAgentMessage(payload);
|
|
}}
|
|
onExecuteAction={(payload) => {
|
|
void handleExecuteCustomWorldAgentAction(payload);
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<DeveloperTeamModal
|
|
isOpen={showDeveloperTeamModal}
|
|
message={DEVELOPER_TEAM_MESSAGE}
|
|
onClose={() => setShowDeveloperTeamModal(false)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|