# Conflicts: # server-rs/crates/api-server/src/jump_hop.rs # server-rs/crates/api-server/src/modules/jump_hop.rs
1478 lines
43 KiB
TypeScript
1478 lines
43 KiB
TypeScript
import type { BigFishSessionSnapshotResponse } from '../../packages/shared/src/contracts/bigFish';
|
||
import type {
|
||
BabyObjectMatchDraft,
|
||
CreateBabyObjectMatchDraftRequest,
|
||
} from '../../packages/shared/src/contracts/edutainmentBabyObject';
|
||
import type {
|
||
CreateMatch3DSessionRequest,
|
||
Match3DAgentSessionSnapshot,
|
||
} from '../../packages/shared/src/contracts/match3dAgent';
|
||
import type {
|
||
CreatePuzzleAgentSessionRequest,
|
||
PuzzleAgentSessionSnapshot,
|
||
} from '../../packages/shared/src/contracts/puzzleAgentSession';
|
||
import type {
|
||
PuzzleClearSessionSnapshotResponse,
|
||
PuzzleClearWorkspaceCreateRequest,
|
||
} from '../../packages/shared/src/contracts/puzzleClear';
|
||
import type {
|
||
CustomWorldGenerationProgress,
|
||
CustomWorldGenerationStep,
|
||
} from '../../packages/shared/src/contracts/runtime';
|
||
import type { SquareHoleSessionSnapshot } from '../../packages/shared/src/contracts/squareHoleAgent';
|
||
import type {
|
||
WoodenFishSessionSnapshotResponse,
|
||
WoodenFishWorkspaceCreateRequest,
|
||
} from '../../packages/shared/src/contracts/woodenFish';
|
||
import type { CustomWorldStructuredAnchorEntry } from './customWorldAgentGenerationProgress';
|
||
import type {
|
||
CreateJumpHopSessionRequest,
|
||
JumpHopSessionSnapshot,
|
||
} from './jump-hop/jumpHopClient';
|
||
|
||
export type MiniGameDraftGenerationKind =
|
||
| 'puzzle'
|
||
| 'big-fish'
|
||
| 'square-hole'
|
||
| 'match3d'
|
||
| 'baby-object-match'
|
||
| 'jump-hop'
|
||
| 'puzzle-clear'
|
||
| 'wooden-fish';
|
||
|
||
export type MiniGameDraftGenerationPhase =
|
||
| 'idle'
|
||
| 'compile'
|
||
| 'puzzle-level-name'
|
||
| 'big-fish-draft'
|
||
| 'big-fish-levels'
|
||
| 'big-fish-runtime'
|
||
| 'square-hole-draft'
|
||
| 'square-hole-cover'
|
||
| 'square-hole-shapes'
|
||
| 'square-hole-ready'
|
||
| 'match3d-work-title'
|
||
| 'match3d-item-names'
|
||
| 'match3d-background-prompt'
|
||
| 'match3d-material-sheet'
|
||
| 'match3d-slice-images'
|
||
| 'match3d-upload-images'
|
||
| 'match3d-generate-views'
|
||
| 'match3d-background-image'
|
||
| 'match3d-level-scene'
|
||
| 'match3d-derived-assets'
|
||
| 'match3d-parse-spritesheet'
|
||
| 'match3d-write-draft'
|
||
| 'match3d-ready'
|
||
| 'baby-object-draft'
|
||
| 'baby-object-images'
|
||
| 'baby-object-ready'
|
||
| 'jump-hop-draft'
|
||
| 'jump-hop-tile-atlas'
|
||
| 'jump-hop-slice-tiles'
|
||
| 'jump-hop-write-draft'
|
||
| 'puzzle-clear-draft'
|
||
| 'puzzle-clear-background'
|
||
| 'puzzle-clear-atlas'
|
||
| 'puzzle-clear-slices'
|
||
| 'puzzle-clear-write-draft'
|
||
| 'wooden-fish-draft'
|
||
| 'wooden-fish-hit-object'
|
||
| 'wooden-fish-background'
|
||
| 'wooden-fish-back-button'
|
||
| 'wooden-fish-write-draft'
|
||
| 'puzzle-cover-image'
|
||
| 'puzzle-level-scene'
|
||
| 'puzzle-ui-assets'
|
||
| 'puzzle-images'
|
||
| 'puzzle-ui-background'
|
||
| 'puzzle-select-image'
|
||
| 'ready'
|
||
| 'failed';
|
||
|
||
export type MiniGameDraftGenerationState = {
|
||
kind: MiniGameDraftGenerationKind;
|
||
phase: MiniGameDraftGenerationPhase;
|
||
startedAtMs: number;
|
||
finishedAtMs?: number;
|
||
completedAssetCount: number;
|
||
totalAssetCount: number;
|
||
error: string | null;
|
||
metadata?: {
|
||
puzzleAiRedraw?: boolean;
|
||
puzzleActivePhaseId?: MiniGameDraftGenerationPhase;
|
||
puzzleActiveStepStartedAtMs?: number;
|
||
puzzleProgressPercent?: number;
|
||
};
|
||
};
|
||
|
||
type MiniGameStepDefinition = {
|
||
id: MiniGameDraftGenerationPhase;
|
||
label: string;
|
||
detail: string;
|
||
weight: number;
|
||
};
|
||
|
||
type TimedMiniGameStepDefinition = Omit<MiniGameStepDefinition, 'weight'> & {
|
||
durationMs: number;
|
||
};
|
||
|
||
type MiniGameAnchorSource = {
|
||
key: string;
|
||
label: string;
|
||
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;
|
||
}
|
||
|
||
function buildWeightedPuzzleSteps(
|
||
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
|
||
) {
|
||
const totalDuration = steps.reduce((sum, step) => sum + step.durationMs, 0);
|
||
let usedWeight = 0;
|
||
return steps.map((step, index) => {
|
||
const weight =
|
||
index === steps.length - 1
|
||
? Math.max(1, 100 - usedWeight)
|
||
: Math.max(1, Math.round((step.durationMs / totalDuration) * 100));
|
||
usedWeight += weight;
|
||
return {
|
||
id: step.id,
|
||
label: step.label,
|
||
detail: step.detail,
|
||
weight,
|
||
} satisfies MiniGameStepDefinition;
|
||
});
|
||
}
|
||
|
||
function buildPuzzleTimedSteps(state: MiniGameDraftGenerationState) {
|
||
const steps: TimedMiniGameStepDefinition[] = [
|
||
{
|
||
id: 'compile',
|
||
label: '编译首关草稿',
|
||
detail: '建立可恢复草稿,整理首关描述与关卡结构,约 8 秒。',
|
||
durationMs: PUZZLE_COMPILE_EXPECTED_MS,
|
||
},
|
||
{
|
||
id: 'puzzle-level-name',
|
||
label: '生成关卡名称',
|
||
detail: '根据描述生成关卡名、作品描述和标签,约 10 秒。',
|
||
durationMs: PUZZLE_LEVEL_NAME_EXPECTED_MS,
|
||
},
|
||
];
|
||
|
||
if (!shouldSkipPuzzleCoverGeneration(state)) {
|
||
steps.push({
|
||
id: 'puzzle-cover-image',
|
||
label: '生成拼图首图',
|
||
detail: '生成 1:1 拼图首图,预计 4 分钟。',
|
||
durationMs: PUZZLE_COVER_IMAGE_GENERATION_EXPECTED_MS,
|
||
});
|
||
}
|
||
|
||
steps.push(
|
||
{
|
||
id: 'puzzle-level-scene',
|
||
label: '生成关卡画面',
|
||
detail: shouldSkipPuzzleCoverGeneration(state)
|
||
? '直接使用上传图作为参考,生成 9:16 完整关卡画面,预计 90 秒。'
|
||
: '使用拼图首图作为参考,生成 9:16 完整关卡画面,预计 90 秒。',
|
||
durationMs: PUZZLE_IMAGE_GENERATION_EXPECTED_MS,
|
||
},
|
||
{
|
||
id: 'puzzle-ui-assets',
|
||
label: '生成UI与背景',
|
||
detail:
|
||
'用关卡画面作参考,并发生成 UI spritesheet 与 9:16 纯背景,预计 90 秒。',
|
||
durationMs: PUZZLE_IMAGE_GENERATION_EXPECTED_MS,
|
||
},
|
||
{
|
||
id: 'puzzle-select-image',
|
||
label: '写入正式草稿',
|
||
detail: '校验资产并写入正式首关、作品摘要和草稿投影,约 10 秒。',
|
||
durationMs: PUZZLE_WRITE_DRAFT_EXPECTED_MS,
|
||
},
|
||
);
|
||
return steps;
|
||
}
|
||
|
||
function buildPuzzleSteps(state: MiniGameDraftGenerationState) {
|
||
return buildWeightedPuzzleSteps(buildPuzzleTimedSteps(state));
|
||
}
|
||
|
||
function resolvePuzzleEstimatedWaitMs(state: MiniGameDraftGenerationState) {
|
||
return buildPuzzleTimedSteps(state).reduce(
|
||
(sum, step) => sum + step.durationMs,
|
||
0,
|
||
);
|
||
}
|
||
|
||
const PUZZLE_NON_READY_MAX_PROGRESS = 98;
|
||
const BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS = 6 * 60_000;
|
||
|
||
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 = [
|
||
{
|
||
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<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>;
|
||
|
||
const MATCH3D_STEPS = [
|
||
{
|
||
id: 'match3d-work-title',
|
||
label: '建立草稿存档',
|
||
detail: '创建可恢复作品草稿,锁定本次题材和难度。',
|
||
weight: 8,
|
||
},
|
||
{
|
||
id: 'match3d-item-names',
|
||
label: '生成作品计划',
|
||
detail: '生成游戏名称、物品名称与标签。',
|
||
weight: 10,
|
||
},
|
||
{
|
||
id: 'match3d-level-scene',
|
||
label: '生成关卡整图',
|
||
detail: '生成 9:16 完整抓大鹅关卡画面。',
|
||
weight: 28,
|
||
},
|
||
{
|
||
id: 'match3d-derived-assets',
|
||
label: '生成三张派生图',
|
||
detail: '以关卡整图为参考,并发生成 UI、背景和 10x10 物品 Sprite。',
|
||
weight: 34,
|
||
},
|
||
{
|
||
id: 'match3d-parse-spritesheet',
|
||
label: '解析物品Sprite',
|
||
detail: '解析 20 个物品和每个物品的 5 个形态,并上传透明 PNG。',
|
||
weight: 18,
|
||
},
|
||
{
|
||
id: 'match3d-write-draft',
|
||
label: '写入草稿页',
|
||
detail: '保存关卡整图、派生图集、20 种物品素材和作品草稿。',
|
||
weight: 2,
|
||
},
|
||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||
|
||
const MATCH3D_ESTIMATED_WAIT_MS = 460_000;
|
||
|
||
const MATCH3D_PHASE_ORDER: Partial<
|
||
Record<MiniGameDraftGenerationPhase, number>
|
||
> = {
|
||
'match3d-work-title': 0,
|
||
'match3d-item-names': 1,
|
||
'match3d-level-scene': 2,
|
||
'match3d-derived-assets': 3,
|
||
'match3d-parse-spritesheet': 4,
|
||
'match3d-write-draft': 5,
|
||
// 中文注释:旧生成页阶段在恢复生成中草稿时归并到新流程对应阶段。
|
||
'match3d-background-prompt': 1,
|
||
'match3d-material-sheet': 3,
|
||
'match3d-slice-images': 4,
|
||
'match3d-upload-images': 4,
|
||
'match3d-generate-views': 4,
|
||
'match3d-background-image': 3,
|
||
};
|
||
|
||
function normalizeMatch3DGenerationPhase(
|
||
phase: MiniGameDraftGenerationPhase,
|
||
): MiniGameDraftGenerationPhase {
|
||
switch (phase) {
|
||
case 'match3d-background-prompt':
|
||
return 'match3d-item-names';
|
||
case 'match3d-material-sheet':
|
||
case 'match3d-background-image':
|
||
return 'match3d-derived-assets';
|
||
case 'match3d-slice-images':
|
||
case 'match3d-upload-images':
|
||
case 'match3d-generate-views':
|
||
return 'match3d-parse-spritesheet';
|
||
default:
|
||
return phase;
|
||
}
|
||
}
|
||
|
||
const BABY_OBJECT_MATCH_STEPS = [
|
||
{
|
||
id: 'baby-object-draft',
|
||
label: '整理识物草稿',
|
||
detail: '写入两个物品名称与寓教于乐标签。',
|
||
weight: 22,
|
||
},
|
||
{
|
||
id: 'baby-object-images',
|
||
label: '生成游戏素材',
|
||
detail: '生成物品图、背景、礼物盒、篮子和界面包装。',
|
||
weight: 68,
|
||
},
|
||
{
|
||
id: 'baby-object-ready',
|
||
label: '准备结果页',
|
||
detail: '校验草稿字段并进入结果页。',
|
||
weight: 10,
|
||
},
|
||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||
|
||
const JUMP_HOP_STEPS = [
|
||
{
|
||
id: 'jump-hop-draft',
|
||
label: '整理玩法草稿',
|
||
detail: '保存主题并派生作品信息和默认角色配置。',
|
||
weight: 12,
|
||
},
|
||
{
|
||
id: 'jump-hop-tile-atlas',
|
||
label: '生成 5x5 地块图集',
|
||
detail: '调用 image2 生成 25 个主题地块素材。',
|
||
weight: 54,
|
||
},
|
||
{
|
||
id: 'jump-hop-slice-tiles',
|
||
label: '切分 25 个地块',
|
||
detail: '按 5 行 5 列切分透明地块 PNG。',
|
||
weight: 24,
|
||
},
|
||
{
|
||
id: 'jump-hop-write-draft',
|
||
label: '写入正式草稿',
|
||
detail: '保存地块池、无限路径缓冲和运行态配置。',
|
||
weight: 10,
|
||
},
|
||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||
|
||
const JUMP_HOP_ESTIMATED_WAIT_MS = 5 * 60_000;
|
||
|
||
const PUZZLE_CLEAR_STEPS = [
|
||
{
|
||
id: 'puzzle-clear-draft',
|
||
label: '整理玩法草稿',
|
||
detail: '保存作品信息、主题词与底图策略。',
|
||
weight: 8,
|
||
},
|
||
{
|
||
id: 'puzzle-clear-background',
|
||
label: '准备场地底图',
|
||
detail: '处理上传底图或生成中央场地底图。',
|
||
weight: 22,
|
||
},
|
||
{
|
||
id: 'puzzle-clear-atlas',
|
||
label: '生成复合图集',
|
||
detail: '生成 135 组复合图案 atlas。',
|
||
weight: 42,
|
||
},
|
||
{
|
||
id: 'puzzle-clear-slices',
|
||
label: '切分卡牌碎片',
|
||
detail: '按预排坐标切成 1x1 卡牌碎片并校验。',
|
||
weight: 20,
|
||
},
|
||
{
|
||
id: 'puzzle-clear-write-draft',
|
||
label: '写入正式草稿',
|
||
detail: '保存底图、图集、碎片和作品摘要。',
|
||
weight: 8,
|
||
},
|
||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||
|
||
const PUZZLE_CLEAR_ESTIMATED_WAIT_MS = 620_000;
|
||
|
||
const WOODEN_FISH_STEPS = [
|
||
{
|
||
id: 'wooden-fish-draft',
|
||
label: '整理玩法草稿',
|
||
detail: '保存作品信息、敲击物、音效和飘字配置。',
|
||
weight: 8,
|
||
},
|
||
{
|
||
id: 'wooden-fish-hit-object',
|
||
label: '生成敲击物图案',
|
||
detail: '调用 image2 生成绿幕敲击物并去绿透明化,预计约 3 分钟。',
|
||
weight: 32,
|
||
},
|
||
{
|
||
id: 'wooden-fish-background',
|
||
label: '生成背景环境图',
|
||
detail: '使用透明敲击物作参考生成 9:16 背景环境图,预计约 3 分钟。',
|
||
weight: 32,
|
||
},
|
||
{
|
||
id: 'wooden-fish-back-button',
|
||
label: '生成返回按钮图',
|
||
detail: '使用敲击物和背景作参考生成主题圆形返回按钮,预计约 3 分钟。',
|
||
weight: 20,
|
||
},
|
||
{
|
||
id: 'wooden-fish-write-draft',
|
||
label: '写入正式草稿',
|
||
detail: '保存图案、背景、返回按钮、音效、飘字和封面摘要。',
|
||
weight: 8,
|
||
},
|
||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||
|
||
const WOODEN_FISH_COMPILE_EXPECTED_MS = 8_000;
|
||
const WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS = 180_000;
|
||
const WOODEN_FISH_WRITE_DRAFT_EXPECTED_MS = 10_000;
|
||
const WOODEN_FISH_ESTIMATED_WAIT_MS =
|
||
WOODEN_FISH_COMPILE_EXPECTED_MS +
|
||
WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS * 3 +
|
||
WOODEN_FISH_WRITE_DRAFT_EXPECTED_MS;
|
||
|
||
function clampProgress(value: number) {
|
||
return Math.max(0, Math.min(100, Math.round(value)));
|
||
}
|
||
|
||
export function resolveMiniGameDraftGenerationStartedAtMs(
|
||
startedAt: string | number | null | undefined,
|
||
fallbackMs = Date.now(),
|
||
) {
|
||
if (typeof startedAt === 'number' && Number.isFinite(startedAt)) {
|
||
return startedAt;
|
||
}
|
||
|
||
if (typeof startedAt === 'string') {
|
||
const parsed = Date.parse(startedAt);
|
||
if (Number.isFinite(parsed)) {
|
||
return parsed;
|
||
}
|
||
}
|
||
|
||
return fallbackMs;
|
||
}
|
||
|
||
function getStepDefinitions(kind: MiniGameDraftGenerationKind) {
|
||
if (kind === 'puzzle') {
|
||
return buildPuzzleSteps(createMiniGameDraftGenerationState('puzzle'));
|
||
}
|
||
if (kind === 'square-hole') {
|
||
return SQUARE_HOLE_STEPS;
|
||
}
|
||
if (kind === 'match3d') {
|
||
return MATCH3D_STEPS;
|
||
}
|
||
if (kind === 'baby-object-match') {
|
||
return BABY_OBJECT_MATCH_STEPS;
|
||
}
|
||
if (kind === 'jump-hop') {
|
||
return JUMP_HOP_STEPS;
|
||
}
|
||
if (kind === 'puzzle-clear') {
|
||
return PUZZLE_CLEAR_STEPS;
|
||
}
|
||
if (kind === 'wooden-fish') {
|
||
return WOODEN_FISH_STEPS;
|
||
}
|
||
return BIG_FISH_STEPS;
|
||
}
|
||
|
||
function getActiveStepIndex(
|
||
steps: ReadonlyArray<MiniGameStepDefinition>,
|
||
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<MiniGameStepDefinition>,
|
||
activeStepIndex: number,
|
||
state: MiniGameDraftGenerationState,
|
||
activeStepProgressRatio: number,
|
||
) {
|
||
return steps.map((step, index) => {
|
||
// 中文注释:拼图草稿编译的 action 回包才代表可进入结果页;
|
||
// 但预计写入时长已耗尽时,最后一步自身应呈现已完成,避免出现“进行中 100%”。
|
||
const isTimedWriteStepCompleted =
|
||
(state.kind === 'puzzle' || state.kind === 'wooden-fish') &&
|
||
state.phase !== 'failed' &&
|
||
(step.id === 'puzzle-select-image' ||
|
||
step.id === 'wooden-fish-write-draft') &&
|
||
clampProgress(activeStepProgressRatio * 100) >= 100;
|
||
const isCompleted =
|
||
state.phase === 'ready' ||
|
||
index < activeStepIndex ||
|
||
isTimedWriteStepCompleted;
|
||
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
|
||
: isActive
|
||
? activeStepProgressRatio
|
||
: 0,
|
||
total: isAssetStep ? state.totalAssetCount : 1,
|
||
status: isCompleted ? 'completed' : isActive ? 'active' : 'pending',
|
||
} satisfies CustomWorldGenerationStep;
|
||
});
|
||
}
|
||
|
||
export function createMiniGameDraftGenerationState(
|
||
kind: MiniGameDraftGenerationKind,
|
||
startedAtMs = Date.now(),
|
||
): MiniGameDraftGenerationState {
|
||
return {
|
||
kind,
|
||
phase:
|
||
kind === 'big-fish'
|
||
? 'big-fish-draft'
|
||
: kind === 'square-hole'
|
||
? 'square-hole-draft'
|
||
: kind === 'match3d'
|
||
? 'match3d-work-title'
|
||
: kind === 'baby-object-match'
|
||
? 'baby-object-draft'
|
||
: kind === 'jump-hop'
|
||
? 'jump-hop-draft'
|
||
: kind === 'puzzle-clear'
|
||
? 'puzzle-clear-draft'
|
||
: kind === 'wooden-fish'
|
||
? 'wooden-fish-draft'
|
||
: 'compile',
|
||
startedAtMs,
|
||
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';
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
function resolveMatch3DPhaseByElapsedMs(
|
||
elapsedMs: number,
|
||
currentPhase: MiniGameDraftGenerationPhase,
|
||
): MiniGameDraftGenerationPhase {
|
||
const elapsedPhase =
|
||
elapsedMs >= 450_000
|
||
? 'match3d-write-draft'
|
||
: elapsedMs >= 360_000
|
||
? 'match3d-parse-spritesheet'
|
||
: elapsedMs >= 118_000
|
||
? 'match3d-derived-assets'
|
||
: elapsedMs >= 28_000
|
||
? 'match3d-level-scene'
|
||
: elapsedMs >= 8_000
|
||
? 'match3d-item-names'
|
||
: 'match3d-work-title';
|
||
const elapsedOrder = MATCH3D_PHASE_ORDER[elapsedPhase] ?? 0;
|
||
const normalizedCurrentPhase = normalizeMatch3DGenerationPhase(currentPhase);
|
||
const currentOrder = MATCH3D_PHASE_ORDER[normalizedCurrentPhase] ?? -1;
|
||
return currentOrder > elapsedOrder ? normalizedCurrentPhase : elapsedPhase;
|
||
}
|
||
|
||
function resolveBabyObjectMatchPhaseByElapsedMs(
|
||
elapsedMs: number,
|
||
): MiniGameDraftGenerationPhase {
|
||
if (elapsedMs >= 330_000) {
|
||
return 'baby-object-ready';
|
||
}
|
||
if (elapsedMs >= 8_000) {
|
||
return 'baby-object-images';
|
||
}
|
||
return 'baby-object-draft';
|
||
}
|
||
|
||
function resolveJumpHopPhaseByElapsedMs(
|
||
elapsedMs: number,
|
||
): MiniGameDraftGenerationPhase {
|
||
if (elapsedMs >= 270_000) {
|
||
return 'jump-hop-write-draft';
|
||
}
|
||
if (elapsedMs >= 220_000) {
|
||
return 'jump-hop-slice-tiles';
|
||
}
|
||
if (elapsedMs >= 115_000) {
|
||
return 'jump-hop-tile-atlas';
|
||
}
|
||
if (elapsedMs >= 12_000) {
|
||
return 'jump-hop-tile-atlas';
|
||
}
|
||
return 'jump-hop-draft';
|
||
}
|
||
|
||
function resolvePuzzleClearPhaseByElapsedMs(
|
||
elapsedMs: number,
|
||
): MiniGameDraftGenerationPhase {
|
||
if (elapsedMs >= 590_000) {
|
||
return 'puzzle-clear-write-draft';
|
||
}
|
||
if (elapsedMs >= 470_000) {
|
||
return 'puzzle-clear-slices';
|
||
}
|
||
if (elapsedMs >= 120_000) {
|
||
return 'puzzle-clear-atlas';
|
||
}
|
||
if (elapsedMs >= 8_000) {
|
||
return 'puzzle-clear-background';
|
||
}
|
||
return 'puzzle-clear-draft';
|
||
}
|
||
|
||
function buildWoodenFishPhaseTimeline(): Array<{
|
||
phase: Extract<
|
||
MiniGameDraftGenerationPhase,
|
||
| 'wooden-fish-draft'
|
||
| 'wooden-fish-hit-object'
|
||
| 'wooden-fish-background'
|
||
| 'wooden-fish-back-button'
|
||
| 'wooden-fish-write-draft'
|
||
>;
|
||
durationMs: number;
|
||
}> {
|
||
return [
|
||
{
|
||
phase: 'wooden-fish-draft',
|
||
durationMs: WOODEN_FISH_COMPILE_EXPECTED_MS,
|
||
},
|
||
{
|
||
phase: 'wooden-fish-hit-object',
|
||
durationMs: WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS,
|
||
},
|
||
{
|
||
phase: 'wooden-fish-background',
|
||
durationMs: WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS,
|
||
},
|
||
{
|
||
phase: 'wooden-fish-back-button',
|
||
durationMs: WOODEN_FISH_IMAGE_GENERATION_EXPECTED_MS,
|
||
},
|
||
{
|
||
phase: 'wooden-fish-write-draft',
|
||
durationMs: WOODEN_FISH_WRITE_DRAFT_EXPECTED_MS,
|
||
},
|
||
];
|
||
}
|
||
|
||
function resolveWoodenFishTimelineByElapsedMs(elapsedMs: number) {
|
||
let elapsedBeforePhase = 0;
|
||
|
||
for (const item of buildWoodenFishPhaseTimeline()) {
|
||
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;
|
||
}
|
||
|
||
return {
|
||
phase: 'wooden-fish-write-draft' as const,
|
||
activeStepProgressRatio: 1,
|
||
};
|
||
}
|
||
|
||
function resolvePuzzleActiveStepProgressRatio(
|
||
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
|
||
activeStepIndex: number,
|
||
elapsedMs: number,
|
||
) {
|
||
const activeStep = steps[activeStepIndex];
|
||
if (!activeStep) {
|
||
return 0;
|
||
}
|
||
|
||
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 === 'puzzle-clear'
|
||
? PUZZLE_CLEAR_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(
|
||
state: MiniGameDraftGenerationState | null,
|
||
nowMs = Date.now(),
|
||
): CustomWorldGenerationProgress | null {
|
||
if (!state) {
|
||
return null;
|
||
}
|
||
|
||
const effectiveNowMs =
|
||
typeof state.finishedAtMs === 'number' &&
|
||
Number.isFinite(state.finishedAtMs)
|
||
? state.finishedAtMs
|
||
: nowMs;
|
||
const elapsedMs = Math.max(0, effectiveNowMs - state.startedAtMs);
|
||
const puzzleBackendPhase =
|
||
state.kind === 'puzzle' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? resolvePuzzlePhaseByBackendProgress(state)
|
||
: null;
|
||
const woodenFishTimeline =
|
||
state.kind === 'wooden-fish' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? resolveWoodenFishTimelineByElapsedMs(elapsedMs)
|
||
: null;
|
||
const normalizedState =
|
||
puzzleBackendPhase != null
|
||
? {
|
||
...state,
|
||
phase: puzzleBackendPhase,
|
||
}
|
||
: woodenFishTimeline != null
|
||
? {
|
||
...state,
|
||
phase: woodenFishTimeline.phase,
|
||
}
|
||
: state.kind === 'big-fish' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? {
|
||
...state,
|
||
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
||
}
|
||
: state.kind === 'square-hole' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? {
|
||
...state,
|
||
phase: resolveSquareHolePhaseByElapsedMs(elapsedMs),
|
||
}
|
||
: state.kind === 'match3d' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? {
|
||
...state,
|
||
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs, state.phase),
|
||
}
|
||
: state.kind === 'baby-object-match' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? {
|
||
...state,
|
||
phase: resolveBabyObjectMatchPhaseByElapsedMs(elapsedMs),
|
||
}
|
||
: state.kind === 'jump-hop' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? {
|
||
...state,
|
||
phase: resolveJumpHopPhaseByElapsedMs(elapsedMs),
|
||
}
|
||
: state.kind === 'puzzle-clear' &&
|
||
state.phase !== 'failed' &&
|
||
state.phase !== 'ready'
|
||
? {
|
||
...state,
|
||
phase: resolvePuzzleClearPhaseByElapsedMs(elapsedMs),
|
||
}
|
||
: state;
|
||
|
||
const puzzleTimedSteps =
|
||
normalizedState.kind === 'puzzle'
|
||
? buildPuzzleTimedSteps(normalizedState)
|
||
: null;
|
||
const steps =
|
||
normalizedState.kind === 'puzzle'
|
||
? buildWeightedPuzzleSteps(
|
||
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
|
||
)
|
||
: getStepDefinitions(normalizedState.kind);
|
||
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
|
||
const activeStep = steps[activeStepIndex] ?? steps[0];
|
||
const activeStepProgressRatio =
|
||
normalizedState.phase === 'failed'
|
||
? 0
|
||
: normalizedState.kind === 'puzzle'
|
||
? normalizedState.phase === 'ready'
|
||
? 1
|
||
: 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 === 'puzzle-clear'
|
||
? 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 overallProgress =
|
||
normalizedState.phase === 'failed'
|
||
? Math.max(1, completedWeight)
|
||
: normalizedState.phase === 'ready'
|
||
? 100
|
||
: completedWeight + activeStep.weight * activeStepProgressRatio;
|
||
const cappedOverallProgress =
|
||
normalizedState.phase === 'ready' || normalizedState.phase === 'failed'
|
||
? overallProgress
|
||
: Math.min(PUZZLE_NON_READY_MAX_PROGRESS, overallProgress);
|
||
|
||
return {
|
||
phaseId: normalizedState.phase,
|
||
phaseLabel:
|
||
normalizedState.phase === 'failed'
|
||
? '生成失败'
|
||
: normalizedState.phase === 'ready'
|
||
? '生成完成'
|
||
: activeStep.label,
|
||
phaseDetail:
|
||
normalizedState.error ??
|
||
(normalizedState.phase === 'ready'
|
||
? normalizedState.kind === 'big-fish'
|
||
? '玩法草稿已准备完成,可进入结果页继续生成主图、动作和背景。'
|
||
: normalizedState.kind === 'match3d'
|
||
? '抓大鹅素材与草稿已准备完成,可进入结果页继续编辑。'
|
||
: normalizedState.kind === 'baby-object-match'
|
||
? '宝贝识物草稿已准备完成,可进入结果页继续发布。'
|
||
: normalizedState.kind === 'jump-hop'
|
||
? '跳一跳草稿已准备完成,可进入结果页试玩或发布。'
|
||
: normalizedState.kind === 'puzzle-clear'
|
||
? '拼消消草稿已准备完成,可进入结果页试玩或发布。'
|
||
: normalizedState.kind === 'wooden-fish'
|
||
? '敲木鱼草稿已准备完成,可进入结果页试玩或发布。'
|
||
: '首关草稿与正式图已准备完成,可进入结果页补作品信息。'
|
||
: activeStep.detail),
|
||
batchLabel: activeStep.label,
|
||
overallProgress: clampProgress(cappedOverallProgress),
|
||
completedWeight: clampProgress(cappedOverallProgress),
|
||
totalWeight: 100,
|
||
elapsedMs,
|
||
estimatedRemainingMs:
|
||
normalizedState.phase === 'ready' || normalizedState.phase === 'failed'
|
||
? 0
|
||
: normalizedState.kind === 'puzzle'
|
||
? Math.max(
|
||
0,
|
||
resolvePuzzleEstimatedWaitMs(normalizedState) - elapsedMs,
|
||
)
|
||
: normalizedState.kind === 'big-fish'
|
||
? Math.max(0, 7_000 - elapsedMs)
|
||
: normalizedState.kind === 'square-hole'
|
||
? Math.max(0, 12_000 - elapsedMs)
|
||
: normalizedState.kind === 'match3d'
|
||
? Math.max(0, MATCH3D_ESTIMATED_WAIT_MS - elapsedMs)
|
||
: normalizedState.kind === 'baby-object-match'
|
||
? Math.max(0, BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS - elapsedMs)
|
||
: normalizedState.kind === 'jump-hop'
|
||
? Math.max(0, JUMP_HOP_ESTIMATED_WAIT_MS - elapsedMs)
|
||
: normalizedState.kind === 'puzzle-clear'
|
||
? Math.max(
|
||
0,
|
||
PUZZLE_CLEAR_ESTIMATED_WAIT_MS - elapsedMs,
|
||
)
|
||
: normalizedState.kind === 'wooden-fish'
|
||
? Math.max(0, WOODEN_FISH_ESTIMATED_WAIT_MS - elapsedMs)
|
||
: null,
|
||
activeStepIndex,
|
||
steps: buildMiniGameProgressSteps(
|
||
steps,
|
||
activeStepIndex,
|
||
normalizedState,
|
||
activeStepProgressRatio,
|
||
),
|
||
};
|
||
}
|
||
|
||
export function buildJumpHopGenerationAnchorEntries(
|
||
session: JumpHopSessionSnapshot | null | undefined,
|
||
formPayload: CreateJumpHopSessionRequest | null | undefined = null,
|
||
): CustomWorldStructuredAnchorEntry[] {
|
||
const sessionRecord = session as
|
||
| {
|
||
config?: Partial<CreateJumpHopSessionRequest>;
|
||
draft?: {
|
||
workTitle?: string;
|
||
themeText?: string;
|
||
characterPrompt?: string;
|
||
tilePrompt?: string;
|
||
} | null;
|
||
}
|
||
| null
|
||
| undefined;
|
||
const config = sessionRecord?.config;
|
||
const draft = sessionRecord?.draft;
|
||
const entries: Array<MiniGameAnchorSource | null> = [
|
||
{
|
||
key: 'jump-hop-theme',
|
||
label: '主题',
|
||
value:
|
||
formPayload?.themeText?.trim() ||
|
||
config?.themeText?.trim() ||
|
||
draft?.themeText?.trim() ||
|
||
draft?.workTitle?.trim() ||
|
||
'',
|
||
},
|
||
{
|
||
key: 'jump-hop-tile-style',
|
||
label: '地块图集',
|
||
value:
|
||
formPayload?.tilePrompt?.trim() ||
|
||
config?.tilePrompt?.trim() ||
|
||
draft?.tilePrompt?.trim() ||
|
||
'',
|
||
},
|
||
];
|
||
|
||
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 buildWoodenFishGenerationAnchorEntries(
|
||
session: WoodenFishSessionSnapshotResponse | null | undefined,
|
||
formPayload: WoodenFishWorkspaceCreateRequest | null | undefined = null,
|
||
): CustomWorldStructuredAnchorEntry[] {
|
||
const draft = session?.draft;
|
||
const entries: Array<MiniGameAnchorSource | null> = [
|
||
{
|
||
key: 'wooden-fish-hit-object',
|
||
label: '敲击物',
|
||
value:
|
||
formPayload?.hitObjectPrompt?.trim() ||
|
||
draft?.hitObjectPrompt?.trim() ||
|
||
'',
|
||
},
|
||
{
|
||
key: 'wooden-fish-hit-sound',
|
||
label: '音效',
|
||
value:
|
||
formPayload?.hitSoundAsset?.prompt?.trim() ||
|
||
draft?.hitSoundAsset?.prompt?.trim() ||
|
||
'',
|
||
},
|
||
{
|
||
key: 'wooden-fish-words',
|
||
label: '飘字',
|
||
value:
|
||
formPayload?.floatingWords
|
||
?.map((word) => word.trim())
|
||
.filter(Boolean)
|
||
.slice(0, 8)
|
||
.join('、') ||
|
||
draft?.floatingWords
|
||
?.map((word) => word.trim())
|
||
.filter(Boolean)
|
||
.slice(0, 8)
|
||
.join('、') ||
|
||
'',
|
||
},
|
||
];
|
||
|
||
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 buildPuzzleClearGenerationAnchorEntries(
|
||
session: PuzzleClearSessionSnapshotResponse | null | undefined,
|
||
formPayload: PuzzleClearWorkspaceCreateRequest | null | undefined = null,
|
||
): CustomWorldStructuredAnchorEntry[] {
|
||
const draft = session?.draft;
|
||
const entries: Array<MiniGameAnchorSource | null> = [
|
||
{
|
||
key: 'puzzle-clear-title',
|
||
label: '作品',
|
||
value:
|
||
formPayload?.workTitle?.trim() || draft?.workTitle?.trim() || '拼消消',
|
||
},
|
||
{
|
||
key: 'puzzle-clear-theme',
|
||
label: '主题',
|
||
value:
|
||
formPayload?.themePrompt?.trim() || draft?.themePrompt?.trim() || '',
|
||
},
|
||
{
|
||
key: 'puzzle-clear-background',
|
||
label: '底图',
|
||
value:
|
||
formPayload?.boardBackgroundPrompt?.trim() ||
|
||
draft?.boardBackgroundPrompt?.trim() ||
|
||
(formPayload?.boardBackgroundAsset ?? draft?.boardBackgroundAsset)
|
||
?.prompt?.trim() ||
|
||
(formPayload?.generateBoardBackground ??
|
||
draft?.generateBoardBackground
|
||
? 'AI生成'
|
||
: '上传底图'),
|
||
},
|
||
];
|
||
|
||
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 buildPuzzleGenerationAnchorEntries(
|
||
session: PuzzleAgentSessionSnapshot | null | undefined,
|
||
formPayload: CreatePuzzleAgentSessionRequest | null | undefined = null,
|
||
): CustomWorldStructuredAnchorEntry[] {
|
||
if (!session) {
|
||
return [];
|
||
}
|
||
|
||
const entries: Array<MiniGameAnchorSource | null> = [
|
||
{
|
||
key: 'picture-description',
|
||
label: '画面描述',
|
||
value:
|
||
formPayload?.pictureDescription?.trim() ||
|
||
formPayload?.seedText?.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<MiniGameAnchorSource | null> = [
|
||
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());
|
||
}
|
||
|
||
export function buildMatch3DGenerationAnchorEntries(
|
||
session: Match3DAgentSessionSnapshot | null | undefined,
|
||
formPayload: CreateMatch3DSessionRequest | null | undefined = null,
|
||
): CustomWorldStructuredAnchorEntry[] {
|
||
if (!session && !formPayload) {
|
||
return [];
|
||
}
|
||
|
||
const config = session?.config;
|
||
const entries: Array<MiniGameAnchorSource | null> = [
|
||
{
|
||
key: 'match3d-theme',
|
||
label: '题材',
|
||
value:
|
||
formPayload?.themeText?.trim() ||
|
||
config?.themeText?.trim() ||
|
||
session?.anchorPack.theme.value ||
|
||
'',
|
||
},
|
||
{
|
||
key: 'match3d-items',
|
||
label: '素材数量',
|
||
value: `${resolveMatch3DGeneratedItemCount()} 种素材`,
|
||
},
|
||
];
|
||
|
||
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());
|
||
}
|
||
|
||
function resolveMatch3DGeneratedItemCount(
|
||
_clearCount: number | null | undefined = null,
|
||
_difficulty: number | null | undefined = null,
|
||
) {
|
||
return 20;
|
||
}
|
||
|
||
export function buildBabyObjectMatchGenerationAnchorEntries(
|
||
formPayload: CreateBabyObjectMatchDraftRequest | null | undefined,
|
||
draft: BabyObjectMatchDraft | null | undefined = null,
|
||
): CustomWorldStructuredAnchorEntry[] {
|
||
const itemNames =
|
||
formPayload?.itemAName?.trim() || formPayload?.itemBName?.trim()
|
||
? [formPayload.itemAName.trim(), formPayload.itemBName.trim()]
|
||
: (draft?.itemNames ?? []);
|
||
|
||
return itemNames.filter(Boolean).map((value, index) => ({
|
||
id: `baby-object-item-${index + 1}`,
|
||
label: `物品 ${index + 1}`,
|
||
value,
|
||
}));
|
||
}
|
||
|
||
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());
|
||
}
|