Files
Genarrative/src/components/platform-entry/platformPublicWorkDetailFlow.ts
T

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;
}