Merge branch 'codex/profile-mobile-ui-reference'
This commit is contained in:
@@ -91,6 +91,9 @@ export type MiniGameDraftGenerationState = {
|
||||
error: string | null;
|
||||
metadata?: {
|
||||
puzzleAiRedraw?: boolean;
|
||||
puzzleActivePhaseId?: MiniGameDraftGenerationPhase;
|
||||
puzzleActiveStepStartedAtMs?: number;
|
||||
puzzleProgressPercent?: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -111,10 +114,14 @@ type MiniGameAnchorSource = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const PUZZLE_COVER_IMAGE_GENERATION_EXPECTED_MS = 240_000;
|
||||
const PUZZLE_IMAGE_GENERATION_EXPECTED_MS = 90_000;
|
||||
const PUZZLE_COMPILE_EXPECTED_MS = 8_000;
|
||||
const PUZZLE_LEVEL_NAME_EXPECTED_MS = 10_000;
|
||||
const PUZZLE_WRITE_DRAFT_EXPECTED_MS = 10_000;
|
||||
const PUZZLE_COMPILE_MILESTONE_PROGRESS = 88;
|
||||
const PUZZLE_IMAGE_MILESTONE_PROGRESS = 94;
|
||||
const PUZZLE_UI_MILESTONE_PROGRESS = 96;
|
||||
|
||||
function shouldSkipPuzzleCoverGeneration(state: MiniGameDraftGenerationState) {
|
||||
return state.metadata?.puzzleAiRedraw === false;
|
||||
@@ -160,8 +167,8 @@ function buildPuzzleTimedSteps(state: MiniGameDraftGenerationState) {
|
||||
steps.push({
|
||||
id: 'puzzle-cover-image',
|
||||
label: '生成拼图首图',
|
||||
detail: '调用 gpt-image-2 生成 1:1 拼图首图,预计 90 秒。',
|
||||
durationMs: PUZZLE_IMAGE_GENERATION_EXPECTED_MS,
|
||||
detail: '调用 gpt-image-2 生成 1:1 拼图首图,预计 4 分钟。',
|
||||
durationMs: PUZZLE_COVER_IMAGE_GENERATION_EXPECTED_MS,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,30 +211,40 @@ function resolvePuzzleEstimatedWaitMs(state: MiniGameDraftGenerationState) {
|
||||
|
||||
const PUZZLE_NON_READY_MAX_PROGRESS = 98;
|
||||
const BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS = 6 * 60_000;
|
||||
function buildPuzzlePhaseTimeline(state: MiniGameDraftGenerationState): Array<{
|
||||
phase: Extract<
|
||||
MiniGameDraftGenerationPhase,
|
||||
| 'compile'
|
||||
| 'puzzle-level-name'
|
||||
| 'puzzle-cover-image'
|
||||
| 'puzzle-level-scene'
|
||||
| 'puzzle-ui-assets'
|
||||
| 'puzzle-select-image'
|
||||
>;
|
||||
durationMs: number;
|
||||
}> {
|
||||
return buildPuzzleTimedSteps(state).map((step) => ({
|
||||
phase: step.id as Extract<
|
||||
MiniGameDraftGenerationPhase,
|
||||
| 'compile'
|
||||
| 'puzzle-level-name'
|
||||
| 'puzzle-cover-image'
|
||||
| 'puzzle-level-scene'
|
||||
| 'puzzle-ui-assets'
|
||||
| 'puzzle-select-image'
|
||||
>,
|
||||
durationMs: step.durationMs,
|
||||
}));
|
||||
|
||||
function resolvePuzzleBackendProgressPercent(
|
||||
state: MiniGameDraftGenerationState,
|
||||
) {
|
||||
const progressPercent = state.metadata?.puzzleProgressPercent;
|
||||
if (typeof progressPercent !== 'number' || !Number.isFinite(progressPercent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, Math.round(progressPercent)));
|
||||
}
|
||||
|
||||
function resolvePuzzlePhaseByBackendProgress(
|
||||
state: MiniGameDraftGenerationState,
|
||||
): MiniGameDraftGenerationPhase | null {
|
||||
const progressPercent = resolvePuzzleBackendProgressPercent(state);
|
||||
if (progressPercent == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 中文注释:拼图生成页的跨步骤只跟随后端会话真实里程碑;
|
||||
// 每步内部的等待反馈仍由本地假进度补足。
|
||||
if (progressPercent >= 96) {
|
||||
return 'puzzle-select-image';
|
||||
}
|
||||
if (progressPercent >= 94) {
|
||||
return 'puzzle-ui-assets';
|
||||
}
|
||||
if (progressPercent >= 88) {
|
||||
return shouldSkipPuzzleCoverGeneration(state)
|
||||
? 'puzzle-level-scene'
|
||||
: 'puzzle-cover-image';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const BIG_FISH_STEPS = [
|
||||
@@ -685,32 +702,141 @@ function resolveWoodenFishTimelineByElapsedMs(elapsedMs: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePuzzleTimelineByElapsedMs(
|
||||
function resolvePuzzleActiveStepProgressRatio(
|
||||
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
|
||||
activeStepIndex: number,
|
||||
elapsedMs: number,
|
||||
state: MiniGameDraftGenerationState,
|
||||
) {
|
||||
let elapsedBeforePhase = 0;
|
||||
|
||||
for (const item of buildPuzzlePhaseTimeline(state)) {
|
||||
const elapsedInPhase = elapsedMs - elapsedBeforePhase;
|
||||
|
||||
if (elapsedInPhase < item.durationMs) {
|
||||
return {
|
||||
phase: item.phase,
|
||||
activeStepProgressRatio: Math.max(
|
||||
0,
|
||||
Math.min(1, elapsedInPhase / item.durationMs),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
elapsedBeforePhase += item.durationMs;
|
||||
const activeStep = steps[activeStepIndex];
|
||||
if (!activeStep) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return {
|
||||
phase: 'puzzle-select-image' as const,
|
||||
activeStepProgressRatio: 1,
|
||||
};
|
||||
const elapsedBeforeActiveStep = steps
|
||||
.slice(0, activeStepIndex)
|
||||
.reduce((sum, step) => sum + step.durationMs, 0);
|
||||
const elapsedInActiveStep = Math.max(0, elapsedMs - elapsedBeforeActiveStep);
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(0.98, elapsedInActiveStep / Math.max(1, activeStep.durationMs)),
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePuzzleActiveStepElapsedProgressRatio(
|
||||
state: MiniGameDraftGenerationState,
|
||||
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
|
||||
activeStepIndex: number,
|
||||
elapsedMs: number,
|
||||
effectiveNowMs: number,
|
||||
) {
|
||||
if (resolvePuzzleBackendProgressPercent(state) != null) {
|
||||
const stepStartedAtMs = state.metadata?.puzzleActiveStepStartedAtMs;
|
||||
if (
|
||||
state.metadata?.puzzleActivePhaseId === state.phase &&
|
||||
typeof stepStartedAtMs === 'number' &&
|
||||
Number.isFinite(stepStartedAtMs)
|
||||
) {
|
||||
const activeStep = steps[activeStepIndex];
|
||||
if (!activeStep) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
0.98,
|
||||
(effectiveNowMs - stepStartedAtMs) /
|
||||
Math.max(1, activeStep.durationMs),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return resolvePuzzleActiveStepProgressRatio(
|
||||
steps,
|
||||
activeStepIndex,
|
||||
elapsedMs,
|
||||
);
|
||||
}
|
||||
|
||||
const activeStep = steps[activeStepIndex];
|
||||
if (!activeStep) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 中文注释:未收到后端真实里程碑时,跨步骤必须卡住;
|
||||
// 但当前步骤内的假进度要按整段等待时间继续向前走,避免短步骤几秒后停死。
|
||||
const fallbackDurationMs = Math.max(1, resolvePuzzleEstimatedWaitMs(state));
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(0.98, elapsedMs / fallbackDurationMs),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveElapsedActiveStepProgressRatio(
|
||||
kind: MiniGameDraftGenerationKind,
|
||||
elapsedMs: number,
|
||||
) {
|
||||
const estimatedWaitMs =
|
||||
kind === 'big-fish'
|
||||
? 7_000
|
||||
: kind === 'square-hole'
|
||||
? 12_000
|
||||
: kind === 'match3d'
|
||||
? MATCH3D_ESTIMATED_WAIT_MS
|
||||
: kind === 'baby-object-match'
|
||||
? BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS
|
||||
: kind === 'jump-hop'
|
||||
? JUMP_HOP_ESTIMATED_WAIT_MS
|
||||
: kind === 'wooden-fish'
|
||||
? WOODEN_FISH_ESTIMATED_WAIT_MS
|
||||
: 1;
|
||||
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(0.98, elapsedMs / Math.max(1, estimatedWaitMs)),
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePuzzleOverallProgress(
|
||||
state: MiniGameDraftGenerationState,
|
||||
activeStepProgressRatio: number,
|
||||
) {
|
||||
const backendProgressPercent = resolvePuzzleBackendProgressPercent(state);
|
||||
// 中文注释:88 以下的后端进度只保留为会话事实,不参与首帧总进度抬升。
|
||||
// 生成页恢复时必须先从 0% 起步,再由当前步骤内的假进度平滑推进。
|
||||
const backendProgressFloor =
|
||||
backendProgressPercent != null &&
|
||||
backendProgressPercent >= PUZZLE_COMPILE_MILESTONE_PROGRESS
|
||||
? backendProgressPercent
|
||||
: 0;
|
||||
const range =
|
||||
state.phase === 'puzzle-select-image'
|
||||
? {
|
||||
start: PUZZLE_UI_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_NON_READY_MAX_PROGRESS,
|
||||
}
|
||||
: state.phase === 'puzzle-ui-assets'
|
||||
? {
|
||||
start: PUZZLE_IMAGE_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_UI_MILESTONE_PROGRESS,
|
||||
}
|
||||
: state.phase === 'puzzle-cover-image' ||
|
||||
state.phase === 'puzzle-level-scene'
|
||||
? {
|
||||
start: PUZZLE_COMPILE_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_IMAGE_MILESTONE_PROGRESS,
|
||||
}
|
||||
: {
|
||||
start: 0,
|
||||
end: PUZZLE_COMPILE_MILESTONE_PROGRESS,
|
||||
};
|
||||
const fakeProgress =
|
||||
range.start + (range.end - range.start) * activeStepProgressRatio;
|
||||
const nextProgress = Math.min(
|
||||
PUZZLE_NON_READY_MAX_PROGRESS,
|
||||
Math.max(range.start, backendProgressFloor, fakeProgress),
|
||||
);
|
||||
|
||||
return nextProgress;
|
||||
}
|
||||
|
||||
export function buildMiniGameDraftGenerationProgress(
|
||||
@@ -726,11 +852,11 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? state.finishedAtMs
|
||||
: nowMs;
|
||||
const elapsedMs = Math.max(0, effectiveNowMs - state.startedAtMs);
|
||||
const puzzleTimeline =
|
||||
const puzzleBackendPhase =
|
||||
state.kind === 'puzzle' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? resolvePuzzleTimelineByElapsedMs(elapsedMs, state)
|
||||
? resolvePuzzlePhaseByBackendProgress(state)
|
||||
: null;
|
||||
const woodenFishTimeline =
|
||||
state.kind === 'wooden-fish' &&
|
||||
@@ -739,10 +865,10 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? resolveWoodenFishTimelineByElapsedMs(elapsedMs)
|
||||
: null;
|
||||
const normalizedState =
|
||||
puzzleTimeline != null
|
||||
puzzleBackendPhase != null
|
||||
? {
|
||||
...state,
|
||||
phase: puzzleTimeline.phase,
|
||||
phase: puzzleBackendPhase,
|
||||
}
|
||||
: woodenFishTimeline != null
|
||||
? {
|
||||
@@ -786,47 +912,83 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
}
|
||||
: state;
|
||||
|
||||
const puzzleTimedSteps =
|
||||
normalizedState.kind === 'puzzle'
|
||||
? buildPuzzleTimedSteps(normalizedState)
|
||||
: null;
|
||||
const steps =
|
||||
normalizedState.kind === 'puzzle'
|
||||
? buildPuzzleSteps(normalizedState)
|
||||
? buildWeightedPuzzleSteps(
|
||||
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
|
||||
)
|
||||
: getStepDefinitions(normalizedState.kind);
|
||||
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
|
||||
const activeStep = steps[activeStepIndex] ?? steps[0];
|
||||
const activeStepProgressRatio =
|
||||
normalizedState.kind === 'puzzle'
|
||||
? normalizedState.phase === 'ready'
|
||||
? 1
|
||||
: normalizedState.phase === 'failed'
|
||||
? 0
|
||||
: resolvePuzzleActiveStepElapsedProgressRatio(
|
||||
normalizedState,
|
||||
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
|
||||
activeStepIndex,
|
||||
elapsedMs,
|
||||
effectiveNowMs,
|
||||
)
|
||||
: normalizedState.totalAssetCount > 0
|
||||
? Math.min(
|
||||
1,
|
||||
normalizedState.completedAssetCount / normalizedState.totalAssetCount,
|
||||
)
|
||||
: normalizedState.phase === 'ready'
|
||||
? 1
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'match3d'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'jump-hop'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? (woodenFishTimeline?.activeStepProgressRatio ?? 0)
|
||||
: 0;
|
||||
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 === 'puzzle'
|
||||
? (puzzleTimeline?.activeStepProgressRatio ?? 0)
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? 0.55
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? 0.42
|
||||
: normalizedState.kind === 'match3d'
|
||||
? 0.5
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? 0.52
|
||||
: normalizedState.kind === 'jump-hop'
|
||||
? 0.5
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? (woodenFishTimeline?.activeStepProgressRatio ?? 0)
|
||||
: 0;
|
||||
const overallProgress =
|
||||
normalizedState.phase === 'failed'
|
||||
? Math.max(1, completedWeight)
|
||||
: normalizedState.phase === 'ready'
|
||||
? 100
|
||||
: completedWeight + activeStep.weight * assetRatio;
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? resolvePuzzleOverallProgress(
|
||||
normalizedState,
|
||||
activeStepProgressRatio,
|
||||
)
|
||||
: completedWeight + activeStep.weight * activeStepProgressRatio;
|
||||
const cappedOverallProgress =
|
||||
normalizedState.phase === 'ready' || normalizedState.phase === 'failed'
|
||||
? overallProgress
|
||||
@@ -890,7 +1052,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
steps,
|
||||
activeStepIndex,
|
||||
normalizedState,
|
||||
assetRatio,
|
||||
activeStepProgressRatio,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user