From 8d3e14020f51fd3ab0370f8c461b61986265c270 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 4 Jun 2026 01:11:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E8=BF=90=E8=A1=8C=E6=80=81=E5=90=AF=E5=8A=A8=E6=84=8F?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 2 +- docs/README.md | 2 +- ...入口PublicGalleryFlowModule收口计划-2026-06-03.md | 14 +- .../PlatformEntryFlowShellImpl.tsx | 184 ++++++------ .../platformPublicGalleryFlow.test.ts | 279 ++++++++++++++++++ .../platformPublicGalleryFlow.ts | 232 +++++++++++++++ 6 files changed, 612 insertions(+), 101 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 4f7e0899..bab8893e 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -49,7 +49,7 @@ ## 2026-06-03 平台入口公开作品流身份规则收口 - 背景:平台入口公开作品推荐流需要同时处理 RPG、拼图、抓大鹅、跳一跳、敲木鱼、视觉小说、Bark Battle、宝贝识物等卡片,公开作品身份、跨玩法去重、排序和推荐运行态 kind 判定曾放在 `PlatformEntryFlowShellImpl.tsx` 巨型实现里。 -- 决策:公开作品身份和排序规则统一收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`;入口壳层只调用该 Module 的 `getPlatformPublicGalleryEntryKey`、`getPlatformRecommendRuntimeKind`、`isSamePlatformPublicGalleryEntry` 和 `mergePlatformPublicGalleryEntries`。`edutainment` key 必须带 `templateId`,RPG 卡片回退为 `rpg`。 +- 决策:公开作品身份、排序规则和推荐 runtime 启动意图统一收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`;入口壳层只调用该 Module 的 `getPlatformPublicGalleryEntryKey`、`getPlatformRecommendRuntimeKind`、`resolvePlatformRecommendRuntimeStartIntent`、`isSamePlatformPublicGalleryEntry` 和 `mergePlatformPublicGalleryEntries`。`edutainment` key 必须带 `templateId`,RPG 卡片回退为 `rpg`。推荐 runtime 启动 intent 只返回启动目标、`embedded` / `returnStage` 参数、阻断文案和错误落点;壳层仍执行 request key、运行态 API、错误 setter 与 UI 状态。 - 影响范围:平台入口推荐流、公开作品详情、推荐 runtime 启动、跨玩法公开作品合并,以及后续新增玩法的入口接入。 - 验证方式:`npm run test -- src/components/platform-entry/platformPublicGalleryFlow.test.ts`、`npm run typecheck`、`npm run check:encoding`、相关文件 ESLint 通过。 - 关联文档:`docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md`。 diff --git a/docs/README.md b/docs/README.md index abdad1fa..4dd4b85b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,7 +39,7 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 前端 Server-Sent Events 客户端传输层收口到 `src/services/sseStream.ts`,事件边界、UTF-8 flush、JSON 解析跳过和提前取消约定见 [【前端架构】SSE客户端传输层收口约定-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91SSE%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BC%A0%E8%BE%93%E5%B1%82%E6%94%B6%E5%8F%A3%E7%BA%A6%E5%AE%9A-2026-06-03.md)。 -平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 +平台入口公开作品身份、跨玩法去重、推荐运行态 kind 判定、推荐 runtime 启动意图和最新排序收口到 `src/components/platform-entry/platformPublicGalleryFlow.ts`,规则见 [【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%B9%B3%E5%8F%B0%E5%85%A5%E5%8F%A3PublicGalleryFlowModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 统一作品详情页的玩法 kind、详情打开策略、自有作品动作模式、编辑 / 点赞 / 改造 / 启动意图和公开详情映射收口到 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`;抓大鹅公开详情映射与启动 / 编辑 Adapter 的素材归一仍归 `platformMatch3DRuntimeProfile.ts`,规则见 [【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md](./technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md b/docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md index 5cb2ca6c..6b09dd89 100644 --- a/docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md +++ b/docs/technical/【前端架构】平台入口PublicGalleryFlowModule收口计划-2026-06-03.md @@ -10,10 +10,11 @@ - `getPlatformPublicGalleryEntryKey(entry)`:按玩法类型、作者和 `profileId` 生成公开作品身份。 - `getPlatformRecommendRuntimeKind(entry)`:把公开作品卡映射为推荐运行态 kind。 +- `resolvePlatformRecommendRuntimeStartIntent(entry, deps)`:把公开作品卡映射为推荐 runtime 启动意图、错误落点和 embedded / returnStage 参数。 - `isSamePlatformPublicGalleryEntry(left, right)`:按公开作品身份比较。 - `mergePlatformPublicGalleryEntries(rpgEntries, puzzleEntries)`:统一完成 RPG 与各玩法公开作品去重、覆盖和倒序排序。 -入口壳层只调用这些函数,不再在 `PlatformEntryFlowShellImpl.tsx` 内手写公开作品身份和排序规则。`isRecommendRuntimeReadyForEntry` 暂留入口壳层,因为它依赖各运行态 run 状态,直接抽出会把更多 runtime 状态类型拖入这个 Module,降低本次改造的 locality。 +入口壳层只调用这些函数,不再在 `PlatformEntryFlowShellImpl.tsx` 内手写公开作品身份、排序规则和推荐 runtime 启动能力矩阵。`isRecommendRuntimeReadyForEntry` 暂留入口壳层,因为它依赖各运行态 run 状态,直接抽出会把更多 runtime 状态类型拖入这个 Module,降低本次改造的 locality。 ## 玩法身份规则 @@ -23,9 +24,18 @@ - 最终 key 格式为 `${kind}:${ownerUserId}:${profileId}`。 - 合并时后进入的相同 key 会覆盖先进入的卡片,然后按 `publishedAt ?? updatedAt` 新到旧排序;非法时间按 `0` 处理。 +## 推荐 runtime 启动意图 + +- `resolvePlatformRecommendRuntimeStartIntent` 只表达推荐 runtime 的启动目标,不执行鉴权、运行态 API、错误 setter、缓存、request key 或 UI 状态更新。 +- 大鱼吃小鱼、拼图、跳一跳、敲木鱼、抓大鹅、方洞挑战、视觉小说、汪汪声浪和宝贝识物返回对应启动 intent;RPG 维持当前无嵌入 runtime 的 `mark-ready` 行为。 +- 大鱼吃小鱼、拼图、抓大鹅、方洞挑战和汪汪声浪在公开卡无法拼出启动 work 时返回 `blocked`,同时给出 `errorTarget`,由壳层 Adapter 分发到对应玩法错误 setter。 +- 拼图优先使用同 `profileId` 的 `selectedPuzzleDetail`,否则从公开卡映射兼容 work 摘要。 +- 抓大鹅 public detail -> work mapper 必须作为 Adapter 注入,继续由 Match3D Runtime Profile Module 维护 `generatedItemAssets` 归一化与背景资产提升。推荐 runtime 固定沿用旧参数 `returnStage = 'work-detail'` 与 `embedded = true`。 +- 汪汪声浪优先使用推荐流已持有的 `barkBattleGalleryEntries`,再回退公开卡映射;不额外读取作品架列表。 + ## 后续深化 -下一步可继续把平台入口的作品架刷新、删除确认和直达恢复逻辑收口成更深的 Work Shelf **Module**。当前 `platformPublicGalleryFlow` 先提供一个稳定 seam,使公开作品 identity 与 runtime kind 的修改集中在一处。 +下一步可继续把平台入口的作品架刷新、删除确认和直达恢复逻辑收口成更深的 Work Shelf **Module**。当前 `platformPublicGalleryFlow` 先提供一个稳定 seam,使公开作品 identity、runtime kind 与推荐 runtime 启动意图的修改集中在一处。 ## 验证 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index a828c87d..3b9c34f6 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -361,15 +361,7 @@ import { selectAdjacentPlatformRecommendEntry, } from '../rpg-entry/rpgEntryPublicGalleryViewModel'; import { - isBarkBattleGalleryEntry, - isBigFishGalleryEntry, isEdutainmentGalleryEntry, - isJumpHopGalleryEntry, - isMatch3DGalleryEntry, - isPuzzleGalleryEntry, - isSquareHoleGalleryEntry, - isVisualNovelGalleryEntry, - isWoodenFishGalleryEntry, mapBabyObjectMatchDraftToPlatformGalleryCard, mapBarkBattleWorkToPlatformGalleryCard, mapBigFishWorkToPlatformGalleryCard, @@ -514,15 +506,12 @@ import { isSamePlatformPublicGalleryEntry, mergePlatformPublicGalleryEntries, type RecommendRuntimeKind, + resolvePlatformRecommendRuntimeStartIntent, } from './platformPublicGalleryFlow'; import { - mapBarkBattlePublicDetailToWorkSummary, mapBarkBattleWorkToPublicWorkDetail, mapBigFishWorkToPublicWorkDetail, mapJumpHopWorkToPublicWorkDetail, - mapPublicWorkDetailToBigFishWork, - mapPublicWorkDetailToPuzzleWork, - mapPublicWorkDetailToSquareHoleWork, mapPuzzleWorkToPublicWorkDetail, mapRpgGalleryCardToPublicWorkDetail, mapSquareHoleWorkToPublicWorkDetail, @@ -12762,98 +12751,99 @@ export function PlatformEntryFlowShellImpl({ try { let started = false; - if (isBigFishGalleryEntry(entry)) { - const work = mapPublicWorkDetailToBigFishWork(entry); - if (!work) { - setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。'); - } else { - started = await startBigFishRunFromWork(work, 'platform', { - embedded: true, + const intent = resolvePlatformRecommendRuntimeStartIntent(entry, { + selectedPuzzleDetail, + barkBattleGalleryEntries, + mapMatch3DWork: mapPublicWorkDetailToMatch3DWork, + }); + + switch (intent.type) { + case 'blocked': + if (intent.errorTarget === 'big-fish') { + setBigFishError(intent.errorMessage); + } else if (intent.errorTarget === 'puzzle') { + setPuzzleError(intent.errorMessage); + } else if (intent.errorTarget === 'match3d') { + setMatch3DError(intent.errorMessage); + } else if (intent.errorTarget === 'square-hole') { + setSquareHoleError(intent.errorMessage); + } else { + setBarkBattleError(intent.errorMessage); + } + break; + case 'start-big-fish': + started = await startBigFishRunFromWork(intent.work, 'platform', { + embedded: intent.embedded, }); - } - } else if (isPuzzleGalleryEntry(entry)) { - const work = - selectedPuzzleDetail?.profileId === entry.profileId - ? selectedPuzzleDetail - : mapPublicWorkDetailToPuzzleWork(entry); - if (!work) { - setPuzzleError('当前拼图作品信息不完整,暂时无法进入玩法。'); - } else { + break; + case 'start-puzzle': started = await startPuzzleRunFromProfile( - work.profileId, - 'platform', - work, + intent.work.profileId, + intent.returnStage, + intent.work, false, null, - { embedded: true }, + { embedded: intent.embedded }, ); - } - } else if (isJumpHopGalleryEntry(entry)) { - started = await startJumpHopRunFromProfile(entry.profileId, { - embedded: true, - returnStage: 'platform', - }); - } else if (isWoodenFishGalleryEntry(entry)) { - started = await startWoodenFishRunFromProfile(entry.profileId, { - embedded: true, - returnStage: 'platform', - }); - } else if (isMatch3DGalleryEntry(entry)) { - const work = mapPublicWorkDetailToMatch3DWork(entry); - if (!work) { - setMatch3DError('当前抓大鹅作品信息不完整,暂时无法进入玩法。'); - } else { - started = await startMatch3DRunFromProfile( - work, - 'work-detail', - false, - { embedded: true }, - ); - } - } else if (isSquareHoleGalleryEntry(entry)) { - const work = mapPublicWorkDetailToSquareHoleWork(entry); - if (!work) { - setSquareHoleError( - '当前方洞挑战作品信息不完整,暂时无法进入玩法。', - ); - } else { - started = await startSquareHoleRunFromProfile( - work, - 'platform', - false, - { embedded: true }, - ); - } - } else if (isVisualNovelGalleryEntry(entry)) { - started = await startVisualNovelRunFromProfile( - entry.profileId, - 'platform', - { embedded: true }, - ); - } else if (isBarkBattleGalleryEntry(entry)) { - const work = - barkBattleGalleryEntries.find( - (item) => item.workId === entry.workId, - ) ?? mapBarkBattlePublicDetailToWorkSummary(entry); - if (!work) { - setBarkBattleError( - '当前汪汪声浪作品信息不完整,暂时无法进入玩法。', - ); - } else { - started = await startBarkBattleRunFromWork(work, 'platform', { - embedded: true, + break; + case 'start-jump-hop': + started = await startJumpHopRunFromProfile(intent.profileId, { + embedded: intent.embedded, + returnStage: intent.returnStage, }); + break; + case 'start-wooden-fish': + started = await startWoodenFishRunFromProfile(intent.profileId, { + embedded: intent.embedded, + returnStage: intent.returnStage, + }); + break; + case 'start-match3d': + started = await startMatch3DRunFromProfile( + intent.work, + intent.returnStage, + false, + { embedded: intent.embedded }, + ); + break; + case 'start-square-hole': + started = await startSquareHoleRunFromProfile( + intent.work, + intent.returnStage, + false, + { embedded: intent.embedded }, + ); + break; + case 'start-visual-novel': + started = await startVisualNovelRunFromProfile( + intent.profileId, + intent.returnStage, + { embedded: intent.embedded }, + ); + break; + case 'start-bark-battle': + started = await startBarkBattleRunFromWork( + intent.work, + intent.returnStage, + { embedded: intent.embedded }, + ); + break; + case 'start-edutainment': + started = await startBabyObjectMatchRuntimeFromEntry( + intent.entry, + intent.returnStage, + { + embedded: intent.embedded, + }, + ); + break; + case 'mark-ready': + started = true; + break; + default: { + const exhaustive: never = intent; + return exhaustive; } - } else if (isEdutainmentGalleryEntry(entry)) { - started = await startBabyObjectMatchRuntimeFromEntry( - entry, - 'platform', - { - embedded: true, - }, - ); - } else { - started = true; } if (!isCurrentStartRequest()) { diff --git a/src/components/platform-entry/platformPublicGalleryFlow.test.ts b/src/components/platform-entry/platformPublicGalleryFlow.test.ts index 7003243c..410c601c 100644 --- a/src/components/platform-entry/platformPublicGalleryFlow.test.ts +++ b/src/components/platform-entry/platformPublicGalleryFlow.test.ts @@ -1,5 +1,8 @@ import { expect, test } from 'vitest'; +import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; +import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime'; import { EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID, @@ -12,8 +15,16 @@ import { getPlatformRecommendRuntimeKind, isSamePlatformPublicGalleryEntry, mergePlatformPublicGalleryEntries, + type PlatformRecommendRuntimeStartIntentDeps, type RecommendRuntimeKind, + resolvePlatformRecommendRuntimeStartIntent, } from './platformPublicGalleryFlow'; +import { + mapBarkBattlePublicDetailToWorkSummary, + mapPublicWorkDetailToBigFishWork, + mapPublicWorkDetailToPuzzleWork, + mapPublicWorkDetailToSquareHoleWork, +} from './platformPublicWorkDetailFlow'; type TypedPlatformPublicGalleryCard = Extract< PlatformPublicGalleryCard, @@ -109,6 +120,99 @@ function buildTypedEntry( } } +function buildPuzzleWork( + overrides: Partial = {}, +): PuzzleWorkSummary { + return { + workId: 'puzzle-work', + profileId: 'puzzle-profile', + ownerUserId: 'user-1', + sourceSessionId: 'puzzle-session', + authorDisplayName: '玩家', + levelName: '拼图作品', + summary: '拼图摘要', + themeTags: ['拼图'], + coverImageSrc: '/puzzle-cover.png', + publicationStatus: 'published', + updatedAt: '2026-06-01T01:00:00.000Z', + publishedAt: '2026-06-01T00:00:00.000Z', + playCount: 3, + remixCount: 2, + likeCount: 1, + pointIncentiveTotalHalfPoints: 0, + pointIncentiveClaimedPoints: 0, + pointIncentiveTotalPoints: 0, + pointIncentiveClaimablePoints: 0, + publishReady: true, + ...overrides, + }; +} + +function buildMatch3DWork( + overrides: Partial = {}, +): Match3DWorkSummary { + return { + workId: 'match3d-work', + profileId: 'match3d-profile', + ownerUserId: 'user-1', + sourceSessionId: 'match3d-session', + gameName: '抓大鹅作品', + themeText: '经典消除', + summary: '抓大鹅摘要', + tags: ['抓大鹅'], + coverImageSrc: '/match3d-cover.png', + referenceImageSrc: null, + clearCount: 12, + difficulty: 4, + publicationStatus: 'published', + playCount: 10, + updatedAt: '2026-06-01T01:00:00.000Z', + publishedAt: '2026-06-01T00:00:00.000Z', + publishReady: true, + generatedItemAssets: [], + ...overrides, + }; +} + +function buildBarkBattleWork( + overrides: Partial = {}, +): BarkBattleWorkSummary { + return { + workId: 'bark-battle-work', + draftId: 'bark-battle-draft', + ownerUserId: 'user-1', + authorDisplayName: '玩家', + title: '汪汪声浪作品', + summary: '汪汪摘要', + themeDescription: '森林擂台', + playerImageDescription: '小狗', + opponentImageDescription: '对手', + playerCharacterImageSrc: '/player.png', + opponentCharacterImageSrc: '/opponent.png', + uiBackgroundImageSrc: '/bark-bg.png', + difficultyPreset: 'normal', + status: 'published', + generationStatus: 'ready', + publishReady: true, + playCount: 9, + recentPlayCount7d: 2, + updatedAt: '2026-06-01T01:00:00.000Z', + publishedAt: '2026-06-01T00:00:00.000Z', + ...overrides, + }; +} + +function buildRecommendRuntimeStartDeps( + overrides: Partial = {}, +): PlatformRecommendRuntimeStartIntentDeps { + return { + selectedPuzzleDetail: null, + barkBattleGalleryEntries: [], + mapMatch3DWork: () => buildMatch3DWork(), + ...overrides, + }; +} + test('platform public gallery flow resolves stable key and runtime kind for every play kind', () => { const cases: Array< [sourceType: PlatformGallerySourceType, keyKind: string, kind: RecommendRuntimeKind] @@ -160,6 +264,181 @@ test('platform public gallery flow compares entries by resolved identity', () => expect(isSamePlatformPublicGalleryEntry(left, otherKind)).toBe(false); }); +test('platform public gallery flow resolves recommend runtime start intent', () => { + const bigFishEntry = buildTypedEntry('big-fish'); + expect( + resolvePlatformRecommendRuntimeStartIntent( + bigFishEntry, + buildRecommendRuntimeStartDeps(), + ), + ).toEqual({ + type: 'start-big-fish', + work: mapPublicWorkDetailToBigFishWork(bigFishEntry), + returnStage: 'platform', + embedded: true, + }); + + const selectedPuzzleDetail = buildPuzzleWork({ + profileId: 'puzzle-profile', + }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + buildTypedEntry('puzzle'), + buildRecommendRuntimeStartDeps({ selectedPuzzleDetail }), + ), + ).toEqual({ + type: 'start-puzzle', + work: selectedPuzzleDetail, + returnStage: 'platform', + embedded: true, + }); + + const puzzleEntry = buildTypedEntry('puzzle', { + profileId: 'fallback-puzzle-profile', + }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + puzzleEntry, + buildRecommendRuntimeStartDeps({ + selectedPuzzleDetail: buildPuzzleWork({ profileId: 'stale-profile' }), + }), + ), + ).toEqual({ + type: 'start-puzzle', + work: mapPublicWorkDetailToPuzzleWork(puzzleEntry), + returnStage: 'platform', + embedded: true, + }); + + expect( + resolvePlatformRecommendRuntimeStartIntent( + buildTypedEntry('jump-hop'), + buildRecommendRuntimeStartDeps(), + ), + ).toEqual({ + type: 'start-jump-hop', + profileId: 'jump-hop-profile', + returnStage: 'platform', + embedded: true, + }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + buildTypedEntry('wooden-fish'), + buildRecommendRuntimeStartDeps(), + ), + ).toEqual({ + type: 'start-wooden-fish', + profileId: 'wooden-fish-profile', + returnStage: 'platform', + embedded: true, + }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + buildTypedEntry('visual-novel'), + buildRecommendRuntimeStartDeps(), + ), + ).toEqual({ + type: 'start-visual-novel', + profileId: 'visual-novel-profile', + returnStage: 'platform', + embedded: true, + }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + buildTypedEntry('edutainment'), + buildRecommendRuntimeStartDeps(), + ), + ).toEqual({ + type: 'start-edutainment', + entry: buildTypedEntry('edutainment'), + returnStage: 'platform', + embedded: true, + }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + buildRpgEntry(), + buildRecommendRuntimeStartDeps(), + ), + ).toEqual({ + type: 'mark-ready', + }); +}); + +test('platform public gallery flow resolves recommend runtime mapper-backed start intent', () => { + const match3DEntry = buildTypedEntry('match3d'); + const match3DWork = buildMatch3DWork({ workId: 'mapped-match3d-work' }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + match3DEntry, + buildRecommendRuntimeStartDeps({ + mapMatch3DWork: (entry) => + entry === match3DEntry ? match3DWork : null, + }), + ), + ).toEqual({ + type: 'start-match3d', + work: match3DWork, + returnStage: 'work-detail', + embedded: true, + }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + match3DEntry, + buildRecommendRuntimeStartDeps({ mapMatch3DWork: () => null }), + ), + ).toEqual({ + type: 'blocked', + errorTarget: 'match3d', + errorMessage: '当前抓大鹅作品信息不完整,暂时无法进入玩法。', + }); + + const squareHoleEntry = buildTypedEntry('square-hole'); + expect( + resolvePlatformRecommendRuntimeStartIntent( + squareHoleEntry, + buildRecommendRuntimeStartDeps(), + ), + ).toEqual({ + type: 'start-square-hole', + work: mapPublicWorkDetailToSquareHoleWork(squareHoleEntry), + returnStage: 'platform', + embedded: true, + }); +}); + +test('platform public gallery flow resolves recommend runtime bark battle priority', () => { + const entry = buildTypedEntry('bark-battle'); + const galleryWork = buildBarkBattleWork({ + workId: 'bark-battle-work', + title: '推荐缓存', + }); + + expect( + resolvePlatformRecommendRuntimeStartIntent( + entry, + buildRecommendRuntimeStartDeps({ + barkBattleGalleryEntries: [galleryWork], + }), + ), + ).toEqual({ + type: 'start-bark-battle', + work: galleryWork, + returnStage: 'platform', + embedded: true, + }); + expect( + resolvePlatformRecommendRuntimeStartIntent( + entry, + buildRecommendRuntimeStartDeps(), + ), + ).toEqual({ + type: 'start-bark-battle', + work: mapBarkBattlePublicDetailToWorkSummary(entry), + returnStage: 'platform', + embedded: true, + }); +}); + test('platform public gallery flow merges duplicate identities and sorts newest first', () => { const staleRpgEntry = buildRpgEntry({ profileId: 'shared-rpg', diff --git a/src/components/platform-entry/platformPublicGalleryFlow.ts b/src/components/platform-entry/platformPublicGalleryFlow.ts index 6a28e0c9..c8dd70e7 100644 --- a/src/components/platform-entry/platformPublicGalleryFlow.ts +++ b/src/components/platform-entry/platformPublicGalleryFlow.ts @@ -1,4 +1,9 @@ +import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; +import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; +import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime'; +import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import { isBarkBattleGalleryEntry, isBigFishGalleryEntry, @@ -11,6 +16,12 @@ import { isWoodenFishGalleryEntry, type PlatformPublicGalleryCard, } from '../rpg-entry/rpgEntryWorldPresentation'; +import { + mapBarkBattlePublicDetailToWorkSummary, + mapPublicWorkDetailToBigFishWork, + mapPublicWorkDetailToPuzzleWork, + mapPublicWorkDetailToSquareHoleWork, +} from './platformPublicWorkDetailFlow'; export type RecommendRuntimeKind = | 'bark-battle' @@ -24,6 +35,85 @@ export type RecommendRuntimeKind = | 'visual-novel' | 'rpg'; +export type PlatformRecommendRuntimeStartErrorTarget = + | 'bark-battle' + | 'big-fish' + | 'match3d' + | 'puzzle' + | 'square-hole'; + +export type PlatformRecommendRuntimeStartIntent = + | { + type: 'blocked'; + errorTarget: PlatformRecommendRuntimeStartErrorTarget; + errorMessage: string; + } + | { + type: 'start-big-fish'; + work: BigFishWorkSummary; + returnStage: 'platform'; + embedded: true; + } + | { + type: 'start-puzzle'; + work: PuzzleWorkSummary; + returnStage: 'platform'; + embedded: true; + } + | { + type: 'start-jump-hop'; + profileId: string; + returnStage: 'platform'; + embedded: true; + } + | { + type: 'start-wooden-fish'; + profileId: string; + returnStage: 'platform'; + embedded: true; + } + | { + type: 'start-match3d'; + work: Match3DWorkSummary; + returnStage: 'work-detail'; + embedded: true; + } + | { + type: 'start-square-hole'; + work: SquareHoleWorkSummary; + returnStage: 'platform'; + embedded: true; + } + | { + type: 'start-visual-novel'; + profileId: string; + returnStage: 'platform'; + embedded: true; + } + | { + type: 'start-bark-battle'; + work: BarkBattleWorkSummary; + returnStage: 'platform'; + embedded: true; + } + | { + type: 'start-edutainment'; + entry: PlatformPublicGalleryCard; + returnStage: 'platform'; + embedded: true; + } + | { + type: 'mark-ready'; + }; + +export type PlatformRecommendRuntimeStartIntentDeps = { + selectedPuzzleDetail?: PuzzleWorkSummary | null; + barkBattleGalleryEntries?: readonly BarkBattleWorkSummary[]; + mapMatch3DWork: ( + entry: PlatformPublicGalleryCard, + ) => Match3DWorkSummary | null; +}; + export function getPlatformPublicGalleryEntryTime( entry: PlatformPublicGalleryCard, ) { @@ -100,6 +190,148 @@ export function getPlatformRecommendRuntimeKind( return 'rpg'; } +export function resolvePlatformRecommendRuntimeStartIntent( + entry: PlatformPublicGalleryCard, + deps: PlatformRecommendRuntimeStartIntentDeps, +): PlatformRecommendRuntimeStartIntent { + if (isBigFishGalleryEntry(entry)) { + const work = mapPublicWorkDetailToBigFishWork(entry); + if (!work) { + return { + type: 'blocked', + errorTarget: 'big-fish', + errorMessage: '当前作品缺少会话信息,暂时无法进入玩法。', + }; + } + + return { + type: 'start-big-fish', + work, + returnStage: 'platform', + embedded: true, + }; + } + + if (isPuzzleGalleryEntry(entry)) { + const work = + deps.selectedPuzzleDetail?.profileId === entry.profileId + ? deps.selectedPuzzleDetail + : mapPublicWorkDetailToPuzzleWork(entry); + if (!work) { + return { + type: 'blocked', + errorTarget: 'puzzle', + errorMessage: '当前拼图作品信息不完整,暂时无法进入玩法。', + }; + } + + return { + type: 'start-puzzle', + work, + returnStage: 'platform', + embedded: true, + }; + } + + if (isJumpHopGalleryEntry(entry)) { + return { + type: 'start-jump-hop', + profileId: entry.profileId, + returnStage: 'platform', + embedded: true, + }; + } + + if (isWoodenFishGalleryEntry(entry)) { + return { + type: 'start-wooden-fish', + profileId: entry.profileId, + returnStage: 'platform', + embedded: true, + }; + } + + if (isMatch3DGalleryEntry(entry)) { + // 中文注释:抓大鹅推荐 runtime 仍接 Match3D Module 的 Adapter,避免复制素材归一规则。 + const work = deps.mapMatch3DWork(entry); + if (!work) { + return { + type: 'blocked', + errorTarget: 'match3d', + errorMessage: '当前抓大鹅作品信息不完整,暂时无法进入玩法。', + }; + } + + return { + type: 'start-match3d', + work, + returnStage: 'work-detail', + embedded: true, + }; + } + + if (isSquareHoleGalleryEntry(entry)) { + const work = mapPublicWorkDetailToSquareHoleWork(entry); + if (!work) { + return { + type: 'blocked', + errorTarget: 'square-hole', + errorMessage: '当前方洞挑战作品信息不完整,暂时无法进入玩法。', + }; + } + + return { + type: 'start-square-hole', + work, + returnStage: 'platform', + embedded: true, + }; + } + + if (isVisualNovelGalleryEntry(entry)) { + return { + type: 'start-visual-novel', + profileId: entry.profileId, + returnStage: 'platform', + embedded: true, + }; + } + + if (isBarkBattleGalleryEntry(entry)) { + const work = + deps.barkBattleGalleryEntries?.find( + (item) => item.workId === entry.workId, + ) ?? mapBarkBattlePublicDetailToWorkSummary(entry); + if (!work) { + return { + type: 'blocked', + errorTarget: 'bark-battle', + errorMessage: '当前汪汪声浪作品信息不完整,暂时无法进入玩法。', + }; + } + + return { + type: 'start-bark-battle', + work, + returnStage: 'platform', + embedded: true, + }; + } + + if (isEdutainmentGalleryEntry(entry)) { + return { + type: 'start-edutainment', + entry, + returnStage: 'platform', + embedded: true, + }; + } + + return { + type: 'mark-ready', + }; +} + export function isSamePlatformPublicGalleryEntry( left: PlatformPublicGalleryCard, right: PlatformPublicGalleryCard,