48c7cce1ba
合入 master 最新的认证、玩法契约与推荐页改动。 修复拼图草稿生成、推荐页下一关和公开详情访客试玩回归。 修复抓大鹅草稿试玩鉴权与首屏推荐详情测试入口。 补齐相关测试夹具、文档与团队记忆更新。
595 lines
17 KiB
TypeScript
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,
|
|
};
|
|
}
|