refactor: 收口小游戏生成状态模型
This commit is contained in:
@@ -1395,6 +1395,14 @@
|
|||||||
- 验证方式:`npm run test -- src/components/platform-entry/platformRpgAgentResultPreviewModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
- 验证方式:`npm run test -- src/components/platform-entry/platformRpgAgentResultPreviewModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
- 关联文档:`docs/technical/【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
- 关联文档:`docs/technical/【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||||
|
|
||||||
|
## 2026-06-04 Platform Mini Game Draft Generation State Model 收口
|
||||||
|
|
||||||
|
- 背景:`PlatformEntryFlowShellImpl.tsx` 内联维护小游戏生成状态恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并和 ready / generating 判定,壳层同时承担 API / background task 副作用和 `MiniGameDraftGenerationState` 生命周期细节。
|
||||||
|
- 决策:新增 `src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts`,收口恢复态、失败态、完成态、展示 rebase、拼图 progress phase 阈值和进度 metadata 合并。壳层继续负责 API、后台任务、React state 写入、作品架刷新、URL 和 stage 切换。
|
||||||
|
- 影响范围:拼图 / 抓大鹅 / 大鱼吃小鱼 / 方洞 / 跳一跳 / 敲木鱼 / 宝贝识物生成状态恢复、完成失败收尾、生成页返回展示和拼图轮询进度合并。
|
||||||
|
- 验证方式:`npm run test -- src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
|
- 关联文档:`docs/technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||||
|
|
||||||
## 2026-06-03 Public Work Presentation 收口
|
## 2026-06-03 Public Work Presentation 收口
|
||||||
|
|
||||||
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。
|
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
平台壳的拼图 runtime 恢复 work、跳一跳 pending session、敲木鱼 detail 恢复 session 和敲木鱼 pending session DTO 映射收口到 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,壳层只保留网络、状态、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md)。
|
平台壳的拼图 runtime 恢复 work、跳一跳 pending session、敲木鱼 detail 恢复 session 和敲木鱼 pending session DTO 映射收口到 `src/components/platform-entry/platformMiniGameSessionMappingModel.ts`,壳层只保留网络、状态、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameSessionMappingModel收口计划-2026-06-04.md)。
|
||||||
|
|
||||||
|
平台小游戏生成状态的恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并和 ready / generating 判定收口到 `src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts`,壳层只保留 API、后台任务、React state、URL 与 stage 副作用,规则见 [【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformMiniGameDraftGenerationStateModel收口计划-2026-06-04.md)。
|
||||||
|
|
||||||
RPG Agent 结果页发布门禁展示和预览来源 label 收口到 `src/components/platform-entry/platformRpgAgentResultPreviewModel.ts`,壳层只保留 session/profile 编排和结果页 props 传递,规则见 [【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md)。
|
RPG Agent 结果页发布门禁展示和预览来源 label 收口到 `src/components/platform-entry/platformRpgAgentResultPreviewModel.ts`,壳层只保留 session/profile 编排和结果页 props 传递,规则见 [【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformRpgAgentResultPreviewModel收口计划-2026-06-04.md)。
|
||||||
|
|
||||||
平台入口创作生成通知、pending 作品架占位、失败覆盖、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
平台入口创作生成通知、pending 作品架占位、失败覆盖、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 【前端架构】Platform Mini Game Draft Generation State Model 收口计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl.tsx` 曾内联维护小游戏生成状态的恢复、失败/完成收尾、展示 rebase、拼图后端进度合并和生成中 / ready 判定。壳层因此既要处理 API 回包、React state、后台任务、URL 和 stage,又要记住 `MiniGameDraftGenerationState` 的生命周期细节。
|
||||||
|
|
||||||
|
这些状态变换不读取 DOM,不请求网络,也不写 React state;它们属于平台层小游戏草稿生成状态 **Module**。壳层只应决定何时调用、把返回值写入对应 state。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
新增 `src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts` 作为 Platform Mini Game Draft Generation State **Module**。其公开 **Interface** 为:
|
||||||
|
|
||||||
|
- `createMiniGameDraftGenerationStateForRestoredDraft(kind, metadata?, startedAtMs?)`:为恢复的草稿重建生成态,并保留后端开始时间作为进度事实源。
|
||||||
|
- `createFailedMiniGameDraftGenerationStateForRestoredDraft(kind, updatedAt, error, metadata?)`:恢复失败草稿时按后端 `updatedAt` 建立失败态。
|
||||||
|
- `rebaseMiniGameDraftGenerationStateForDisplay(state)` 与 `rebaseMiniGameDraftBackgroundCompileTaskForDisplay(task)`:清理展示用 `finishedAtMs`,避免返回生成页后沿用结束态计时。
|
||||||
|
- `createPuzzleDraftGenerationStateFromPayload(payload, session?)`、`resolvePuzzlePhaseFromSessionProgress(state, session)`、`mergePuzzleSessionProgressIntoGenerationState(state, session)`:集中处理拼图生成的 aiRedraw、后端进度百分比和 phase 推进。
|
||||||
|
- `resolveFinishedMiniGameDraftGenerationState(state, phase, options?)`:统一完成 / 失败收尾的 `finishedAtMs`、错误与资产计数合并。
|
||||||
|
- `isMiniGameDraftReady(state)` 与 `isMiniGameDraftGenerating(state)`:统一生成态轻量判定。
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl.tsx` 仍作为 **Adapter**:它继续负责 API、background task、React state 写入、作品架刷新、URL 与 stage 切换。
|
||||||
|
|
||||||
|
## Interface 约束
|
||||||
|
|
||||||
|
- 恢复草稿状态必须允许调用方传入 `startedAtMs`;未传时使用当前时间,与旧逻辑一致。
|
||||||
|
- 恢复失败状态必须通过 `resolveMiniGameDraftGenerationStartedAtMs(updatedAt)` 解析后端时间,并保留传入 metadata。
|
||||||
|
- `resolveFinishedMiniGameDraftGenerationState` 只覆盖显式传入的 `error`、`completedAssetCount`、`totalAssetCount`;未传时沿用原 state。
|
||||||
|
- 拼图 session 只有在 `draft` 存在且不是 `formDraft` 时才视为后端编译生成中 session,才写入 `puzzleProgressPercent` 并推进 phase。
|
||||||
|
- 拼图进度阈值保持旧值:`>=96` 到 `puzzle-select-image`,`>=94` 到 `puzzle-ui-assets`,`>=88` 时按 `puzzleAiRedraw=false` 进入 `puzzle-level-scene`,否则进入 `puzzle-cover-image`。
|
||||||
|
- phase 变化时 `puzzleActiveStepStartedAtMs` 使用 session `updatedAt` 解析值;phase 不变时保留旧值。
|
||||||
|
- 展示 rebase 只清理 `finishedAtMs`,不得修改 phase、error、资产计数或 metadata。
|
||||||
|
|
||||||
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
|
- **Depth**:壳层以状态变换函数表达意图;生成态字段、拼图阈值、时间解析与计数合并藏入 Module Implementation。
|
||||||
|
- **Leverage**:后续新增小游戏生成恢复或调整拼图后端进度阈值时,先改 Module 与单测,再让壳层 Adapter 保持调用点不变。
|
||||||
|
- **Locality**:小游戏生成状态规则集中到一个纯测试面,避免在大型壳层的 API callback、background task 和恢复流程中重复推理 `MiniGameDraftGenerationState`。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
- `npm run test -- src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts`
|
||||||
|
- `npx eslint src/components/platform-entry/platformMiniGameDraftGenerationStateModel.ts src/components/platform-entry/platformMiniGameDraftGenerationStateModel.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run check:encoding`
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
拼图 runtime 刷新恢复、跳一跳生成中草稿打开和敲木鱼生成中 / detail 草稿恢复所需的 session / work DTO 映射统一由 `platformMiniGameSessionMappingModel.ts` 构造。平台壳只负责读取后端、写入本地 state、写 URL 和切换 stage;不得在壳层重新手写 sessionId 优先级、pending draft 空素材默认值或拼图稳定 ID 映射。
|
拼图 runtime 刷新恢复、跳一跳生成中草稿打开和敲木鱼生成中 / detail 草稿恢复所需的 session / work DTO 映射统一由 `platformMiniGameSessionMappingModel.ts` 构造。平台壳只负责读取后端、写入本地 state、写 URL 和切换 stage;不得在壳层重新手写 sessionId 优先级、pending draft 空素材默认值或拼图稳定 ID 映射。
|
||||||
|
|
||||||
|
平台小游戏生成状态的恢复、失败 / 完成收尾、展示 rebase、拼图后端进度合并和 ready / generating 判定统一由 `platformMiniGameDraftGenerationStateModel.ts` 处理。平台壳只决定何时调用并写入对应 React state,不得在壳层重新维护 `MiniGameDraftGenerationState` 的 phase 阈值、`finishedAtMs` 清理或拼图进度 metadata 合并规则。
|
||||||
|
|
||||||
RPG Agent 结果页发布门禁展示由 `platformRpgAgentResultPreviewModel.ts` 判定:平台壳不得重新手写 `CustomWorldProfile` 顶层、`creatorIntent`、`anchorContent`、章节蓝图与首幕 acts 的结构探测,也不得在壳层内联 result preview source label 映射;壳层只负责 session/profile 编排和结果页 props 传递。
|
RPG Agent 结果页发布门禁展示由 `platformRpgAgentResultPreviewModel.ts` 判定:平台壳不得重新手写 `CustomWorldProfile` 顶层、`creatorIntent`、`anchorContent`、章节蓝图与首幕 acts 的结构探测,也不得在壳层内联 result preview source label 映射;壳层只负责 session/profile 编排和结果页 props 传递。
|
||||||
|
|
||||||
统一创作入口覆盖当前可进入创作链路的已有模板:`rpg`、`big-fish`、`puzzle`、`match3d`、`jump-hop`、`wooden-fish`、`square-hole`、`bark-battle`、`visual-novel`、`baby-object-match` 和 `creative-agent`;`airp` 仍是未开放占位,不作为当前统一创作链路目标。拼图、抓大鹅、跳一跳和敲木鱼在前端继续经过 `UnifiedCreationWorkspace` 和 `UnifiedGenerationPage`:`UnifiedCreationWorkspace` 作为平台壳依赖的统一创作编排层,再内部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace`、`Match3DCreationWorkspace`、`JumpHopCreationWorkspace` 和 `WoodenFishCreationWorkspace`。其它已有模板由平台壳用 `UnifiedCreationPage` 包住既有工作台,复用统一标题栏、返回入口、页面级纵向滚动和隐藏字段契约,同时保留各玩法自己的表单、草稿恢复和后续编排。创作页字段清单由后端在 `GET /api/creation-entry/config` 的 `creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec;字段类型只保留 `text`、`select`、`image`、`audio`。`UnifiedCreationPage` 不在 UI 中额外展示字段说明 chip,也不在右上角显示内部 `playId`、模板 ID 或工作台阶段名;竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交,但返回按钮只保留在统一页头,工作台内部不再重复渲染。暗色创作进度卡片位于 `platform-remap-surface` 内时,必须用组件专属 class 覆盖浅色主题 remap,确保白字、浅色边框和进度条底色不会被全局规则改成深色;不要只依赖通用 `text-white*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。
|
统一创作入口覆盖当前可进入创作链路的已有模板:`rpg`、`big-fish`、`puzzle`、`match3d`、`jump-hop`、`wooden-fish`、`square-hole`、`bark-battle`、`visual-novel`、`baby-object-match` 和 `creative-agent`;`airp` 仍是未开放占位,不作为当前统一创作链路目标。拼图、抓大鹅、跳一跳和敲木鱼在前端继续经过 `UnifiedCreationWorkspace` 和 `UnifiedGenerationPage`:`UnifiedCreationWorkspace` 作为平台壳依赖的统一创作编排层,再内部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace`、`Match3DCreationWorkspace`、`JumpHopCreationWorkspace` 和 `WoodenFishCreationWorkspace`。其它已有模板由平台壳用 `UnifiedCreationPage` 包住既有工作台,复用统一标题栏、返回入口、页面级纵向滚动和隐藏字段契约,同时保留各玩法自己的表单、草稿恢复和后续编排。创作页字段清单由后端在 `GET /api/creation-entry/config` 的 `creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec;字段类型只保留 `text`、`select`、`image`、`audio`。`UnifiedCreationPage` 不在 UI 中额外展示字段说明 chip,也不在右上角显示内部 `playId`、模板 ID 或工作台阶段名;竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交,但返回按钮只保留在统一页头,工作台内部不再重复渲染。暗色创作进度卡片位于 `platform-remap-surface` 内时,必须用组件专属 class 覆盖浅色主题 remap,确保白字、浅色边框和进度条底色不会被全局规则改成深色;不要只依赖通用 `text-white*` 类。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。
|
||||||
|
|||||||
@@ -213,8 +213,6 @@ import {
|
|||||||
buildSquareHoleGenerationAnchorEntries,
|
buildSquareHoleGenerationAnchorEntries,
|
||||||
buildWoodenFishGenerationAnchorEntries,
|
buildWoodenFishGenerationAnchorEntries,
|
||||||
createMiniGameDraftGenerationState,
|
createMiniGameDraftGenerationState,
|
||||||
type MiniGameDraftGenerationKind,
|
|
||||||
type MiniGameDraftGenerationPhase,
|
|
||||||
type MiniGameDraftGenerationState,
|
type MiniGameDraftGenerationState,
|
||||||
resolveMiniGameDraftGenerationStartedAtMs,
|
resolveMiniGameDraftGenerationStartedAtMs,
|
||||||
} from '../../services/miniGameDraftGenerationProgress';
|
} from '../../services/miniGameDraftGenerationProgress';
|
||||||
@@ -513,6 +511,17 @@ import {
|
|||||||
resolveMatch3DRuntimeGeneratedBackgroundAsset,
|
resolveMatch3DRuntimeGeneratedBackgroundAsset,
|
||||||
resolveMatch3DRuntimeGeneratedItemAssets,
|
resolveMatch3DRuntimeGeneratedItemAssets,
|
||||||
} from './platformMatch3DRuntimeProfile';
|
} from './platformMatch3DRuntimeProfile';
|
||||||
|
import {
|
||||||
|
createFailedMiniGameDraftGenerationStateForRestoredDraft,
|
||||||
|
createMiniGameDraftGenerationStateForRestoredDraft,
|
||||||
|
createPuzzleDraftGenerationStateFromPayload,
|
||||||
|
isMiniGameDraftGenerating,
|
||||||
|
isMiniGameDraftReady,
|
||||||
|
mergePuzzleSessionProgressIntoGenerationState,
|
||||||
|
rebaseMiniGameDraftBackgroundCompileTaskForDisplay,
|
||||||
|
rebaseMiniGameDraftGenerationStateForDisplay,
|
||||||
|
resolveFinishedMiniGameDraftGenerationState,
|
||||||
|
} from './platformMiniGameDraftGenerationStateModel';
|
||||||
import {
|
import {
|
||||||
buildJumpHopPendingSession,
|
buildJumpHopPendingSession,
|
||||||
buildPuzzleRuntimeWorkFromSession,
|
buildPuzzleRuntimeWorkFromSession,
|
||||||
@@ -1026,35 +1035,6 @@ function openPuzzleRuntimeStage(
|
|||||||
writePuzzleRuntimeUrlState(state);
|
writePuzzleRuntimeUrlState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 为恢复的小游戏草稿重建生成态,保留后端开始时间作为进度事实源。 */
|
|
||||||
function createMiniGameDraftGenerationStateForRestoredDraft(
|
|
||||||
kind: MiniGameDraftGenerationKind,
|
|
||||||
metadata?: MiniGameDraftGenerationState['metadata'],
|
|
||||||
startedAtMs = Date.now(),
|
|
||||||
): MiniGameDraftGenerationState {
|
|
||||||
return {
|
|
||||||
...createMiniGameDraftGenerationState(kind, startedAtMs),
|
|
||||||
...(metadata ? { metadata } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFailedMiniGameDraftGenerationStateForRestoredDraft(
|
|
||||||
kind: MiniGameDraftGenerationKind,
|
|
||||||
updatedAt: string | null | undefined,
|
|
||||||
error: string,
|
|
||||||
metadata?: MiniGameDraftGenerationState['metadata'],
|
|
||||||
): MiniGameDraftGenerationState {
|
|
||||||
return resolveFinishedMiniGameDraftGenerationState(
|
|
||||||
createMiniGameDraftGenerationStateForRestoredDraft(
|
|
||||||
kind,
|
|
||||||
metadata,
|
|
||||||
resolveMiniGameDraftGenerationStartedAtMs(updatedAt),
|
|
||||||
),
|
|
||||||
'failed',
|
|
||||||
{ error },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPuzzleFormPayloadFromWork(
|
function buildPuzzleFormPayloadFromWork(
|
||||||
item: PuzzleWorkSummary,
|
item: PuzzleWorkSummary,
|
||||||
): CreatePuzzleAgentSessionRequest {
|
): CreatePuzzleAgentSessionRequest {
|
||||||
@@ -1138,122 +1118,6 @@ function buildMatch3DFormPayloadFromWork(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 清理生成态完成时间,避免返回生成页后继续沿用结束态计时。 */
|
|
||||||
function rebaseMiniGameDraftGenerationStateForDisplay(
|
|
||||||
state: MiniGameDraftGenerationState,
|
|
||||||
): MiniGameDraftGenerationState {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
finishedAtMs: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function rebaseMiniGameDraftBackgroundCompileTaskForDisplay<
|
|
||||||
T extends PuzzleBackgroundCompileTask | Match3DBackgroundCompileTask,
|
|
||||||
>(task: T): T {
|
|
||||||
return {
|
|
||||||
...task,
|
|
||||||
generationState: rebaseMiniGameDraftGenerationStateForDisplay(
|
|
||||||
task.generationState,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPuzzleDraftGenerationStateFromPayload(
|
|
||||||
payload: CreatePuzzleAgentSessionRequest | null | undefined,
|
|
||||||
session: PuzzleAgentSessionSnapshot | null | undefined = null,
|
|
||||||
): MiniGameDraftGenerationState {
|
|
||||||
const puzzleProgressPercent =
|
|
||||||
session?.draft && !session.draft.formDraft
|
|
||||||
? session.progressPercent
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...createMiniGameDraftGenerationState(
|
|
||||||
'puzzle',
|
|
||||||
resolveMiniGameDraftGenerationStartedAtMs(session?.updatedAt),
|
|
||||||
),
|
|
||||||
metadata: {
|
|
||||||
puzzleAiRedraw: payload?.aiRedraw ?? true,
|
|
||||||
puzzleActivePhaseId:
|
|
||||||
typeof puzzleProgressPercent === 'number' ? 'compile' : undefined,
|
|
||||||
puzzleActiveStepStartedAtMs:
|
|
||||||
typeof puzzleProgressPercent === 'number' ? Date.now() : undefined,
|
|
||||||
puzzleProgressPercent,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePuzzlePhaseFromSessionProgress(
|
|
||||||
state: MiniGameDraftGenerationState,
|
|
||||||
session: PuzzleAgentSessionSnapshot,
|
|
||||||
): MiniGameDraftGenerationPhase {
|
|
||||||
if (session.progressPercent >= 96) {
|
|
||||||
return 'puzzle-select-image';
|
|
||||||
}
|
|
||||||
if (session.progressPercent >= 94) {
|
|
||||||
return 'puzzle-ui-assets';
|
|
||||||
}
|
|
||||||
if (session.progressPercent >= 88) {
|
|
||||||
return state.metadata?.puzzleAiRedraw === false
|
|
||||||
? 'puzzle-level-scene'
|
|
||||||
: 'puzzle-cover-image';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'compile';
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergePuzzleSessionProgressIntoGenerationState(
|
|
||||||
state: MiniGameDraftGenerationState,
|
|
||||||
session: PuzzleAgentSessionSnapshot,
|
|
||||||
): MiniGameDraftGenerationState {
|
|
||||||
const isCompiledGenerationSession = Boolean(
|
|
||||||
session.draft && !session.draft.formDraft,
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextPhaseId = isCompiledGenerationSession
|
|
||||||
? resolvePuzzlePhaseFromSessionProgress(state, session)
|
|
||||||
: state.metadata?.puzzleActivePhaseId;
|
|
||||||
const shouldResetActiveStepStart =
|
|
||||||
isCompiledGenerationSession &&
|
|
||||||
nextPhaseId != null &&
|
|
||||||
nextPhaseId !== state.metadata?.puzzleActivePhaseId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
metadata: {
|
|
||||||
...state.metadata,
|
|
||||||
puzzleActivePhaseId: nextPhaseId,
|
|
||||||
puzzleActiveStepStartedAtMs: shouldResetActiveStepStart
|
|
||||||
? resolveMiniGameDraftGenerationStartedAtMs(session.updatedAt)
|
|
||||||
: state.metadata?.puzzleActiveStepStartedAtMs,
|
|
||||||
puzzleProgressPercent: isCompiledGenerationSession
|
|
||||||
? session.progressPercent
|
|
||||||
: state.metadata?.puzzleProgressPercent,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFinishedMiniGameDraftGenerationState(
|
|
||||||
state: MiniGameDraftGenerationState,
|
|
||||||
phase: 'ready' | 'failed',
|
|
||||||
options: {
|
|
||||||
error?: string | null;
|
|
||||||
completedAssetCount?: number;
|
|
||||||
totalAssetCount?: number;
|
|
||||||
} = {},
|
|
||||||
): MiniGameDraftGenerationState {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
phase,
|
|
||||||
finishedAtMs: Date.now(),
|
|
||||||
error: options.error ?? state.error,
|
|
||||||
completedAssetCount:
|
|
||||||
options.completedAssetCount ?? state.completedAssetCount,
|
|
||||||
totalAssetCount: options.totalAssetCount ?? state.totalAssetCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRecoveredPuzzleDraftSession(
|
function normalizeRecoveredPuzzleDraftSession(
|
||||||
session: PuzzleAgentSessionSnapshot,
|
session: PuzzleAgentSessionSnapshot,
|
||||||
): PuzzleAgentSessionSnapshot {
|
): PuzzleAgentSessionSnapshot {
|
||||||
@@ -1325,14 +1189,6 @@ function hasRecoverableGeneratedPuzzleDraft(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMiniGameDraftReady(state: MiniGameDraftGenerationState | null) {
|
|
||||||
return state?.phase === 'ready';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMiniGameDraftGenerating(state: MiniGameDraftGenerationState | null) {
|
|
||||||
return Boolean(state && state.phase !== 'ready' && state.phase !== 'failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveProfileWalletBalance(
|
function resolveProfileWalletBalance(
|
||||||
dashboard: { walletBalance?: number | null } | null | undefined,
|
dashboard: { walletBalance?: number | null } | null | undefined,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { PuzzleAnchorPack } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||||||
|
import type {
|
||||||
|
CreatePuzzleAgentSessionRequest,
|
||||||
|
PuzzleAgentSessionSnapshot,
|
||||||
|
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||||
|
import type { MiniGameDraftGenerationState } from '../../services/miniGameDraftGenerationProgress';
|
||||||
|
import {
|
||||||
|
createFailedMiniGameDraftGenerationStateForRestoredDraft,
|
||||||
|
createMiniGameDraftGenerationStateForRestoredDraft,
|
||||||
|
createPuzzleDraftGenerationStateFromPayload,
|
||||||
|
isMiniGameDraftGenerating,
|
||||||
|
isMiniGameDraftReady,
|
||||||
|
mergePuzzleSessionProgressIntoGenerationState,
|
||||||
|
rebaseMiniGameDraftBackgroundCompileTaskForDisplay,
|
||||||
|
rebaseMiniGameDraftGenerationStateForDisplay,
|
||||||
|
resolveFinishedMiniGameDraftGenerationState,
|
||||||
|
resolvePuzzlePhaseFromSessionProgress,
|
||||||
|
} from './platformMiniGameDraftGenerationStateModel';
|
||||||
|
|
||||||
|
const NOW = Date.parse('2026-06-04T03:00:00.000Z');
|
||||||
|
const SESSION_UPDATED_AT = '2026-06-01T10:00:00.000Z';
|
||||||
|
const SESSION_UPDATED_AT_MS = Date.parse(SESSION_UPDATED_AT);
|
||||||
|
|
||||||
|
function buildAnchorPack(): PuzzleAnchorPack {
|
||||||
|
const item = {
|
||||||
|
key: 'theme',
|
||||||
|
label: '主题',
|
||||||
|
value: '星桥机关',
|
||||||
|
status: 'confirmed' as const,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
themePromise: item,
|
||||||
|
visualSubject: item,
|
||||||
|
visualMood: item,
|
||||||
|
compositionHooks: item,
|
||||||
|
tagsAndForbidden: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPuzzleSession(
|
||||||
|
overrides: Partial<PuzzleAgentSessionSnapshot> = {},
|
||||||
|
): PuzzleAgentSessionSnapshot {
|
||||||
|
const anchorPack = buildAnchorPack();
|
||||||
|
return {
|
||||||
|
sessionId: 'puzzle-session-1',
|
||||||
|
seedText: '星桥',
|
||||||
|
currentTurn: 1,
|
||||||
|
progressPercent: 90,
|
||||||
|
stage: 'draft_ready',
|
||||||
|
anchorPack,
|
||||||
|
draft: {
|
||||||
|
workTitle: '星桥拼图',
|
||||||
|
workDescription: '修复星桥机关。',
|
||||||
|
levelName: '星桥机关',
|
||||||
|
summary: '把星桥碎片拼回原位。',
|
||||||
|
themeTags: ['星桥'],
|
||||||
|
forbiddenDirectives: [],
|
||||||
|
creatorIntent: null,
|
||||||
|
anchorPack,
|
||||||
|
candidates: [],
|
||||||
|
selectedCandidateId: null,
|
||||||
|
coverImageSrc: null,
|
||||||
|
coverAssetId: null,
|
||||||
|
generationStatus: 'generating',
|
||||||
|
levels: [],
|
||||||
|
},
|
||||||
|
messages: [],
|
||||||
|
lastAssistantReply: null,
|
||||||
|
publishedProfileId: null,
|
||||||
|
suggestedActions: [],
|
||||||
|
resultPreview: null,
|
||||||
|
updatedAt: SESSION_UPDATED_AT,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildState(
|
||||||
|
overrides: Partial<MiniGameDraftGenerationState> = {},
|
||||||
|
): MiniGameDraftGenerationState {
|
||||||
|
return {
|
||||||
|
kind: 'puzzle',
|
||||||
|
phase: 'compile',
|
||||||
|
startedAtMs: 100,
|
||||||
|
completedAssetCount: 0,
|
||||||
|
totalAssetCount: 0,
|
||||||
|
error: null,
|
||||||
|
metadata: {
|
||||||
|
puzzleAiRedraw: true,
|
||||||
|
puzzleActivePhaseId: 'compile',
|
||||||
|
puzzleActiveStepStartedAtMs: 200,
|
||||||
|
puzzleProgressPercent: 20,
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(NOW);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('platformMiniGameDraftGenerationStateModel', () => {
|
||||||
|
test('creates restored generation state with metadata and explicit start time', () => {
|
||||||
|
expect(
|
||||||
|
createMiniGameDraftGenerationStateForRestoredDraft(
|
||||||
|
'match3d',
|
||||||
|
{ puzzleAiRedraw: false },
|
||||||
|
123,
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
kind: 'match3d',
|
||||||
|
phase: 'match3d-work-title',
|
||||||
|
startedAtMs: 123,
|
||||||
|
metadata: {
|
||||||
|
puzzleAiRedraw: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates failed restored state from backend updated time', () => {
|
||||||
|
expect(
|
||||||
|
createFailedMiniGameDraftGenerationStateForRestoredDraft(
|
||||||
|
'puzzle',
|
||||||
|
SESSION_UPDATED_AT,
|
||||||
|
'生成失败',
|
||||||
|
{ puzzleAiRedraw: true },
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
kind: 'puzzle',
|
||||||
|
phase: 'failed',
|
||||||
|
startedAtMs: SESSION_UPDATED_AT_MS,
|
||||||
|
finishedAtMs: NOW,
|
||||||
|
error: '生成失败',
|
||||||
|
metadata: {
|
||||||
|
puzzleAiRedraw: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rebases finished state for display without changing other fields', () => {
|
||||||
|
const state = buildState({
|
||||||
|
phase: 'ready',
|
||||||
|
finishedAtMs: 300,
|
||||||
|
completedAssetCount: 2,
|
||||||
|
totalAssetCount: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rebaseMiniGameDraftGenerationStateForDisplay(state)).toEqual({
|
||||||
|
...state,
|
||||||
|
finishedAtMs: undefined,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
rebaseMiniGameDraftBackgroundCompileTaskForDisplay({
|
||||||
|
sessionId: 'task-1',
|
||||||
|
generationState: state,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
sessionId: 'task-1',
|
||||||
|
generationState: {
|
||||||
|
...state,
|
||||||
|
finishedAtMs: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates puzzle generation state from payload and compiled session', () => {
|
||||||
|
const payload: CreatePuzzleAgentSessionRequest = {
|
||||||
|
seedText: '星桥',
|
||||||
|
aiRedraw: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createPuzzleDraftGenerationStateFromPayload(payload)).toMatchObject({
|
||||||
|
kind: 'puzzle',
|
||||||
|
phase: 'compile',
|
||||||
|
startedAtMs: NOW,
|
||||||
|
metadata: {
|
||||||
|
puzzleAiRedraw: false,
|
||||||
|
puzzleActivePhaseId: undefined,
|
||||||
|
puzzleActiveStepStartedAtMs: undefined,
|
||||||
|
puzzleProgressPercent: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
createPuzzleDraftGenerationStateFromPayload(payload, buildPuzzleSession()),
|
||||||
|
).toMatchObject({
|
||||||
|
kind: 'puzzle',
|
||||||
|
phase: 'compile',
|
||||||
|
startedAtMs: SESSION_UPDATED_AT_MS,
|
||||||
|
metadata: {
|
||||||
|
puzzleAiRedraw: false,
|
||||||
|
puzzleActivePhaseId: 'compile',
|
||||||
|
puzzleActiveStepStartedAtMs: NOW,
|
||||||
|
puzzleProgressPercent: 90,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves puzzle phase from backend progress thresholds', () => {
|
||||||
|
const state = buildState();
|
||||||
|
expect(
|
||||||
|
resolvePuzzlePhaseFromSessionProgress(
|
||||||
|
state,
|
||||||
|
buildPuzzleSession({ progressPercent: 96 }),
|
||||||
|
),
|
||||||
|
).toBe('puzzle-select-image');
|
||||||
|
expect(
|
||||||
|
resolvePuzzlePhaseFromSessionProgress(
|
||||||
|
state,
|
||||||
|
buildPuzzleSession({ progressPercent: 94 }),
|
||||||
|
),
|
||||||
|
).toBe('puzzle-ui-assets');
|
||||||
|
expect(
|
||||||
|
resolvePuzzlePhaseFromSessionProgress(
|
||||||
|
buildState({ metadata: { puzzleAiRedraw: false } }),
|
||||||
|
buildPuzzleSession({ progressPercent: 88 }),
|
||||||
|
),
|
||||||
|
).toBe('puzzle-level-scene');
|
||||||
|
expect(
|
||||||
|
resolvePuzzlePhaseFromSessionProgress(
|
||||||
|
state,
|
||||||
|
buildPuzzleSession({ progressPercent: 88 }),
|
||||||
|
),
|
||||||
|
).toBe('puzzle-cover-image');
|
||||||
|
expect(
|
||||||
|
resolvePuzzlePhaseFromSessionProgress(
|
||||||
|
state,
|
||||||
|
buildPuzzleSession({ progressPercent: 20 }),
|
||||||
|
),
|
||||||
|
).toBe('compile');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('merges compiled puzzle session progress into generation state', () => {
|
||||||
|
expect(
|
||||||
|
mergePuzzleSessionProgressIntoGenerationState(
|
||||||
|
buildState({
|
||||||
|
metadata: {
|
||||||
|
puzzleAiRedraw: false,
|
||||||
|
puzzleActivePhaseId: 'compile',
|
||||||
|
puzzleActiveStepStartedAtMs: 200,
|
||||||
|
puzzleProgressPercent: 20,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
buildPuzzleSession({ progressPercent: 90 }),
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
metadata: {
|
||||||
|
puzzleAiRedraw: false,
|
||||||
|
puzzleActivePhaseId: 'puzzle-level-scene',
|
||||||
|
puzzleActiveStepStartedAtMs: SESSION_UPDATED_AT_MS,
|
||||||
|
puzzleProgressPercent: 90,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
mergePuzzleSessionProgressIntoGenerationState(
|
||||||
|
buildState(),
|
||||||
|
buildPuzzleSession({
|
||||||
|
draft: {
|
||||||
|
...buildPuzzleSession().draft!,
|
||||||
|
formDraft: {
|
||||||
|
pictureDescription: '星桥',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).metadata,
|
||||||
|
).toMatchObject({
|
||||||
|
puzzleActivePhaseId: 'compile',
|
||||||
|
puzzleActiveStepStartedAtMs: 200,
|
||||||
|
puzzleProgressPercent: 20,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('finishes generation state and resolves ready/generating flags', () => {
|
||||||
|
const failedState = resolveFinishedMiniGameDraftGenerationState(
|
||||||
|
buildState({ error: '旧错误' }),
|
||||||
|
'failed',
|
||||||
|
{
|
||||||
|
completedAssetCount: 1,
|
||||||
|
totalAssetCount: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(failedState).toMatchObject({
|
||||||
|
phase: 'failed',
|
||||||
|
finishedAtMs: NOW,
|
||||||
|
error: '旧错误',
|
||||||
|
completedAssetCount: 1,
|
||||||
|
totalAssetCount: 2,
|
||||||
|
});
|
||||||
|
expect(isMiniGameDraftReady(failedState)).toBe(false);
|
||||||
|
expect(isMiniGameDraftGenerating(failedState)).toBe(false);
|
||||||
|
expect(isMiniGameDraftReady({ ...failedState, phase: 'ready' })).toBe(true);
|
||||||
|
expect(isMiniGameDraftGenerating(buildState())).toBe(true);
|
||||||
|
expect(isMiniGameDraftGenerating(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import type {
|
||||||
|
CreatePuzzleAgentSessionRequest,
|
||||||
|
PuzzleAgentSessionSnapshot,
|
||||||
|
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||||
|
import {
|
||||||
|
createMiniGameDraftGenerationState,
|
||||||
|
type MiniGameDraftGenerationKind,
|
||||||
|
type MiniGameDraftGenerationPhase,
|
||||||
|
type MiniGameDraftGenerationState,
|
||||||
|
resolveMiniGameDraftGenerationStartedAtMs,
|
||||||
|
} from '../../services/miniGameDraftGenerationProgress';
|
||||||
|
|
||||||
|
export function createMiniGameDraftGenerationStateForRestoredDraft(
|
||||||
|
kind: MiniGameDraftGenerationKind,
|
||||||
|
metadata?: MiniGameDraftGenerationState['metadata'],
|
||||||
|
startedAtMs = Date.now(),
|
||||||
|
): MiniGameDraftGenerationState {
|
||||||
|
return {
|
||||||
|
...createMiniGameDraftGenerationState(kind, startedAtMs),
|
||||||
|
...(metadata ? { metadata } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFailedMiniGameDraftGenerationStateForRestoredDraft(
|
||||||
|
kind: MiniGameDraftGenerationKind,
|
||||||
|
updatedAt: string | null | undefined,
|
||||||
|
error: string,
|
||||||
|
metadata?: MiniGameDraftGenerationState['metadata'],
|
||||||
|
): MiniGameDraftGenerationState {
|
||||||
|
return resolveFinishedMiniGameDraftGenerationState(
|
||||||
|
createMiniGameDraftGenerationStateForRestoredDraft(
|
||||||
|
kind,
|
||||||
|
metadata,
|
||||||
|
resolveMiniGameDraftGenerationStartedAtMs(updatedAt),
|
||||||
|
),
|
||||||
|
'failed',
|
||||||
|
{ error },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清理生成态完成时间,避免返回生成页后继续沿用结束态计时。 */
|
||||||
|
export function rebaseMiniGameDraftGenerationStateForDisplay(
|
||||||
|
state: MiniGameDraftGenerationState,
|
||||||
|
): MiniGameDraftGenerationState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
finishedAtMs: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rebaseMiniGameDraftBackgroundCompileTaskForDisplay<
|
||||||
|
T extends { generationState: MiniGameDraftGenerationState },
|
||||||
|
>(task: T): T {
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
generationState: rebaseMiniGameDraftGenerationStateForDisplay(
|
||||||
|
task.generationState,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPuzzleDraftGenerationStateFromPayload(
|
||||||
|
payload: CreatePuzzleAgentSessionRequest | null | undefined,
|
||||||
|
session: PuzzleAgentSessionSnapshot | null | undefined = null,
|
||||||
|
): MiniGameDraftGenerationState {
|
||||||
|
const puzzleProgressPercent =
|
||||||
|
session?.draft && !session.draft.formDraft
|
||||||
|
? session.progressPercent
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...createMiniGameDraftGenerationState(
|
||||||
|
'puzzle',
|
||||||
|
resolveMiniGameDraftGenerationStartedAtMs(session?.updatedAt),
|
||||||
|
),
|
||||||
|
metadata: {
|
||||||
|
puzzleAiRedraw: payload?.aiRedraw ?? true,
|
||||||
|
puzzleActivePhaseId:
|
||||||
|
typeof puzzleProgressPercent === 'number' ? 'compile' : undefined,
|
||||||
|
puzzleActiveStepStartedAtMs:
|
||||||
|
typeof puzzleProgressPercent === 'number' ? Date.now() : undefined,
|
||||||
|
puzzleProgressPercent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePuzzlePhaseFromSessionProgress(
|
||||||
|
state: MiniGameDraftGenerationState,
|
||||||
|
session: PuzzleAgentSessionSnapshot,
|
||||||
|
): MiniGameDraftGenerationPhase {
|
||||||
|
if (session.progressPercent >= 96) {
|
||||||
|
return 'puzzle-select-image';
|
||||||
|
}
|
||||||
|
if (session.progressPercent >= 94) {
|
||||||
|
return 'puzzle-ui-assets';
|
||||||
|
}
|
||||||
|
if (session.progressPercent >= 88) {
|
||||||
|
return state.metadata?.puzzleAiRedraw === false
|
||||||
|
? 'puzzle-level-scene'
|
||||||
|
: 'puzzle-cover-image';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'compile';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergePuzzleSessionProgressIntoGenerationState(
|
||||||
|
state: MiniGameDraftGenerationState,
|
||||||
|
session: PuzzleAgentSessionSnapshot,
|
||||||
|
): MiniGameDraftGenerationState {
|
||||||
|
const isCompiledGenerationSession = Boolean(
|
||||||
|
session.draft && !session.draft.formDraft,
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextPhaseId = isCompiledGenerationSession
|
||||||
|
? resolvePuzzlePhaseFromSessionProgress(state, session)
|
||||||
|
: state.metadata?.puzzleActivePhaseId;
|
||||||
|
const shouldResetActiveStepStart =
|
||||||
|
isCompiledGenerationSession &&
|
||||||
|
nextPhaseId != null &&
|
||||||
|
nextPhaseId !== state.metadata?.puzzleActivePhaseId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadata: {
|
||||||
|
...state.metadata,
|
||||||
|
puzzleActivePhaseId: nextPhaseId,
|
||||||
|
puzzleActiveStepStartedAtMs: shouldResetActiveStepStart
|
||||||
|
? resolveMiniGameDraftGenerationStartedAtMs(session.updatedAt)
|
||||||
|
: state.metadata?.puzzleActiveStepStartedAtMs,
|
||||||
|
puzzleProgressPercent: isCompiledGenerationSession
|
||||||
|
? session.progressPercent
|
||||||
|
: state.metadata?.puzzleProgressPercent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFinishedMiniGameDraftGenerationState(
|
||||||
|
state: MiniGameDraftGenerationState,
|
||||||
|
phase: 'ready' | 'failed',
|
||||||
|
options: {
|
||||||
|
error?: string | null;
|
||||||
|
completedAssetCount?: number;
|
||||||
|
totalAssetCount?: number;
|
||||||
|
} = {},
|
||||||
|
): MiniGameDraftGenerationState {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
phase,
|
||||||
|
finishedAtMs: Date.now(),
|
||||||
|
error: options.error ?? state.error,
|
||||||
|
completedAssetCount:
|
||||||
|
options.completedAssetCount ?? state.completedAssetCount,
|
||||||
|
totalAssetCount: options.totalAssetCount ?? state.totalAssetCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMiniGameDraftReady(
|
||||||
|
state: MiniGameDraftGenerationState | null,
|
||||||
|
) {
|
||||||
|
return state?.phase === 'ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMiniGameDraftGenerating(
|
||||||
|
state: MiniGameDraftGenerationState | null,
|
||||||
|
) {
|
||||||
|
return Boolean(state && state.phase !== 'ready' && state.phase !== 'failed');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user