Files
Genarrative/src/components/platform-entry/platformPublicGalleryFlow.ts
T
kdletters 48c7cce1ba 合并 master 并修复架构分支回归
合入 master 最新的认证、玩法契约与推荐页改动。

修复拼图草稿生成、推荐页下一关和公开详情访客试玩回归。

修复抓大鹅草稿试玩鉴权与首屏推荐详情测试入口。

补齐相关测试夹具、文档与团队记忆更新。
2026-06-07 21:35:47 +08:00

595 lines
17 KiB
TypeScript

import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { JumpHopGalleryCardResponse } from '../../../packages/shared/src/contracts/jumpHop';
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 type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import type { WoodenFishGalleryCardResponse } from '../../../packages/shared/src/contracts/woodenFish';
import type { PuzzleClearGalleryCardResponse } from '../../services/puzzle-clear/puzzleClearClient';
import {
isBarkBattleGalleryEntry,
isBigFishGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isMatch3DGalleryEntry,
isPuzzleClearGalleryEntry,
isPuzzleGalleryEntry,
isSquareHoleGalleryEntry,
isVisualNovelGalleryEntry,
isWoodenFishGalleryEntry,
mapBabyObjectMatchDraftToPlatformGalleryCard,
mapBarkBattleWorkToPlatformGalleryCard,
mapBigFishWorkToPlatformGalleryCard,
mapJumpHopWorkToPlatformGalleryCard,
mapPuzzleClearWorkToPlatformGalleryCard,
mapPuzzleWorkToPlatformGalleryCard,
mapSquareHoleWorkToPlatformGalleryCard,
mapVisualNovelWorkToPlatformGalleryCard,
mapWoodenFishWorkToPlatformGalleryCard,
type PlatformPublicGalleryCard,
} from '../rpg-entry/rpgEntryWorldPresentation';
import { mapMatch3DWorkToPublicWorkDetail } from './platformMatch3DRuntimeProfile';
import {
mapBarkBattlePublicDetailToWorkSummary,
mapPublicWorkDetailToBigFishWork,
mapPublicWorkDetailToPuzzleWork,
mapPublicWorkDetailToSquareHoleWork,
} from './platformPublicWorkDetailFlow';
export type RecommendRuntimeKind =
| 'bark-battle'
| 'big-fish'
| 'edutainment'
| 'jump-hop'
| 'match3d'
| 'puzzle'
| 'puzzle-clear'
| 'square-hole'
| 'wooden-fish'
| 'visual-novel'
| 'rpg';
export type PlatformRecommendRuntimeStartErrorTarget =
| 'bark-battle'
| 'big-fish'
| 'match3d'
| 'puzzle'
| 'puzzle-clear'
| '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-puzzle-clear';
profileId: string;
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 type PlatformRecommendRuntimeReadyState = {
activeKind: RecommendRuntimeKind | null;
hasBabyObjectMatchDraft?: boolean;
hasBigFishRun?: boolean;
hasJumpHopRun?: boolean;
hasMatch3DRun?: boolean;
hasPuzzleClearRun?: boolean;
hasSquareHoleRun?: boolean;
hasVisualNovelRun?: boolean;
hasWoodenFishRun?: boolean;
puzzleRunEntryProfileId?: string | null;
puzzleRunCurrentLevelProfileId?: string | null;
};
export type PlatformRecommendRuntimeAutoStartDecision =
| { type: 'noop' }
| { type: 'clear' }
| { type: 'start'; entry: PlatformPublicGalleryCard };
export type PlatformRecommendRuntimeAutoStartInput = {
isDesktopLayout: boolean;
selectionStage: string;
platformTab: string;
isLoadingPlatform: boolean;
entries: readonly PlatformPublicGalleryCard[];
activeEntryKey: string | null;
isStarting: boolean;
hasStartError?: boolean;
readyState: PlatformRecommendRuntimeReadyState;
};
export type PlatformPublicGalleryFeedsInput = {
rpgEntries: readonly CustomWorldGalleryCard[];
bigFishEntries: readonly BigFishWorkSummary[];
match3dEntries: readonly Match3DWorkSummary[];
puzzleEntries: readonly PuzzleWorkSummary[];
puzzleClearEntries: readonly PuzzleClearGalleryCardResponse[];
barkBattleGalleryEntries: readonly BarkBattleWorkSummary[];
barkBattleWorks: readonly BarkBattleWorkSummary[];
jumpHopEntries: readonly JumpHopGalleryCardResponse[];
woodenFishEntries: readonly WoodenFishGalleryCardResponse[];
squareHoleEntries: readonly SquareHoleWorkSummary[];
visualNovelEntries: readonly VisualNovelWorkSummary[];
babyObjectMatchDrafts: readonly BabyObjectMatchDraft[];
isBigFishCreationVisible: boolean;
isBabyObjectMatchVisible: boolean;
isVisualNovelCreationOpen: boolean;
};
export type PlatformPublicGalleryFeeds = {
featuredEntries: PlatformPublicGalleryCard[];
latestEntries: PlatformPublicGalleryCard[];
};
export function getPlatformPublicGalleryEntryTime(
entry: PlatformPublicGalleryCard,
) {
const rawTime = entry.publishedAt ?? entry.updatedAt;
const timestamp = new Date(rawTime).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}
export function getPlatformPublicGalleryEntryKey(
entry: PlatformPublicGalleryCard,
) {
// 同一作品身份由玩法、作者与 profile 共同确定,避免不同玩法共享 profileId 时误合并。
const kind = isBigFishGalleryEntry(entry)
? 'big-fish'
: isPuzzleGalleryEntry(entry)
? 'puzzle'
: isPuzzleClearGalleryEntry(entry)
? 'puzzle-clear'
: isJumpHopGalleryEntry(entry)
? 'jump-hop'
: isWoodenFishGalleryEntry(entry)
? 'wooden-fish'
: isMatch3DGalleryEntry(entry)
? 'match3d'
: isSquareHoleGalleryEntry(entry)
? 'square-hole'
: isVisualNovelGalleryEntry(entry)
? 'visual-novel'
: isBarkBattleGalleryEntry(entry)
? 'bark-battle'
: isEdutainmentGalleryEntry(entry)
? `edutainment:${entry.templateId}`
: 'rpg';
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
}
export function getPlatformRecommendRuntimeKind(
entry: PlatformPublicGalleryCard,
): RecommendRuntimeKind {
if (isBigFishGalleryEntry(entry)) {
return 'big-fish';
}
if (isPuzzleGalleryEntry(entry)) {
return 'puzzle';
}
if (isPuzzleClearGalleryEntry(entry)) {
return 'puzzle-clear';
}
if (isJumpHopGalleryEntry(entry)) {
return 'jump-hop';
}
if (isWoodenFishGalleryEntry(entry)) {
return 'wooden-fish';
}
if (isMatch3DGalleryEntry(entry)) {
return 'match3d';
}
if (isSquareHoleGalleryEntry(entry)) {
return 'square-hole';
}
if (isVisualNovelGalleryEntry(entry)) {
return 'visual-novel';
}
if (isBarkBattleGalleryEntry(entry)) {
return 'bark-battle';
}
if (isEdutainmentGalleryEntry(entry)) {
return 'edutainment';
}
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 (isPuzzleClearGalleryEntry(entry)) {
return {
type: 'start-puzzle-clear',
profileId: entry.profileId,
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 isPlatformRecommendRuntimeReadyForEntry(
entry: PlatformPublicGalleryCard,
state: PlatformRecommendRuntimeReadyState,
) {
const expectedKind = getPlatformRecommendRuntimeKind(entry);
if (state.activeKind !== expectedKind) {
return false;
}
if (expectedKind === 'big-fish') {
return Boolean(state.hasBigFishRun);
}
if (expectedKind === 'jump-hop') {
return Boolean(state.hasJumpHopRun);
}
if (expectedKind === 'wooden-fish') {
return Boolean(state.hasWoodenFishRun);
}
if (expectedKind === 'match3d') {
return Boolean(state.hasMatch3DRun);
}
if (expectedKind === 'puzzle') {
return (
state.puzzleRunEntryProfileId === entry.profileId ||
state.puzzleRunCurrentLevelProfileId === entry.profileId
);
}
if (expectedKind === 'puzzle-clear') {
return Boolean(state.hasPuzzleClearRun);
}
if (expectedKind === 'square-hole') {
return Boolean(state.hasSquareHoleRun);
}
if (expectedKind === 'visual-novel') {
return Boolean(state.hasVisualNovelRun);
}
if (expectedKind === 'bark-battle') {
return true;
}
if (expectedKind === 'edutainment') {
return Boolean(state.hasBabyObjectMatchDraft);
}
return true;
}
export function resolvePlatformRecommendRuntimeAutoStartDecision(
input: PlatformRecommendRuntimeAutoStartInput,
): PlatformRecommendRuntimeAutoStartDecision {
if (
input.isDesktopLayout ||
input.selectionStage !== 'platform' ||
input.platformTab !== 'home' ||
input.isLoadingPlatform
) {
return { type: 'noop' };
}
if (input.entries.length === 0) {
return { type: 'clear' };
}
const activeEntry = input.activeEntryKey
? (input.entries.find(
(entry) =>
getPlatformPublicGalleryEntryKey(entry) === input.activeEntryKey,
) ?? null)
: null;
const isActiveRuntimeReady =
activeEntry !== null &&
isPlatformRecommendRuntimeReadyForEntry(activeEntry, input.readyState);
if (
(activeEntry !== null && isActiveRuntimeReady) ||
input.isStarting ||
(activeEntry !== null && input.hasStartError)
) {
return { type: 'noop' };
}
const nextEntry = activeEntry ?? input.entries[0];
return nextEntry ? { type: 'start', entry: nextEntry } : { type: 'clear' };
}
export function isSamePlatformPublicGalleryEntry(
left: PlatformPublicGalleryCard,
right: PlatformPublicGalleryCard,
) {
return (
getPlatformPublicGalleryEntryKey(left) ===
getPlatformPublicGalleryEntryKey(right)
);
}
export function mergePlatformPublicGalleryEntries(
rpgEntries: readonly CustomWorldGalleryCard[],
puzzleEntries: readonly PlatformPublicGalleryCard[],
) {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...rpgEntries, ...puzzleEntries].forEach((entry) => {
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
});
return Array.from(entryMap.values()).sort(
(left, right) =>
getPlatformPublicGalleryEntryTime(right) -
getPlatformPublicGalleryEntryTime(left),
);
}
export function buildPlatformPublicGalleryFeeds(
input: PlatformPublicGalleryFeedsInput,
): PlatformPublicGalleryFeeds {
const bigFishEntries = input.isBigFishCreationVisible
? input.bigFishEntries.map(mapBigFishWorkToPlatformGalleryCard)
: [];
const babyObjectMatchEntries = input.isBabyObjectMatchVisible
? input.babyObjectMatchDrafts
.filter((draft) => draft.publicationStatus === 'published')
.map(mapBabyObjectMatchDraftToPlatformGalleryCard)
: [];
const barkBattleGalleryEntries = input.barkBattleGalleryEntries.map(
mapBarkBattleWorkToPlatformGalleryCard,
);
const barkBattleFallbackEntries =
input.barkBattleGalleryEntries.length === 0
? input.barkBattleWorks
.filter((work) => work.status === 'published')
.map(mapBarkBattleWorkToPlatformGalleryCard)
: [];
const visualNovelEntries = input.isVisualNovelCreationOpen
? input.visualNovelEntries.map(mapVisualNovelWorkToPlatformGalleryCard)
: [];
const latestEntries = mergePlatformPublicGalleryEntries(input.rpgEntries, [
...bigFishEntries,
...input.match3dEntries.map(mapMatch3DWorkToPublicWorkDetail),
...input.puzzleEntries.map(mapPuzzleWorkToPlatformGalleryCard),
...input.puzzleClearEntries.map(mapPuzzleClearWorkToPlatformGalleryCard),
...barkBattleGalleryEntries,
...input.jumpHopEntries.map(mapJumpHopWorkToPlatformGalleryCard),
...barkBattleFallbackEntries,
...input.woodenFishEntries.map(mapWoodenFishWorkToPlatformGalleryCard),
...input.squareHoleEntries.map(mapSquareHoleWorkToPlatformGalleryCard),
...visualNovelEntries,
...babyObjectMatchEntries,
]);
const featuredEntries = mergePlatformPublicGalleryEntries(input.rpgEntries, [
...bigFishEntries,
...input.match3dEntries.map(mapMatch3DWorkToPublicWorkDetail),
...input.puzzleEntries.map(mapPuzzleWorkToPlatformGalleryCard),
...input.puzzleClearEntries.map(mapPuzzleClearWorkToPlatformGalleryCard),
...(barkBattleGalleryEntries.length > 0
? barkBattleGalleryEntries
: barkBattleFallbackEntries),
...input.squareHoleEntries.map(mapSquareHoleWorkToPlatformGalleryCard),
...input.jumpHopEntries.map(mapJumpHopWorkToPlatformGalleryCard),
...input.woodenFishEntries.map(mapWoodenFishWorkToPlatformGalleryCard),
...visualNovelEntries,
...babyObjectMatchEntries,
]).slice(0, 6);
return {
featuredEntries,
latestEntries,
};
}