582 lines
15 KiB
TypeScript
582 lines
15 KiB
TypeScript
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
|
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
|
import type {
|
|
JumpHopGalleryCardResponse,
|
|
JumpHopWorkProfileResponse,
|
|
} from '../../../packages/shared/src/contracts/jumpHop';
|
|
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
|
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,
|
|
WoodenFishWorkProfileResponse,
|
|
} from '../../../packages/shared/src/contracts/woodenFish';
|
|
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
|
import {
|
|
isBarkBattleGalleryEntry,
|
|
isBigFishGalleryEntry,
|
|
isEdutainmentGalleryEntry,
|
|
isJumpHopGalleryEntry,
|
|
isMatch3DGalleryEntry,
|
|
isPuzzleGalleryEntry,
|
|
isSquareHoleGalleryEntry,
|
|
isVisualNovelGalleryEntry,
|
|
isWoodenFishGalleryEntry,
|
|
mapBarkBattleWorkToPlatformGalleryCard,
|
|
mapBigFishWorkToPlatformGalleryCard,
|
|
mapJumpHopWorkToPlatformGalleryCard,
|
|
mapPuzzleWorkToPlatformGalleryCard,
|
|
mapSquareHoleWorkToPlatformGalleryCard,
|
|
mapVisualNovelWorkToPlatformGalleryCard,
|
|
mapWoodenFishWorkToPlatformGalleryCard,
|
|
type PlatformPublicGalleryCard,
|
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
|
import {
|
|
canExposePublicWork,
|
|
EDUTAINMENT_HIDDEN_MESSAGE,
|
|
} from './platformEdutainmentVisibility';
|
|
|
|
export type PlatformPublicWorkDetailKind =
|
|
| 'bark-battle'
|
|
| 'big-fish'
|
|
| 'edutainment'
|
|
| 'jump-hop'
|
|
| 'match3d'
|
|
| 'puzzle'
|
|
| 'rpg'
|
|
| 'square-hole'
|
|
| 'visual-novel'
|
|
| 'wooden-fish';
|
|
|
|
export type PlatformPublicWorkDetailOpenStrategy =
|
|
| {
|
|
type: 'use-entry';
|
|
kind: Exclude<
|
|
PlatformPublicWorkDetailKind,
|
|
'jump-hop' | 'puzzle' | 'rpg' | 'visual-novel' | 'wooden-fish'
|
|
>;
|
|
}
|
|
| {
|
|
type: 'load-puzzle-detail';
|
|
profileId: string;
|
|
}
|
|
| {
|
|
type: 'load-jump-hop-detail';
|
|
profileId: string;
|
|
}
|
|
| {
|
|
type: 'load-wooden-fish-detail';
|
|
profileId: string;
|
|
}
|
|
| {
|
|
type: 'load-visual-novel-detail';
|
|
profileId: string;
|
|
}
|
|
| {
|
|
type: 'load-rpg-detail';
|
|
entry: CustomWorldGalleryCard;
|
|
};
|
|
|
|
export type PlatformPublicWorkActionMode = 'edit' | 'remix';
|
|
|
|
export type PlatformPublicWorkLikeIntent =
|
|
| {
|
|
type: 'like-big-fish';
|
|
profileId: string;
|
|
}
|
|
| {
|
|
type: 'like-puzzle';
|
|
profileId: string;
|
|
}
|
|
| {
|
|
type: 'like-rpg-gallery';
|
|
ownerUserId: string;
|
|
profileId: string;
|
|
}
|
|
| {
|
|
type: 'unsupported';
|
|
errorMessage: string;
|
|
};
|
|
|
|
export type PlatformPublicWorkDetailOpenDecision =
|
|
| {
|
|
type: 'blocked';
|
|
selectedDetail: null;
|
|
errorMessage: string;
|
|
selectionStage: 'platform';
|
|
historyPath: null;
|
|
}
|
|
| {
|
|
type: 'open';
|
|
selectedDetail: PlatformPublicGalleryCard;
|
|
errorMessage: null;
|
|
selectionStage: 'work-detail';
|
|
historyPath: string | null;
|
|
};
|
|
|
|
export type PlatformPublicWorkDetailOpenDecisionDeps = {
|
|
canExposeEntry?: (entry: PlatformPublicGalleryCard) => boolean;
|
|
hiddenMessage?: string;
|
|
buildWorkDetailPath?: (publicWorkCode: string) => string;
|
|
};
|
|
|
|
export type ActivePlatformPublicWorkAuthorEntryInput = {
|
|
selectionStage: string;
|
|
selectedPublicWorkDetail: PlatformPublicGalleryCard | null;
|
|
selectedRpgDetailEntry: CustomWorldGalleryCard | null;
|
|
};
|
|
|
|
export function isRpgPublicWorkDetailEntry(
|
|
entry: PlatformPublicGalleryCard,
|
|
): entry is CustomWorldGalleryCard {
|
|
return !('sourceType' in entry);
|
|
}
|
|
|
|
export function mapRpgGalleryCardToPublicWorkDetail(
|
|
entry: CustomWorldGalleryCard,
|
|
): PlatformPublicGalleryCard {
|
|
return entry;
|
|
}
|
|
|
|
export function mapPuzzleWorkToPublicWorkDetail(
|
|
item: PuzzleWorkSummary,
|
|
): PlatformPublicGalleryCard {
|
|
return mapPuzzleWorkToPlatformGalleryCard(item);
|
|
}
|
|
|
|
export function mapSquareHoleWorkToPublicWorkDetail(
|
|
item: SquareHoleWorkSummary,
|
|
): PlatformPublicGalleryCard {
|
|
return mapSquareHoleWorkToPlatformGalleryCard(item);
|
|
}
|
|
|
|
export function mapBigFishWorkToPublicWorkDetail(
|
|
item: BigFishWorkSummary,
|
|
): PlatformPublicGalleryCard {
|
|
return mapBigFishWorkToPlatformGalleryCard(item);
|
|
}
|
|
|
|
export function mapVisualNovelWorkToPublicWorkDetail(
|
|
item: VisualNovelWorkSummary,
|
|
): PlatformPublicGalleryCard {
|
|
return mapVisualNovelWorkToPlatformGalleryCard(item);
|
|
}
|
|
|
|
export function mapJumpHopWorkToPublicWorkDetail(
|
|
item: JumpHopGalleryCardResponse | JumpHopWorkProfileResponse,
|
|
): PlatformPublicGalleryCard {
|
|
return mapJumpHopWorkToPlatformGalleryCard(item);
|
|
}
|
|
|
|
export function mapBarkBattleWorkToPublicWorkDetail(
|
|
item: BarkBattleWorkSummary,
|
|
): PlatformPublicGalleryCard {
|
|
return mapBarkBattleWorkToPlatformGalleryCard(item);
|
|
}
|
|
|
|
export function mapWoodenFishWorkToPublicWorkDetail(
|
|
item: WoodenFishGalleryCardResponse | WoodenFishWorkProfileResponse,
|
|
): PlatformPublicGalleryCard {
|
|
return mapWoodenFishWorkToPlatformGalleryCard(item);
|
|
}
|
|
|
|
export function mapBarkBattlePublicDetailToWorkSummary(
|
|
entry: PlatformPublicGalleryCard,
|
|
): BarkBattleWorkSummary | null {
|
|
if (!isBarkBattleGalleryEntry(entry)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
workId: entry.workId,
|
|
draftId: entry.sourceSessionId ?? null,
|
|
ownerUserId: entry.ownerUserId,
|
|
authorDisplayName: entry.authorDisplayName,
|
|
title: entry.worldName,
|
|
summary: entry.summaryText,
|
|
themeDescription: entry.themeTags[0] ?? entry.summaryText,
|
|
playerImageDescription: entry.themeTags[1] ?? entry.summaryText,
|
|
opponentImageDescription: entry.themeTags[2] ?? entry.summaryText,
|
|
onomatopoeia: undefined,
|
|
playerCharacterImageSrc: entry.coverCharacterImageSrcs[0] ?? null,
|
|
opponentCharacterImageSrc: entry.coverCharacterImageSrcs[1] ?? null,
|
|
uiBackgroundImageSrc: entry.coverImageSrc,
|
|
difficultyPreset: 'normal',
|
|
status: 'published',
|
|
generationStatus: 'ready',
|
|
publishReady: true,
|
|
playCount: entry.playCount ?? 0,
|
|
recentPlayCount7d: entry.recentPlayCount7d ?? 0,
|
|
updatedAt: entry.updatedAt,
|
|
publishedAt: entry.publishedAt,
|
|
};
|
|
}
|
|
|
|
export function mapPublicWorkDetailToPuzzleWork(
|
|
entry: PlatformPublicGalleryCard,
|
|
): PuzzleWorkSummary | null {
|
|
if (!isPuzzleGalleryEntry(entry)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
workId: entry.workId,
|
|
profileId: entry.profileId,
|
|
ownerUserId: entry.ownerUserId,
|
|
sourceSessionId:
|
|
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
|
? entry.sourceSessionId
|
|
: null,
|
|
authorDisplayName: entry.authorDisplayName,
|
|
levelName: entry.worldName,
|
|
summary: entry.summaryText,
|
|
themeTags: entry.themeTags,
|
|
coverImageSrc: entry.coverImageSrc,
|
|
publicationStatus: 'published',
|
|
updatedAt: entry.updatedAt,
|
|
publishedAt: entry.publishedAt,
|
|
playCount: entry.playCount ?? 0,
|
|
remixCount: entry.remixCount ?? 0,
|
|
likeCount: entry.likeCount ?? 0,
|
|
pointIncentiveTotalHalfPoints: 0,
|
|
pointIncentiveClaimedPoints: 0,
|
|
pointIncentiveTotalPoints: 0,
|
|
pointIncentiveClaimablePoints: 0,
|
|
publishReady: true,
|
|
levels:
|
|
entry.coverSlides?.map((slide, index) => ({
|
|
levelId: slide.id || `puzzle-level-${index + 1}`,
|
|
levelName: slide.label,
|
|
pictureDescription: entry.summaryText,
|
|
candidates: [],
|
|
selectedCandidateId: null,
|
|
coverImageSrc: slide.imageSrc,
|
|
coverAssetId: null,
|
|
generationStatus: 'ready' as const,
|
|
})) ?? [],
|
|
};
|
|
}
|
|
|
|
export function resolveVisiblePuzzleDetailCoverCount(
|
|
entry: PlatformPublicGalleryCard | null,
|
|
run: PuzzleRunSnapshot | null,
|
|
) {
|
|
if (!entry || !isPuzzleGalleryEntry(entry)) {
|
|
return 1;
|
|
}
|
|
|
|
if (run?.entryProfileId !== entry.profileId) {
|
|
return 1;
|
|
}
|
|
|
|
// 中文注释:封面首图永远公开,后续封面跟随当前玩家本次 run 的通关进度即时解锁。
|
|
return Math.max(1, run.clearedLevelCount + 1);
|
|
}
|
|
|
|
export function mapPublicWorkDetailToBigFishWork(
|
|
entry: PlatformPublicGalleryCard,
|
|
): BigFishWorkSummary | null {
|
|
if (!isBigFishGalleryEntry(entry)) {
|
|
return null;
|
|
}
|
|
|
|
const levelCount = Number.parseInt(
|
|
entry.themeTags.find((tag) => /^\d+级$/u.test(tag))?.replace('级', '') ??
|
|
'0',
|
|
10,
|
|
);
|
|
|
|
return {
|
|
workId: entry.workId,
|
|
sourceSessionId: entry.profileId,
|
|
ownerUserId: entry.ownerUserId,
|
|
authorDisplayName: entry.authorDisplayName,
|
|
title: entry.worldName,
|
|
subtitle: entry.subtitle,
|
|
summary: entry.summaryText,
|
|
coverImageSrc: entry.coverImageSrc,
|
|
status: 'published',
|
|
updatedAt: entry.updatedAt,
|
|
publishedAt: entry.publishedAt,
|
|
publishReady: true,
|
|
levelCount: Number.isNaN(levelCount) ? 0 : levelCount,
|
|
levelMainImageReadyCount: 0,
|
|
levelMotionReadyCount: 0,
|
|
backgroundReady: Boolean(entry.coverImageSrc),
|
|
playCount: entry.playCount ?? 0,
|
|
remixCount: entry.remixCount ?? 0,
|
|
likeCount: entry.likeCount ?? 0,
|
|
};
|
|
}
|
|
|
|
export function mapPublicWorkDetailToSquareHoleWork(
|
|
entry: PlatformPublicGalleryCard,
|
|
): SquareHoleWorkSummary | null {
|
|
if (!isSquareHoleGalleryEntry(entry)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
workId: entry.workId,
|
|
profileId: entry.profileId,
|
|
ownerUserId: entry.ownerUserId,
|
|
sourceSessionId:
|
|
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
|
? entry.sourceSessionId
|
|
: null,
|
|
gameName: entry.worldName,
|
|
themeText: entry.themeTags[0] ?? '方洞挑战',
|
|
twistRule: entry.subtitle,
|
|
summary: entry.summaryText,
|
|
tags: entry.themeTags,
|
|
coverImageSrc: entry.coverImageSrc,
|
|
backgroundPrompt: entry.backgroundPrompt ?? '方洞挑战运行背景',
|
|
backgroundImageSrc: entry.backgroundImageSrc ?? null,
|
|
shapeOptions: entry.shapeOptions ?? [],
|
|
holeOptions: entry.holeOptions ?? [],
|
|
shapeCount: entry.shapeCount ?? 8,
|
|
difficulty: entry.difficulty ?? 4,
|
|
publicationStatus: 'published',
|
|
playCount: entry.playCount ?? 0,
|
|
updatedAt: entry.updatedAt,
|
|
publishedAt: entry.publishedAt,
|
|
publishReady: true,
|
|
};
|
|
}
|
|
|
|
export function getPlatformPublicWorkDetailKind(
|
|
entry: PlatformPublicGalleryCard,
|
|
): PlatformPublicWorkDetailKind {
|
|
if (isBigFishGalleryEntry(entry)) {
|
|
return 'big-fish';
|
|
}
|
|
|
|
if (isPuzzleGalleryEntry(entry)) {
|
|
return 'puzzle';
|
|
}
|
|
|
|
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 resolvePlatformPublicWorkDetailOpenStrategy(
|
|
entry: PlatformPublicGalleryCard,
|
|
): PlatformPublicWorkDetailOpenStrategy {
|
|
if (isBigFishGalleryEntry(entry)) {
|
|
return {
|
|
type: 'use-entry',
|
|
kind: 'big-fish',
|
|
};
|
|
}
|
|
|
|
if (isPuzzleGalleryEntry(entry)) {
|
|
return {
|
|
type: 'load-puzzle-detail',
|
|
profileId: entry.profileId,
|
|
};
|
|
}
|
|
|
|
if (isJumpHopGalleryEntry(entry)) {
|
|
return {
|
|
type: 'load-jump-hop-detail',
|
|
profileId: entry.profileId,
|
|
};
|
|
}
|
|
|
|
if (isWoodenFishGalleryEntry(entry)) {
|
|
return {
|
|
type: 'load-wooden-fish-detail',
|
|
profileId: entry.profileId,
|
|
};
|
|
}
|
|
|
|
if (isMatch3DGalleryEntry(entry)) {
|
|
return {
|
|
type: 'use-entry',
|
|
kind: 'match3d',
|
|
};
|
|
}
|
|
|
|
if (isSquareHoleGalleryEntry(entry)) {
|
|
return {
|
|
type: 'use-entry',
|
|
kind: 'square-hole',
|
|
};
|
|
}
|
|
|
|
if (isVisualNovelGalleryEntry(entry)) {
|
|
return {
|
|
type: 'load-visual-novel-detail',
|
|
profileId: entry.profileId,
|
|
};
|
|
}
|
|
|
|
if (isBarkBattleGalleryEntry(entry)) {
|
|
return {
|
|
type: 'use-entry',
|
|
kind: 'bark-battle',
|
|
};
|
|
}
|
|
|
|
if (isEdutainmentGalleryEntry(entry)) {
|
|
return {
|
|
type: 'use-entry',
|
|
kind: 'edutainment',
|
|
};
|
|
}
|
|
|
|
if (isRpgPublicWorkDetailEntry(entry)) {
|
|
return {
|
|
type: 'load-rpg-detail',
|
|
entry,
|
|
};
|
|
}
|
|
|
|
const exhaustive: never = entry;
|
|
return exhaustive;
|
|
}
|
|
|
|
export function resolvePlatformPublicWorkActionMode(
|
|
entry: PlatformPublicGalleryCard,
|
|
viewerUserId: string | null | undefined,
|
|
): PlatformPublicWorkActionMode {
|
|
return viewerUserId?.trim() && entry.ownerUserId === viewerUserId.trim()
|
|
? 'edit'
|
|
: 'remix';
|
|
}
|
|
|
|
export function resolvePlatformPublicWorkLikeIntent(
|
|
entry: PlatformPublicGalleryCard,
|
|
): PlatformPublicWorkLikeIntent {
|
|
if (isBigFishGalleryEntry(entry)) {
|
|
return {
|
|
type: 'like-big-fish',
|
|
profileId: entry.profileId,
|
|
};
|
|
}
|
|
|
|
if (isPuzzleGalleryEntry(entry)) {
|
|
return {
|
|
type: 'like-puzzle',
|
|
profileId: entry.profileId,
|
|
};
|
|
}
|
|
|
|
if (isEdutainmentGalleryEntry(entry)) {
|
|
return {
|
|
type: 'unsupported',
|
|
errorMessage: '宝贝识物点赞将在后续版本开放。',
|
|
};
|
|
}
|
|
|
|
if (isBarkBattleGalleryEntry(entry)) {
|
|
return {
|
|
type: 'unsupported',
|
|
errorMessage: '汪汪声浪点赞将在后续版本开放。',
|
|
};
|
|
}
|
|
|
|
if (isSquareHoleGalleryEntry(entry)) {
|
|
return {
|
|
type: 'unsupported',
|
|
errorMessage: '方洞挑战点赞将在后续版本开放。',
|
|
};
|
|
}
|
|
|
|
if (isVisualNovelGalleryEntry(entry)) {
|
|
return {
|
|
type: 'unsupported',
|
|
errorMessage: '视觉小说点赞将在后续版本开放。',
|
|
};
|
|
}
|
|
|
|
return {
|
|
type: 'like-rpg-gallery',
|
|
ownerUserId: entry.ownerUserId,
|
|
profileId: entry.profileId,
|
|
};
|
|
}
|
|
|
|
export function resolvePlatformPublicWorkDetailOpenDecision(
|
|
entry: PlatformPublicGalleryCard,
|
|
deps: PlatformPublicWorkDetailOpenDecisionDeps = {},
|
|
): PlatformPublicWorkDetailOpenDecision {
|
|
const canExposeEntry = deps.canExposeEntry ?? canExposePublicWork;
|
|
const hiddenMessage = deps.hiddenMessage ?? EDUTAINMENT_HIDDEN_MESSAGE;
|
|
const buildWorkDetailPath =
|
|
deps.buildWorkDetailPath ??
|
|
((publicWorkCode: string) =>
|
|
buildPublicWorkStagePath('work-detail', publicWorkCode));
|
|
|
|
if (!canExposeEntry(entry)) {
|
|
return {
|
|
type: 'blocked',
|
|
selectedDetail: null,
|
|
errorMessage: hiddenMessage,
|
|
selectionStage: 'platform',
|
|
historyPath: null,
|
|
};
|
|
}
|
|
|
|
const publicWorkCode = entry.publicWorkCode?.trim()
|
|
? entry.publicWorkCode
|
|
: null;
|
|
|
|
return {
|
|
type: 'open',
|
|
selectedDetail: entry,
|
|
errorMessage: null,
|
|
selectionStage: 'work-detail',
|
|
historyPath: publicWorkCode ? buildWorkDetailPath(publicWorkCode) : null,
|
|
};
|
|
}
|
|
|
|
export function resolveActivePlatformPublicWorkAuthorEntry({
|
|
selectionStage,
|
|
selectedPublicWorkDetail,
|
|
selectedRpgDetailEntry,
|
|
}: ActivePlatformPublicWorkAuthorEntryInput): PlatformPublicGalleryCard | null {
|
|
if (selectionStage === 'work-detail') {
|
|
return selectedPublicWorkDetail;
|
|
}
|
|
|
|
if (
|
|
selectionStage === 'detail' &&
|
|
selectedRpgDetailEntry &&
|
|
selectedRpgDetailEntry.visibility !== 'draft'
|
|
) {
|
|
return selectedRpgDetailEntry;
|
|
}
|
|
|
|
return null;
|
|
}
|