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(); [...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, }; }