diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 4ca4e204..d5207f03 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -276,7 +276,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列 - 创作 Tab 表单:填写作品标题、简介、主题 / 竞技背景描述、玩家形象描述、对手形象描述、拟声词和难度。拟声词支持换行、逗号、顿号、斜杠或竖线分隔;未手动编辑时随主题 / 形象描述自动重算,手动编辑后保持创作者自定义。 - 草稿编译:`POST /api/creation/bark-battle/drafts` 写入配置 JSON,返回包含 `draftId`、稳定 `workId`、`configVersion` 和 `rulesetVersion` 的草稿结果。 - 生成页:`bark-battle-generating` 自动并行产出玩家形象、对手形象和竞技背景三图;前端生成页 UI 和其它玩法保持同一圆环主视觉,`media/create_bg_video.mp4` 作为固定全屏页面背景层循环静音播放,主进度圆环居中展示总进度,只保留当前步骤名称和当前步骤进度,不再渲染三行槽位列表。视频层需要显式触发播放。三图都走 Bark Battle 专用后端生图接口 `POST /api/creation/bark-battle/images/generate`,由后端按 `player-character`、`opponent-character`、`ui-background` 分别拼装正式提示词、写入 `generated-bark-battle-assets` 私有资产前缀并返回实际 prompt。玩家 / 对手形象提示词必须保持用户形象描述,不强行注入狗相关主体,并要求正面、单个完整形象和透明背景。部分失败也继续进入结果页。 -- 结果页:围绕三图槽位展示错误态与已生成结果,只保留单槽重试、重新生成和上传,不再提供一次生成按钮、音频配置入口或排名配置。 +- 结果页:围绕三图槽位展示错误态与已生成结果,只保留单槽重试、重新生成和上传,不再提供一次生成按钮、音频配置入口或排名配置;生成回写 `partial_failed` 时作品架不再显示整卡“生成中”遮罩,由结果页槽位错误承接失败。 - 手动上传:结果页通过平台资产直传 `/api/assets/direct-upload-tickets` 与 `/api/assets/objects/confirm` 写入私有资产,再把返回的历史 generated 路径写回草稿配置。 - 发布:结果页确认后必须携带草稿返回的同一个 `workId` 和结果页最终 `publishedSnapshot` 调用 `POST /api/creation/bark-battle/works/publish`;SpacetimeDB 发布态的 `config_json` 必须使用该最终快照,works summary 若拿到 `publishedSnapshotJson` 也优先使用最终快照映射封面三图。发布成功后先进入统一作品详情页,再由详情页进入正式 runtime;缺少 `workId` 的旧草稿状态需要重新生成草稿。 - 作品架:Bark Battle 草稿 / 已发布列表优先读取后端 `/works`,但创建、生成完成、保存或发布后的本地摘要必须在后端 read model 尚未回读到同 `workId` 前继续保留;创作中心作品架同时接入 pending shelf 兜底,避免 ready 且三图齐全的草稿在刷新窗口期从“我的草稿 / 已发布”中消失。 diff --git a/src/components/custom-world-home/creationWorkShelf.test.ts b/src/components/custom-world-home/creationWorkShelf.test.ts index 6388732a..180e1e7a 100644 --- a/src/components/custom-world-home/creationWorkShelf.test.ts +++ b/src/components/custom-world-home/creationWorkShelf.test.ts @@ -1048,7 +1048,7 @@ test('buildCreationWorkShelfItems maps bark battle works with scene role cover a ); }); -test('bark battle draft generating state follows pending assets or missing three images', () => { +test('bark battle draft generating state only follows pending assets', () => { const draft = { workId: 'bark-battle-work-draft', draftId: 'bark-battle-draft-1', @@ -1073,6 +1073,12 @@ test('bark battle draft generating state follows pending assets or missing three expect(hasBarkBattleRequiredImages(draft)).toBe(false); expect(isPersistedBarkBattleDraftGenerating(draft)).toBe(true); + expect( + isPersistedBarkBattleDraftGenerating({ + ...draft, + generationStatus: 'partial_failed', + }), + ).toBe(false); expect( isPersistedBarkBattleDraftGenerating({ ...draft, diff --git a/src/components/custom-world-home/creationWorkShelf.ts b/src/components/custom-world-home/creationWorkShelf.ts index e44022a2..81300a53 100644 --- a/src/components/custom-world-home/creationWorkShelf.ts +++ b/src/components/custom-world-home/creationWorkShelf.ts @@ -1111,10 +1111,9 @@ export function isPersistedBarkBattleDraftGenerating( return false; } - return ( - item.generationStatus === 'pending_assets' || - !hasBarkBattleRequiredImages(item) - ); + // 中文注释:汪汪声浪生成失败后会回写 partial_failed 并进入结果页承接错误槽位, + // 不能因为三图未齐就继续把作品架整卡锁成“生成中”。 + return item.generationStatus === 'pending_assets'; } export function hasBarkBattleRequiredImages(item: BarkBattleWorkSummary) { diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 301342ea..a9ebe6e4 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -2094,6 +2094,8 @@ function buildDraftCompletionDialogSource( return formatPlatformTaskCompletionSource('方洞挑战草稿', sourceId); case 'jump-hop': return formatPlatformTaskCompletionSource('跳一跳草稿', sourceId); + case 'wooden-fish': + return formatPlatformTaskCompletionSource('敲木鱼草稿', sourceId); case 'puzzle': return formatPlatformTaskCompletionSource('拼图草稿', sourceId); case 'visual-novel': @@ -8887,6 +8889,8 @@ export function PlatformEntryFlowShellImpl({ setWoodenFishGenerationState(generationState); setIsWoodenFishBusy(true); setSelectionStage('wooden-fish-generating'); + markDraftGenerating('wooden-fish', [created.session.sessionId]); + markPendingDraftGenerating('wooden-fish', created.session.sessionId); try { const response = await woodenFishClient.executeAction( @@ -8921,6 +8925,30 @@ export function PlatformEntryFlowShellImpl({ setWoodenFishGenerationState( createReadyWoodenFishGenerationState(generationState), ); + if (response.work) { + setWoodenFishWorks((current) => [ + response.work!.summary, + ...current.filter( + (item) => item.workId !== response.work!.summary.workId, + ), + ]); + markPendingDraftReady( + 'wooden-fish', + created.session.sessionId, + false, + ); + markDraftReady( + 'wooden-fish', + [ + created.session.sessionId, + response.work.summary.workId, + response.work.summary.profileId, + response.work.summary.sourceSessionId, + ], + false, + ); + void refreshWoodenFishShelf().catch(() => undefined); + } setSelectionStage('wooden-fish-result'); } catch (error) { const errorMessage = resolveRpgCreationErrorMessage( @@ -8955,7 +8983,15 @@ export function PlatformEntryFlowShellImpl({ setIsWoodenFishBusy(false); } }, - [createReadyWoodenFishGenerationState, setSelectionStage], + [ + createReadyWoodenFishGenerationState, + markDraftGenerating, + markDraftReady, + markPendingDraftGenerating, + markPendingDraftReady, + refreshWoodenFishShelf, + setSelectionStage, + ], ); const retryWoodenFishDraftGeneration = useCallback(() => {