Extend square-hole creation flow with visual asset timeout guard
This commit is contained in:
@@ -32,22 +32,6 @@ import type {
|
||||
Match3DWorkProfile,
|
||||
Match3DWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import type {
|
||||
CreateSquareHoleSessionRequest,
|
||||
ExecuteSquareHoleActionRequest,
|
||||
SendSquareHoleMessageRequest,
|
||||
SquareHoleActionResponse,
|
||||
SquareHoleSessionResponse,
|
||||
SquareHoleSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||
import type {
|
||||
DropSquareHoleShapeRequest,
|
||||
SquareHoleRunSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import type {
|
||||
SquareHoleWorkProfile,
|
||||
SquareHoleWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type {
|
||||
PuzzleAgentActionRequest,
|
||||
PuzzleAgentOperationRecord,
|
||||
@@ -72,6 +56,21 @@ import type {
|
||||
ProfileSaveArchiveResumeResponse,
|
||||
ProfileSaveArchiveSummary,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type {
|
||||
CreateSquareHoleSessionRequest,
|
||||
ExecuteSquareHoleActionRequest,
|
||||
SendSquareHoleMessageRequest,
|
||||
SquareHoleActionResponse,
|
||||
SquareHoleSessionResponse,
|
||||
SquareHoleSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||
import type {
|
||||
SquareHoleRunSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import type {
|
||||
SquareHoleWorkProfile,
|
||||
SquareHoleWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||
import {
|
||||
buildPublicWorkStagePath,
|
||||
@@ -123,6 +122,7 @@ import {
|
||||
buildBigFishGenerationAnchorEntries,
|
||||
buildMiniGameDraftGenerationProgress,
|
||||
buildPuzzleGenerationAnchorEntries,
|
||||
buildSquareHoleGenerationAnchorEntries,
|
||||
createMiniGameDraftGenerationState,
|
||||
type MiniGameDraftGenerationState,
|
||||
} from '../../services/miniGameDraftGenerationProgress';
|
||||
@@ -552,8 +552,12 @@ function mapPublicWorkDetailToSquareHoleWork(
|
||||
summary: entry.summaryText,
|
||||
tags: entry.themeTags,
|
||||
coverImageSrc: entry.coverImageSrc,
|
||||
shapeCount: 8,
|
||||
difficulty: 4,
|
||||
backgroundPrompt: entry.backgroundPrompt ?? '方洞挑战运行背景',
|
||||
backgroundImageSrc: entry.backgroundImageSrc ?? null,
|
||||
shapeOptions: entry.shapeOptions ?? [],
|
||||
holeOptions: entry.holeOptions ?? [],
|
||||
shapeCount: entry.shapeCount ?? 8,
|
||||
difficulty: entry.difficulty ?? 4,
|
||||
publicationStatus: 'published',
|
||||
playCount: entry.playCount ?? 0,
|
||||
updatedAt: entry.updatedAt,
|
||||
@@ -581,7 +585,11 @@ function buildSquareHoleProfileFromSession(
|
||||
twistRule: draft.twistRule,
|
||||
summary: draft.summary,
|
||||
tags: draft.tags,
|
||||
coverImageSrc: null,
|
||||
coverImageSrc: draft.coverImageSrc ?? null,
|
||||
backgroundPrompt: draft.backgroundPrompt,
|
||||
backgroundImageSrc: draft.backgroundImageSrc ?? null,
|
||||
shapeOptions: draft.shapeOptions,
|
||||
holeOptions: draft.holeOptions,
|
||||
shapeCount: draft.shapeCount,
|
||||
difficulty: draft.difficulty,
|
||||
publicationStatus: 'draft',
|
||||
@@ -608,13 +616,6 @@ function mergeBigFishWorkSummary(
|
||||
: current;
|
||||
}
|
||||
|
||||
function mergeSquareHoleWorkSummary(
|
||||
current: SquareHoleWorkSummary,
|
||||
updated: SquareHoleWorkSummary,
|
||||
): SquareHoleWorkSummary {
|
||||
return current.profileId === updated.profileId ? updated : current;
|
||||
}
|
||||
|
||||
async function resolvePublicWorkAuthorSummary(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
): Promise<PublicUserSummary | null> {
|
||||
@@ -1086,6 +1087,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
useState<SquareHoleRuntimeReturnStage>('square-hole-result');
|
||||
const [isSquareHoleLoadingLibrary, setIsSquareHoleLoadingLibrary] =
|
||||
useState(false);
|
||||
const [squareHoleGenerationState, setSquareHoleGenerationState] =
|
||||
useState<MiniGameDraftGenerationState | null>(null);
|
||||
const [bigFishRun, setBigFishRun] =
|
||||
useState<BigFishRuntimeSnapshotResponse | null>(null);
|
||||
const [bigFishRuntimeShare, setBigFishRuntimeShare] = useState<{
|
||||
@@ -1817,7 +1820,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
workspaceStage: 'square-hole-agent-workspace',
|
||||
resultStage: 'square-hole-result',
|
||||
platformStage: 'platform',
|
||||
isCompileAction: (payload) => payload.action === 'square_hole_compile_draft',
|
||||
isCompileAction: () => false,
|
||||
resolveErrorMessage: resolveSquareHoleErrorMessage,
|
||||
errorMessages: {
|
||||
open: '开启方洞挑战共创工作台失败。',
|
||||
@@ -1831,9 +1834,30 @@ export function PlatformEntryFlowShellImpl({
|
||||
onSessionOpened: () => {
|
||||
setShowCreationTypeModal(false);
|
||||
},
|
||||
beforeExecuteAction: ({ payload }) => {
|
||||
if (payload.action === 'square_hole_compile_draft') {
|
||||
setSquareHoleGenerationState(
|
||||
createMiniGameDraftGenerationState('square-hole'),
|
||||
);
|
||||
setSelectionStage('square-hole-generating');
|
||||
}
|
||||
if (payload.action === 'square_hole_generate_visual_assets') {
|
||||
setSquareHoleGenerationState((current) => ({
|
||||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||
phase: 'square-hole-cover',
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
error: null,
|
||||
}));
|
||||
setSelectionStage('square-hole-generating');
|
||||
}
|
||||
},
|
||||
onActionComplete: async ({ payload, response, setSession }) => {
|
||||
setSession(response.session);
|
||||
if (payload.action !== 'square_hole_compile_draft') {
|
||||
if (
|
||||
payload.action !== 'square_hole_compile_draft' &&
|
||||
payload.action !== 'square_hole_generate_visual_assets'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1843,12 +1867,79 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.action === 'square_hole_compile_draft') {
|
||||
try {
|
||||
const assetResponse = await squareHoleCreationClient.executeAction(
|
||||
response.session.sessionId,
|
||||
{
|
||||
action: 'square_hole_generate_visual_assets',
|
||||
},
|
||||
);
|
||||
setSession(assetResponse.session);
|
||||
const assetProfileId = assetResponse.session.draft?.profileId;
|
||||
if (!assetProfileId) {
|
||||
setSquareHoleProfile(
|
||||
buildSquareHoleProfileFromSession(assetResponse.session),
|
||||
);
|
||||
setSelectionStage('square-hole-result');
|
||||
return;
|
||||
}
|
||||
const { item } = await getSquareHoleWorkDetail(assetProfileId);
|
||||
setSquareHoleProfile(item);
|
||||
setSquareHoleGenerationState((current) => ({
|
||||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||
phase: 'ready',
|
||||
completedAssetCount: item.shapeOptions.length + 2,
|
||||
totalAssetCount: item.shapeOptions.length + 2,
|
||||
error: null,
|
||||
}));
|
||||
await refreshSquareHoleShelf().catch(() => undefined);
|
||||
setSelectionStage('square-hole-result');
|
||||
} catch (error) {
|
||||
const errorMessage = resolveSquareHoleErrorMessage(
|
||||
error,
|
||||
'生成方洞挑战图片失败。',
|
||||
);
|
||||
setSquareHoleError(errorMessage);
|
||||
setSquareHoleGenerationState((current) => ({
|
||||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||
phase: 'failed',
|
||||
error: errorMessage,
|
||||
}));
|
||||
setSquareHoleProfile(buildSquareHoleProfileFromSession(response.session));
|
||||
setSelectionStage('square-hole-generating');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { item } = await getSquareHoleWorkDetail(profileId);
|
||||
setSquareHoleProfile(item);
|
||||
setSquareHoleGenerationState((current) => ({
|
||||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||
phase: 'ready',
|
||||
completedAssetCount: item.shapeOptions.length + 2,
|
||||
totalAssetCount: item.shapeOptions.length + 2,
|
||||
error: null,
|
||||
}));
|
||||
await refreshSquareHoleShelf().catch(() => undefined);
|
||||
setSelectionStage('square-hole-result');
|
||||
} catch {
|
||||
setSquareHoleProfile(buildSquareHoleProfileFromSession(response.session));
|
||||
setSelectionStage('square-hole-result');
|
||||
}
|
||||
},
|
||||
onActionError: ({ payload, errorMessage }) => {
|
||||
if (
|
||||
payload.action === 'square_hole_compile_draft' ||
|
||||
payload.action === 'square_hole_generate_visual_assets'
|
||||
) {
|
||||
setSquareHoleGenerationState((current) => ({
|
||||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||||
phase: 'failed',
|
||||
error: errorMessage,
|
||||
}));
|
||||
setSelectionStage('square-hole-generating');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -2047,6 +2138,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleProfile(null);
|
||||
setSquareHoleRun(null);
|
||||
setSquareHoleError(null);
|
||||
setSquareHoleGenerationState(null);
|
||||
setSquareHoleRuntimeReturnStage('square-hole-result');
|
||||
setStreamingSquareHoleReplyText('');
|
||||
setIsStreamingSquareHoleReply(false);
|
||||
@@ -2162,6 +2254,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleGalleryEntries([]);
|
||||
setSquareHoleRun(null);
|
||||
setSquareHoleRuntimeReturnStage('square-hole-result');
|
||||
setSquareHoleGenerationState(null);
|
||||
setSquareHoleError(null);
|
||||
setStreamingSquareHoleReplyText('');
|
||||
setIsStreamingSquareHoleReply(false);
|
||||
@@ -2291,6 +2384,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
const leaveSquareHoleFlow = useCallback(() => {
|
||||
setSquareHoleRun(null);
|
||||
setSquareHoleRuntimeReturnStage('square-hole-result');
|
||||
setSquareHoleGenerationState(null);
|
||||
squareHoleFlow.leaveFlow();
|
||||
}, [squareHoleFlow]);
|
||||
|
||||
@@ -2316,6 +2410,20 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const executeSquareHoleAction = squareHoleFlow.executeAction;
|
||||
|
||||
const retrySquareHoleAssetGeneration = useCallback(() => {
|
||||
const session = squareHoleSession;
|
||||
if (!session?.draft?.profileId) {
|
||||
void executeSquareHoleAction({
|
||||
action: 'square_hole_compile_draft',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void executeSquareHoleAction({
|
||||
action: 'square_hole_generate_visual_assets',
|
||||
});
|
||||
}, [executeSquareHoleAction, squareHoleSession]);
|
||||
|
||||
const executePuzzleAction = puzzleFlow.executeAction;
|
||||
|
||||
const retryPuzzleDraftGeneration = useCallback(() => {
|
||||
@@ -4013,6 +4121,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
setSquareHoleGenerationState(null);
|
||||
const restoredSession = await squareHoleFlow.restoreDraft(
|
||||
item.sourceSessionId,
|
||||
);
|
||||
@@ -5583,6 +5692,50 @@ export function PlatformEntryFlowShellImpl({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'square-hole-generating' && (
|
||||
<motion.div
|
||||
key="square-hole-generating"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载方洞挑战生成面板..." />}
|
||||
>
|
||||
<CustomWorldGenerationView
|
||||
settingText={
|
||||
squareHoleSession?.lastAssistantReply ??
|
||||
'正在整理当前方洞挑战草稿。'
|
||||
}
|
||||
anchorEntries={buildSquareHoleGenerationAnchorEntries(
|
||||
squareHoleSession,
|
||||
)}
|
||||
progress={buildMiniGameDraftGenerationProgress(
|
||||
squareHoleGenerationState,
|
||||
)}
|
||||
isGenerating={isSquareHoleBusy}
|
||||
error={squareHoleError}
|
||||
onBack={leaveSquareHoleFlow}
|
||||
onEditSetting={() => {
|
||||
setSelectionStage('square-hole-agent-workspace');
|
||||
}}
|
||||
onRetry={retrySquareHoleAssetGeneration}
|
||||
onInterrupt={undefined}
|
||||
backLabel="返回创作中心"
|
||||
settingActionLabel={null}
|
||||
retryLabel="重新生成图片"
|
||||
settingTitle="当前方洞挑战"
|
||||
settingDescription={null}
|
||||
progressTitle="方洞挑战图片生成进度"
|
||||
activeBadgeLabel="图片生成中"
|
||||
pausedBadgeLabel="图片生成已暂停"
|
||||
idleBadgeLabel="等待返回结果页"
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'square-hole-result' && squareHoleSession?.draft && (
|
||||
<motion.div
|
||||
key="square-hole-result"
|
||||
|
||||
@@ -26,6 +26,7 @@ export type SelectionStage =
|
||||
| 'match3d-result'
|
||||
| 'match3d-runtime'
|
||||
| 'square-hole-agent-workspace'
|
||||
| 'square-hole-generating'
|
||||
| 'square-hole-result'
|
||||
| 'square-hole-runtime'
|
||||
| 'puzzle-agent-workspace'
|
||||
|
||||
@@ -2,11 +2,15 @@ import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type {
|
||||
CustomWorldGalleryCard,
|
||||
CustomWorldLibraryEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type {
|
||||
SquareHoleHoleOption,
|
||||
SquareHoleShapeOption,
|
||||
SquareHoleWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
||||
import {
|
||||
@@ -112,6 +116,12 @@ export type PlatformSquareHoleGalleryCard = {
|
||||
subtitle: string;
|
||||
summaryText: string;
|
||||
coverImageSrc: string | null;
|
||||
backgroundPrompt?: string;
|
||||
backgroundImageSrc?: string | null;
|
||||
shapeOptions?: SquareHoleShapeOption[];
|
||||
holeOptions?: SquareHoleHoleOption[];
|
||||
shapeCount?: number;
|
||||
difficulty?: number;
|
||||
themeTags: string[];
|
||||
playCount?: number;
|
||||
remixCount?: number;
|
||||
@@ -224,9 +234,15 @@ export function mapSquareHoleWorkToPlatformGalleryCard(
|
||||
ownerUserId: work.ownerUserId,
|
||||
authorDisplayName: '玩家',
|
||||
worldName: work.gameName,
|
||||
subtitle: '反直觉形状分拣',
|
||||
subtitle: work.twistRule || '反直觉形状分拣',
|
||||
summaryText: work.summary,
|
||||
coverImageSrc: work.coverImageSrc ?? null,
|
||||
backgroundPrompt: work.backgroundPrompt,
|
||||
backgroundImageSrc: work.backgroundImageSrc ?? null,
|
||||
shapeOptions: work.shapeOptions,
|
||||
holeOptions: work.holeOptions,
|
||||
shapeCount: work.shapeCount,
|
||||
difficulty: work.difficulty,
|
||||
themeTags:
|
||||
work.tags.length > 0 ? work.tags : [work.themeText, '方洞挑战'],
|
||||
playCount: work.playCount ?? 0,
|
||||
|
||||
@@ -4,13 +4,17 @@ import {
|
||||
ImagePlus,
|
||||
Loader2,
|
||||
Play,
|
||||
Plus,
|
||||
Send,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { type ChangeEvent, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { SquareHoleResultDraft } from '../../../packages/shared/src/contracts/squareHoleAgent';
|
||||
import type {
|
||||
PutSquareHoleWorkRequest,
|
||||
SquareHoleHoleOption,
|
||||
SquareHoleShapeOption,
|
||||
SquareHoleWorkProfile,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import {
|
||||
@@ -37,13 +41,26 @@ type SquareHoleResultEditState = {
|
||||
summary: string;
|
||||
tagsText: string;
|
||||
coverImageSrc: string;
|
||||
backgroundPrompt: string;
|
||||
backgroundImageSrc: string;
|
||||
themeText: string;
|
||||
twistRule: string;
|
||||
shapeOptions: SquareHoleShapeOption[];
|
||||
holeOptions: SquareHoleHoleOption[];
|
||||
shapeCountText: string;
|
||||
difficultyText: string;
|
||||
};
|
||||
|
||||
const SQUARE_HOLE_AUTOSAVE_DEBOUNCE_MS = 600;
|
||||
const SQUARE_HOLE_SHAPE_KIND_OPTIONS = [
|
||||
'square',
|
||||
'circle',
|
||||
'triangle',
|
||||
'star',
|
||||
'arch',
|
||||
'diamond',
|
||||
];
|
||||
const SQUARE_HOLE_HOLE_KIND_OPTIONS = SQUARE_HOLE_SHAPE_KIND_OPTIONS;
|
||||
|
||||
function normalizeTags(value: string) {
|
||||
return [
|
||||
@@ -78,8 +95,12 @@ function createEditState(
|
||||
summary: profile.summary,
|
||||
tagsText: profile.tags.join(','),
|
||||
coverImageSrc: profile.coverImageSrc?.trim() || '',
|
||||
backgroundPrompt: profile.backgroundPrompt || '',
|
||||
backgroundImageSrc: profile.backgroundImageSrc?.trim() || '',
|
||||
themeText: profile.themeText,
|
||||
twistRule: profile.twistRule,
|
||||
shapeOptions: profile.shapeOptions.map((option) => ({ ...option })),
|
||||
holeOptions: profile.holeOptions.map((option) => ({ ...option })),
|
||||
shapeCountText: String(profile.shapeCount),
|
||||
difficultyText: String(profile.difficulty),
|
||||
};
|
||||
@@ -95,6 +116,28 @@ function buildSavePayload(
|
||||
const twistRule = editState.twistRule.trim();
|
||||
const summary = editState.summary.trim();
|
||||
const tags = normalizeTags(editState.tagsText);
|
||||
const shapeOptions = editState.shapeOptions
|
||||
.map((option) => ({
|
||||
...option,
|
||||
optionId: option.optionId.trim(),
|
||||
shapeKind: option.shapeKind.trim(),
|
||||
label: option.label.trim(),
|
||||
imagePrompt: option.imagePrompt.trim(),
|
||||
imageSrc: option.imageSrc?.trim() || null,
|
||||
}))
|
||||
.filter(
|
||||
(option) =>
|
||||
option.optionId && option.shapeKind && option.label && option.imagePrompt,
|
||||
);
|
||||
const holeOptions = editState.holeOptions
|
||||
.map((option) => ({
|
||||
...option,
|
||||
holeId: option.holeId.trim(),
|
||||
holeKind: option.holeKind.trim(),
|
||||
label: option.label.trim(),
|
||||
bonus: Boolean(option.bonus),
|
||||
}))
|
||||
.filter((option) => option.holeId && option.holeKind && option.label);
|
||||
|
||||
if (
|
||||
!gameName ||
|
||||
@@ -102,6 +145,8 @@ function buildSavePayload(
|
||||
!twistRule ||
|
||||
!summary ||
|
||||
tags.length === 0 ||
|
||||
shapeOptions.length === 0 ||
|
||||
holeOptions.length === 0 ||
|
||||
!shapeCount ||
|
||||
!difficulty
|
||||
) {
|
||||
@@ -115,6 +160,10 @@ function buildSavePayload(
|
||||
summary,
|
||||
tags,
|
||||
coverImageSrc: editState.coverImageSrc.trim() || null,
|
||||
backgroundPrompt: editState.backgroundPrompt.trim(),
|
||||
backgroundImageSrc: editState.backgroundImageSrc.trim() || null,
|
||||
shapeOptions,
|
||||
holeOptions,
|
||||
shapeCount,
|
||||
difficulty,
|
||||
};
|
||||
@@ -129,6 +178,21 @@ function buildPublishBlockers(editState: SquareHoleResultEditState) {
|
||||
...(normalizeTags(editState.tagsText).length > 0
|
||||
? []
|
||||
: ['至少需要 1 个标签。']),
|
||||
...(editState.shapeOptions.some(
|
||||
(option) =>
|
||||
option.optionId.trim() &&
|
||||
option.shapeKind.trim() &&
|
||||
option.label.trim() &&
|
||||
option.imagePrompt.trim(),
|
||||
)
|
||||
? []
|
||||
: ['至少需要 1 个形状选项。']),
|
||||
...(editState.holeOptions.some(
|
||||
(option) =>
|
||||
option.holeId.trim() && option.holeKind.trim() && option.label.trim(),
|
||||
)
|
||||
? []
|
||||
: ['至少需要 1 个洞口选项。']),
|
||||
...(normalizeShapeCount(editState.shapeCountText)
|
||||
? []
|
||||
: ['形状数量需要在 6 到 24 之间。']),
|
||||
@@ -154,6 +218,31 @@ function readImageAsDataUrl(file: File) {
|
||||
});
|
||||
}
|
||||
|
||||
function createShapeOption(index: number): SquareHoleShapeOption {
|
||||
return {
|
||||
optionId: `shape-${Date.now().toString(36)}-${index}`,
|
||||
shapeKind:
|
||||
SQUARE_HOLE_SHAPE_KIND_OPTIONS[
|
||||
index % SQUARE_HOLE_SHAPE_KIND_OPTIONS.length
|
||||
] ?? 'square',
|
||||
label: `形状 ${index + 1}`,
|
||||
imagePrompt: '主题贴图',
|
||||
imageSrc: null,
|
||||
};
|
||||
}
|
||||
|
||||
function createHoleOption(index: number): SquareHoleHoleOption {
|
||||
return {
|
||||
holeId: `hole-${Date.now().toString(36)}-${index}`,
|
||||
holeKind:
|
||||
SQUARE_HOLE_HOLE_KIND_OPTIONS[
|
||||
index % SQUARE_HOLE_HOLE_KIND_OPTIONS.length
|
||||
] ?? 'square',
|
||||
label: `洞口 ${index + 1}`,
|
||||
bonus: index === 0,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPlayableProfile(
|
||||
profile: SquareHoleWorkProfile,
|
||||
editState: SquareHoleResultEditState,
|
||||
@@ -171,6 +260,10 @@ function buildPlayableProfile(
|
||||
summary: payload.summary,
|
||||
tags: payload.tags,
|
||||
coverImageSrc: payload.coverImageSrc,
|
||||
backgroundPrompt: payload.backgroundPrompt ?? profile.backgroundPrompt,
|
||||
backgroundImageSrc: payload.backgroundImageSrc,
|
||||
shapeOptions: payload.shapeOptions ?? profile.shapeOptions,
|
||||
holeOptions: payload.holeOptions ?? profile.holeOptions,
|
||||
shapeCount: payload.shapeCount,
|
||||
difficulty: payload.difficulty,
|
||||
};
|
||||
@@ -241,7 +334,7 @@ export function SquareHoleResultView({
|
||||
setEditState(createEditState(profile));
|
||||
setAutoSaveState('idle');
|
||||
setLocalError(null);
|
||||
}, [profile.profileId, profile.updatedAt]);
|
||||
}, [profile]);
|
||||
|
||||
useEffect(() => {
|
||||
const payload = buildSavePayload(editState);
|
||||
@@ -250,14 +343,21 @@ export function SquareHoleResultView({
|
||||
}
|
||||
|
||||
const currentTags = normalizeTags(profile.tags.join(','));
|
||||
const currentShapeOptions = JSON.stringify(profile.shapeOptions);
|
||||
const currentHoleOptions = JSON.stringify(profile.holeOptions);
|
||||
const changed =
|
||||
payload.gameName !== profile.gameName ||
|
||||
payload.themeText !== profile.themeText ||
|
||||
payload.twistRule !== profile.twistRule ||
|
||||
payload.summary !== profile.summary ||
|
||||
(payload.coverImageSrc ?? '') !== (profile.coverImageSrc ?? '') ||
|
||||
(payload.backgroundPrompt ?? '') !== (profile.backgroundPrompt ?? '') ||
|
||||
(payload.backgroundImageSrc ?? '') !==
|
||||
(profile.backgroundImageSrc ?? '') ||
|
||||
payload.shapeCount !== profile.shapeCount ||
|
||||
payload.difficulty !== profile.difficulty ||
|
||||
JSON.stringify(payload.shapeOptions ?? []) !== currentShapeOptions ||
|
||||
JSON.stringify(payload.holeOptions ?? []) !== currentHoleOptions ||
|
||||
payload.tags.length !== currentTags.length ||
|
||||
payload.tags.some((tag, index) => tag !== currentTags[index]);
|
||||
|
||||
@@ -330,6 +430,60 @@ export function SquareHoleResultView({
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackgroundImageChange = async (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
event.target.value = '';
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataUrl = await readImageAsDataUrl(file);
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
backgroundImageSrc: dataUrl,
|
||||
}));
|
||||
setLocalError(null);
|
||||
} catch (caughtError) {
|
||||
setLocalError(
|
||||
caughtError instanceof Error ? caughtError.message : '背景图读取失败。',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShapeImageChange = async (
|
||||
optionId: string,
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
event.target.value = '';
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dataUrl = await readImageAsDataUrl(file);
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
shapeOptions: current.shapeOptions.map((option) =>
|
||||
option.optionId === optionId
|
||||
? {
|
||||
...option,
|
||||
imageSrc: dataUrl,
|
||||
}
|
||||
: option,
|
||||
),
|
||||
}));
|
||||
setLocalError(null);
|
||||
} catch (caughtError) {
|
||||
setLocalError(
|
||||
caughtError instanceof Error ? caughtError.message : '形状贴图读取失败。',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartTestRun = async () => {
|
||||
if (!canSubmit || isStartingTestRun) {
|
||||
setLocalError(blockers[0] ?? null);
|
||||
@@ -422,6 +576,31 @@ export function SquareHoleResultView({
|
||||
{draft?.publishReady ?? profile.publishReady ? '可发布' : '草稿'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 aspect-[16/9] overflow-hidden rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-[linear-gradient(135deg,rgba(15,23,42,0.12),rgba(34,197,94,0.16))]">
|
||||
{editState.backgroundImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={editState.backgroundImageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center text-slate-700">
|
||||
<ImagePlus className="h-8 w-8" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<label className="platform-button platform-button--ghost mt-3 flex min-h-10 cursor-pointer items-center justify-center gap-2 px-3 py-2 text-sm">
|
||||
<ImagePlus className="h-4 w-4" />
|
||||
背景图
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="sr-only"
|
||||
disabled={busy}
|
||||
onChange={handleBackgroundImageChange}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
|
||||
@@ -497,6 +676,23 @@ export function SquareHoleResultView({
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block sm:col-span-2">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
背景提示
|
||||
</span>
|
||||
<input
|
||||
value={editState.backgroundPrompt}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState({
|
||||
...editState,
|
||||
backgroundPrompt: event.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
形状数量
|
||||
@@ -534,6 +730,242 @@ export function SquareHoleResultView({
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5 lg:col-span-2">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
形状选项
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
shapeOptions: [
|
||||
...current.shapeOptions,
|
||||
createShapeOption(current.shapeOptions.length),
|
||||
],
|
||||
}))
|
||||
}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
新增
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{editState.shapeOptions.map((option) => (
|
||||
<div
|
||||
key={option.optionId}
|
||||
className="rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-3"
|
||||
>
|
||||
<div className="mb-3 flex items-start gap-3">
|
||||
<label className="relative grid h-20 w-20 shrink-0 cursor-pointer place-items-center overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/82 text-slate-600">
|
||||
{option.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={option.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<ImagePlus className="h-6 w-6" />
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="sr-only"
|
||||
disabled={busy}
|
||||
onChange={(event) => {
|
||||
void handleShapeImageChange(option.optionId, event);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<input
|
||||
value={option.label}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
shapeOptions: current.shapeOptions.map((entry) =>
|
||||
entry.optionId === option.optionId
|
||||
? { ...entry, label: event.target.value }
|
||||
: entry,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
<select
|
||||
value={option.shapeKind}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
shapeOptions: current.shapeOptions.map((entry) =>
|
||||
entry.optionId === option.optionId
|
||||
? { ...entry, shapeKind: event.target.value }
|
||||
: entry,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
>
|
||||
{SQUARE_HOLE_SHAPE_KIND_OPTIONS.map((kind) => (
|
||||
<option key={kind} value={kind}>
|
||||
{kind}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy || editState.shapeOptions.length <= 1}
|
||||
onClick={() =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
shapeOptions: current.shapeOptions.filter(
|
||||
(entry) => entry.optionId !== option.optionId,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="rounded-full p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40"
|
||||
aria-label="删除形状选项"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={option.imagePrompt}
|
||||
disabled={busy}
|
||||
rows={2}
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
shapeOptions: current.shapeOptions.map((entry) =>
|
||||
entry.optionId === option.optionId
|
||||
? { ...entry, imagePrompt: event.target.value }
|
||||
: entry,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full resize-none rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm leading-5 text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5 lg:col-span-2">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
洞口选项
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
holeOptions: [
|
||||
...current.holeOptions,
|
||||
createHoleOption(current.holeOptions.length),
|
||||
],
|
||||
}))
|
||||
}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
新增
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{editState.holeOptions.map((option) => (
|
||||
<div
|
||||
key={option.holeId}
|
||||
className="rounded-[1.1rem] border border-[var(--platform-subpanel-border)] bg-white/58 p-3"
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<input
|
||||
value={option.label}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
holeOptions: current.holeOptions.map((entry) =>
|
||||
entry.holeId === option.holeId
|
||||
? { ...entry, label: event.target.value }
|
||||
: entry,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
/>
|
||||
<select
|
||||
value={option.holeKind}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
holeOptions: current.holeOptions.map((entry) =>
|
||||
entry.holeId === option.holeId
|
||||
? { ...entry, holeKind: event.target.value }
|
||||
: entry,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-[0.85rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-2 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
|
||||
>
|
||||
{SQUARE_HOLE_HOLE_KIND_OPTIONS.map((kind) => (
|
||||
<option key={kind} value={kind}>
|
||||
{kind}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label className="inline-flex min-w-0 items-center gap-2 text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={option.bonus}
|
||||
disabled={busy}
|
||||
onChange={(event) =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
holeOptions: current.holeOptions.map((entry) =>
|
||||
entry.holeId === option.holeId
|
||||
? { ...entry, bonus: event.target.checked }
|
||||
: entry,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
加分选项
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy || editState.holeOptions.length <= 1}
|
||||
onClick={() =>
|
||||
setEditState((current) => ({
|
||||
...current,
|
||||
holeOptions: current.holeOptions.filter(
|
||||
(entry) => entry.holeId !== option.holeId,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="rounded-full p-2 text-slate-500 hover:bg-white/72 disabled:opacity-40"
|
||||
aria-label="删除洞口选项"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
SquareHoleHoleSnapshot,
|
||||
SquareHoleRunSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
|
||||
type SquareHoleRuntimeShellProps = {
|
||||
run: SquareHoleRunSnapshot | null;
|
||||
@@ -241,7 +242,17 @@ export function SquareHoleRuntimeShell({
|
||||
|
||||
return (
|
||||
<main className="relative flex min-h-dvh w-full justify-center overflow-hidden bg-[#101827] text-white">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_10%,rgba(125,211,252,0.32),transparent_27%),radial-gradient(circle_at_80%_80%,rgba(248,113,113,0.24),transparent_34%),linear-gradient(180deg,#1f3a5f_0%,#152238_48%,#111827_100%)]" />
|
||||
{run.backgroundImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={run.backgroundImageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_10%,rgba(125,211,252,0.32),transparent_27%),radial-gradient(circle_at_80%_80%,rgba(248,113,113,0.24),transparent_34%),linear-gradient(180deg,#1f3a5f_0%,#152238_48%,#111827_100%)]" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-slate-950/42" />
|
||||
<div
|
||||
className="relative flex min-h-dvh min-w-0 flex-col overflow-hidden px-3 pb-[calc(env(safe-area-inset-bottom,0px)+0.8rem)] pt-[calc(env(safe-area-inset-top,0px)+0.65rem)]"
|
||||
style={{
|
||||
@@ -287,14 +298,14 @@ export function SquareHoleRuntimeShell({
|
||||
|
||||
<section className="mt-3 rounded-[1.5rem] border border-white/14 bg-black/18 p-3 shadow-[0_18px_42px_rgba(15,23,42,0.28)] backdrop-blur">
|
||||
<div className="flex items-center justify-between gap-2 text-xs font-bold text-white/68">
|
||||
<span>{run.ruleLabel}</span>
|
||||
<span>v{run.snapshotVersion}</span>
|
||||
<span>当前形状</span>
|
||||
<span>{progressText}</span>
|
||||
</div>
|
||||
<div className="mt-3 flex min-h-[12rem] items-center justify-center rounded-[1.35rem] border border-white/10 bg-white/10">
|
||||
{currentShape ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div
|
||||
className={`h-24 w-24 shadow-[0_18px_38px_rgba(15,23,42,0.34)] ${getShapePreviewClass(
|
||||
className={`relative h-24 w-24 overflow-hidden shadow-[0_18px_38px_rgba(15,23,42,0.34)] ${getShapePreviewClass(
|
||||
currentShape.shapeKind,
|
||||
)}`}
|
||||
style={{
|
||||
@@ -302,7 +313,16 @@ export function SquareHoleRuntimeShell({
|
||||
currentShape.color ||
|
||||
'linear-gradient(135deg,#f8fafc,#38bdf8)',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{currentShape.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={currentShape.imageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-base font-black">
|
||||
<Shapes size={18} />
|
||||
<span>{currentShape.label}</span>
|
||||
|
||||
@@ -7,9 +7,10 @@ import type {
|
||||
CustomWorldGenerationProgress,
|
||||
CustomWorldGenerationStep,
|
||||
} from '../../packages/shared/src/contracts/runtime';
|
||||
import type { SquareHoleSessionSnapshot } from '../../packages/shared/src/contracts/squareHoleAgent';
|
||||
import type { CustomWorldStructuredAnchorEntry } from './customWorldAgentGenerationProgress';
|
||||
|
||||
export type MiniGameDraftGenerationKind = 'puzzle' | 'big-fish';
|
||||
export type MiniGameDraftGenerationKind = 'puzzle' | 'big-fish' | 'square-hole';
|
||||
|
||||
export type MiniGameDraftGenerationPhase =
|
||||
| 'idle'
|
||||
@@ -17,6 +18,10 @@ export type MiniGameDraftGenerationPhase =
|
||||
| 'big-fish-draft'
|
||||
| 'big-fish-levels'
|
||||
| 'big-fish-runtime'
|
||||
| 'square-hole-draft'
|
||||
| 'square-hole-cover'
|
||||
| 'square-hole-shapes'
|
||||
| 'square-hole-ready'
|
||||
| 'puzzle-images'
|
||||
| 'puzzle-select-image'
|
||||
| 'ready'
|
||||
@@ -86,12 +91,39 @@ const BIG_FISH_STEPS = [
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
|
||||
const SQUARE_HOLE_STEPS = [
|
||||
{
|
||||
id: 'square-hole-draft',
|
||||
label: '整理玩法草稿',
|
||||
detail: '收拢题材、形状、洞口与加分选项。',
|
||||
weight: 28,
|
||||
},
|
||||
{
|
||||
id: 'square-hole-cover',
|
||||
label: '生成封面与背景',
|
||||
detail: '生成作品封面和运行背景。',
|
||||
weight: 32,
|
||||
},
|
||||
{
|
||||
id: 'square-hole-shapes',
|
||||
label: '生成形状贴图',
|
||||
detail: '为每个可投放形状生成贴图。',
|
||||
weight: 40,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
|
||||
function clampProgress(value: number) {
|
||||
return Math.max(0, Math.min(100, Math.round(value)));
|
||||
}
|
||||
|
||||
function getStepDefinitions(kind: MiniGameDraftGenerationKind) {
|
||||
return kind === 'puzzle' ? PUZZLE_STEPS : BIG_FISH_STEPS;
|
||||
if (kind === 'puzzle') {
|
||||
return PUZZLE_STEPS;
|
||||
}
|
||||
if (kind === 'square-hole') {
|
||||
return SQUARE_HOLE_STEPS;
|
||||
}
|
||||
return BIG_FISH_STEPS;
|
||||
}
|
||||
|
||||
function getActiveStepIndex(
|
||||
@@ -132,7 +164,12 @@ export function createMiniGameDraftGenerationState(
|
||||
): MiniGameDraftGenerationState {
|
||||
return {
|
||||
kind,
|
||||
phase: kind === 'big-fish' ? 'big-fish-draft' : 'compile',
|
||||
phase:
|
||||
kind === 'big-fish'
|
||||
? 'big-fish-draft'
|
||||
: kind === 'square-hole'
|
||||
? 'square-hole-draft'
|
||||
: 'compile',
|
||||
startedAtMs: Date.now(),
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
@@ -152,6 +189,18 @@ function resolveBigFishPhaseByElapsedMs(
|
||||
return 'big-fish-draft';
|
||||
}
|
||||
|
||||
function resolveSquareHolePhaseByElapsedMs(
|
||||
elapsedMs: number,
|
||||
): MiniGameDraftGenerationPhase {
|
||||
if (elapsedMs >= 6_500) {
|
||||
return 'square-hole-shapes';
|
||||
}
|
||||
if (elapsedMs >= 2_400) {
|
||||
return 'square-hole-cover';
|
||||
}
|
||||
return 'square-hole-draft';
|
||||
}
|
||||
|
||||
export function buildMiniGameDraftGenerationProgress(
|
||||
state: MiniGameDraftGenerationState | null,
|
||||
nowMs = Date.now(),
|
||||
@@ -169,6 +218,13 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
...state,
|
||||
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state.kind === 'square-hole' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveSquareHolePhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
|
||||
const steps = getStepDefinitions(normalizedState.kind);
|
||||
@@ -190,6 +246,8 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? 1
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? 0.55
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? 0.42
|
||||
: 0;
|
||||
const overallProgress =
|
||||
normalizedState.phase === 'failed'
|
||||
@@ -223,6 +281,8 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? 0
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? Math.max(0, 7_000 - elapsedMs)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? Math.max(0, 12_000 - elapsedMs)
|
||||
: null,
|
||||
activeStepIndex,
|
||||
steps: buildMiniGameProgressSteps(steps, activeStepIndex, normalizedState),
|
||||
@@ -306,3 +366,46 @@ export function buildBigFishGenerationAnchorEntries(
|
||||
}))
|
||||
.filter((entry) => entry.value.trim());
|
||||
}
|
||||
|
||||
export function buildSquareHoleGenerationAnchorEntries(
|
||||
session: SquareHoleSessionSnapshot | null | undefined,
|
||||
): CustomWorldStructuredAnchorEntry[] {
|
||||
if (!session) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const draft = session.draft;
|
||||
const shapeCount =
|
||||
draft?.shapeOptions.filter((option) => option.imageSrc?.trim()).length ??
|
||||
session.config.shapeOptions.filter((option) => option.imageSrc?.trim())
|
||||
.length;
|
||||
const totalShapeCount =
|
||||
draft?.shapeOptions.length || session.config.shapeOptions.length;
|
||||
|
||||
const entries: Array<MiniGameAnchorSource | null> = [
|
||||
{
|
||||
key: 'square-hole-title',
|
||||
label: '作品名称',
|
||||
value: draft?.gameName || `${session.config.themeText}方洞挑战`,
|
||||
},
|
||||
{
|
||||
key: 'square-hole-theme',
|
||||
label: '题材与规则',
|
||||
value: `${session.config.themeText}|${session.config.twistRule}`,
|
||||
},
|
||||
{
|
||||
key: 'square-hole-options',
|
||||
label: '选项资产',
|
||||
value: totalShapeCount > 0 ? `形状贴图 ${shapeCount}/${totalShapeCount}` : '',
|
||||
},
|
||||
];
|
||||
|
||||
return entries
|
||||
.filter((entry): entry is MiniGameAnchorSource => Boolean(entry))
|
||||
.map((entry) => ({
|
||||
id: entry.key,
|
||||
label: entry.label,
|
||||
value: entry.value,
|
||||
}))
|
||||
.filter((entry) => entry.value.trim());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user