refactor: 收口推荐运行态启动意图

This commit is contained in:
2026-06-04 01:11:27 +08:00
parent 7349c6df4f
commit 8d3e14020f
6 changed files with 612 additions and 101 deletions

View File

@@ -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()) {

View File

@@ -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> = {},
): 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> = {},
): 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> = {},
): 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> = {},
): 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',

View File

@@ -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,