import type { BigFishSessionSnapshotResponse } from '../../packages/shared/src/contracts/bigFish'; import type { CreatePuzzleAgentSessionRequest, PuzzleAgentSessionSnapshot, } from '../../packages/shared/src/contracts/puzzleAgentSession'; import type { CustomWorldGenerationProgress, CustomWorldGenerationStep, } from '../../packages/shared/src/contracts/runtime'; import type { CustomWorldStructuredAnchorEntry } from './customWorldAgentGenerationProgress'; export type MiniGameDraftGenerationKind = 'puzzle' | 'big-fish'; export type MiniGameDraftGenerationPhase = | 'idle' | 'compile' | 'big-fish-draft' | 'big-fish-levels' | 'big-fish-runtime' | 'puzzle-images' | 'puzzle-select-image' | 'ready' | 'failed'; export type MiniGameDraftGenerationState = { kind: MiniGameDraftGenerationKind; phase: MiniGameDraftGenerationPhase; startedAtMs: number; completedAssetCount: number; totalAssetCount: number; error: string | null; }; type MiniGameStepDefinition = { id: MiniGameDraftGenerationPhase; label: string; detail: string; weight: number; }; type MiniGameAnchorSource = { key: string; label: string; value: string; }; const PUZZLE_STEPS = [ { id: 'compile', label: '编译拼图草稿', detail: '整理主题、主体、构图与标签。', weight: 34, }, { id: 'puzzle-images', label: '生成拼图图片', detail: '根据草稿生成候选图。', weight: 33, }, { id: 'puzzle-select-image', label: '确认正式图片', detail: '选择候选图写入结果页。', weight: 33, }, ] as const satisfies ReadonlyArray; const BIG_FISH_STEPS = [ { id: 'big-fish-draft', label: '整理玩法骨架', detail: '收拢玩法承诺、成长阶梯与风险节奏。', weight: 30, }, { id: 'big-fish-levels', label: '编译等级蓝图', detail: '生成每级角色描述、形象描述与动作描述。', weight: 45, }, { id: 'big-fish-runtime', label: '校准场地与参数', detail: '整理背景蓝图与运行参数,准备结果页。', weight: 25, }, ] as const satisfies ReadonlyArray; 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; } function getActiveStepIndex( steps: ReadonlyArray, phase: MiniGameDraftGenerationPhase, ) { if (phase === 'ready') { return steps.length - 1; } const index = steps.findIndex((step) => step.id === phase); return index >= 0 ? index : 0; } function buildMiniGameProgressSteps( steps: ReadonlyArray, activeStepIndex: number, state: MiniGameDraftGenerationState, ) { return steps.map((step, index) => { const isCompleted = state.phase === 'ready' || index < activeStepIndex; const isActive = state.phase !== 'failed' && !isCompleted && index === activeStepIndex; const isAssetStep = step.id === state.phase && state.totalAssetCount > 0; return { id: step.id, label: step.label, detail: step.detail, completed: isCompleted ? 1 : isAssetStep ? state.completedAssetCount : 0, total: isAssetStep ? state.totalAssetCount : 1, status: isCompleted ? 'completed' : isActive ? 'active' : 'pending', } satisfies CustomWorldGenerationStep; }); } export function createMiniGameDraftGenerationState( kind: MiniGameDraftGenerationKind, ): MiniGameDraftGenerationState { return { kind, phase: kind === 'big-fish' ? 'big-fish-draft' : 'compile', startedAtMs: Date.now(), completedAssetCount: 0, totalAssetCount: 0, error: null, }; } function resolveBigFishPhaseByElapsedMs( elapsedMs: number, ): MiniGameDraftGenerationPhase { if (elapsedMs >= 4_500) { return 'big-fish-runtime'; } if (elapsedMs >= 1_800) { return 'big-fish-levels'; } return 'big-fish-draft'; } export function buildMiniGameDraftGenerationProgress( state: MiniGameDraftGenerationState | null, nowMs = Date.now(), ): CustomWorldGenerationProgress | null { if (!state) { return null; } const elapsedMs = Math.max(0, nowMs - state.startedAtMs); const normalizedState = state.kind === 'big-fish' && state.phase !== 'failed' && state.phase !== 'ready' ? { ...state, phase: resolveBigFishPhaseByElapsedMs(elapsedMs), } : state; const steps = getStepDefinitions(normalizedState.kind); const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase); const completedWeight = steps .slice( 0, normalizedState.phase === 'ready' ? steps.length : activeStepIndex, ) .reduce((sum, step) => sum + step.weight, 0); const activeStep = steps[activeStepIndex] ?? steps[0]; const assetRatio = normalizedState.totalAssetCount > 0 ? Math.min( 1, normalizedState.completedAssetCount / normalizedState.totalAssetCount, ) : normalizedState.phase === 'ready' ? 1 : normalizedState.kind === 'big-fish' ? 0.55 : 0; const overallProgress = normalizedState.phase === 'failed' ? Math.max(1, completedWeight) : normalizedState.phase === 'ready' ? 100 : completedWeight + activeStep.weight * assetRatio; return { phaseId: normalizedState.phase, phaseLabel: normalizedState.phase === 'failed' ? '生成失败' : normalizedState.phase === 'ready' ? '生成完成' : activeStep.label, phaseDetail: normalizedState.error ?? (normalizedState.phase === 'ready' ? normalizedState.kind === 'big-fish' ? '玩法草稿已准备完成,可进入结果页继续生成主图、动作和背景。' : '完整草稿与资产已准备完成。' : activeStep.detail), batchLabel: activeStep.label, overallProgress: clampProgress(overallProgress), completedWeight: clampProgress(overallProgress), totalWeight: 100, elapsedMs, estimatedRemainingMs: normalizedState.phase === 'ready' ? 0 : normalizedState.kind === 'big-fish' ? Math.max(0, 7_000 - elapsedMs) : null, activeStepIndex, steps: buildMiniGameProgressSteps(steps, activeStepIndex, normalizedState), }; } export function buildPuzzleGenerationAnchorEntries( session: PuzzleAgentSessionSnapshot | null | undefined, formPayload: CreatePuzzleAgentSessionRequest | null | undefined = null, ): CustomWorldStructuredAnchorEntry[] { if (!session) { return []; } const entries: Array = [ { key: 'puzzle-title', label: '作品名称', value: formPayload?.workTitle?.trim() || formPayload?.seedText?.trim() || session.draft?.workTitle || session.anchorPack.themePromise.value, }, { key: 'work-description', label: '作品描述', value: formPayload?.workDescription?.trim() || session.draft?.workDescription || '', }, { key: 'picture-description', label: '画面描述', value: formPayload?.pictureDescription?.trim() || session.draft?.levels?.[0]?.pictureDescription || session.anchorPack.visualSubject.value, }, ]; 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()); } export function buildBigFishGenerationAnchorEntries( session: BigFishSessionSnapshotResponse | null | undefined, ): CustomWorldStructuredAnchorEntry[] { if (!session) { return []; } const draft = session.draft; const assetReadyCount = session.assetSlots.filter( (slot) => slot.status === 'ready', ).length; const entries: Array = [ session.anchorPack.gameplayPromise, session.anchorPack.ecologyVisualTheme, session.anchorPack.growthLadder, session.anchorPack.riskTempo, draft ? { key: 'level-characters', label: '角色描述', value: draft.levels .map( (level) => `Lv.${level.level} ${level.name}:${level.oneLineFantasy}`, ) .join('\n'), } : null, draft ? { key: 'asset-coverage', label: '图片与动作', value: `已生成 ${assetReadyCount}/${session.assetSlots.length} 个资产`, } : null, ]; 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()); }