469 lines
13 KiB
TypeScript
469 lines
13 KiB
TypeScript
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
|
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
|
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
|
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
|
import type {
|
|
CustomWorldGalleryCard,
|
|
CustomWorldLibraryEntry,
|
|
} from '../../../packages/shared/src/contracts/runtime';
|
|
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
|
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
|
import {
|
|
buildBigFishPublicWorkCode,
|
|
buildMatch3DPublicWorkCode,
|
|
buildPuzzlePublicWorkCode,
|
|
} from '../../services/publicWorkCode';
|
|
import type { CustomWorldProfile } from '../../types';
|
|
|
|
export const PLATFORM_WORK_NAME_DISPLAY_LIMIT = 8;
|
|
export const PLATFORM_WORK_TAG_DISPLAY_LIMIT = 4;
|
|
|
|
export type PlatformWorldCardLike =
|
|
| CustomWorldGalleryCard
|
|
| CustomWorldLibraryEntry<CustomWorldProfile>
|
|
| PlatformBigFishGalleryCard
|
|
| PlatformMatch3DGalleryCard
|
|
| PlatformPuzzleGalleryCard;
|
|
|
|
export type PlatformPuzzleGalleryCard = {
|
|
sourceType: 'puzzle';
|
|
workId: string;
|
|
profileId: string;
|
|
publicWorkCode: string;
|
|
ownerUserId: string;
|
|
authorDisplayName: string;
|
|
worldName: string;
|
|
subtitle: string;
|
|
summaryText: string;
|
|
coverImageSrc: string | null;
|
|
coverSlides?: PlatformPuzzleCoverSlide[];
|
|
themeTags: string[];
|
|
playCount?: number;
|
|
remixCount?: number;
|
|
likeCount?: number;
|
|
recentPlayCount7d?: number;
|
|
visibility: 'published';
|
|
publishedAt: string | null;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type PlatformPuzzleCoverSlide = {
|
|
id: string;
|
|
imageSrc: string;
|
|
label: string;
|
|
};
|
|
|
|
export type PlatformBigFishGalleryCard = {
|
|
sourceType: 'big-fish';
|
|
workId: string;
|
|
profileId: string;
|
|
publicWorkCode: string;
|
|
ownerUserId: string;
|
|
authorDisplayName: string;
|
|
worldName: string;
|
|
subtitle: string;
|
|
summaryText: string;
|
|
coverImageSrc: string | null;
|
|
themeTags: string[];
|
|
playCount?: number;
|
|
remixCount?: number;
|
|
likeCount?: number;
|
|
recentPlayCount7d?: number;
|
|
visibility: 'published';
|
|
publishedAt: string | null;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type PlatformMatch3DGalleryCard = {
|
|
sourceType: 'match3d';
|
|
workId: string;
|
|
profileId: string;
|
|
publicWorkCode: string;
|
|
ownerUserId: string;
|
|
authorDisplayName: string;
|
|
worldName: string;
|
|
subtitle: string;
|
|
summaryText: string;
|
|
coverImageSrc: string | null;
|
|
themeTags: string[];
|
|
playCount?: number;
|
|
remixCount?: number;
|
|
likeCount?: number;
|
|
recentPlayCount7d?: number;
|
|
visibility: 'published';
|
|
publishedAt: string | null;
|
|
updatedAt: string;
|
|
};
|
|
|
|
export type PlatformPublicGalleryCard =
|
|
| CustomWorldGalleryCard
|
|
| PlatformBigFishGalleryCard
|
|
| PlatformMatch3DGalleryCard
|
|
| PlatformPuzzleGalleryCard;
|
|
|
|
export function isLibraryWorldEntry(
|
|
entry: PlatformWorldCardLike,
|
|
): entry is CustomWorldLibraryEntry<CustomWorldProfile> {
|
|
return 'profile' in entry;
|
|
}
|
|
|
|
export function isPuzzleGalleryEntry(
|
|
entry: PlatformWorldCardLike,
|
|
): entry is PlatformPuzzleGalleryCard {
|
|
return 'sourceType' in entry && entry.sourceType === 'puzzle';
|
|
}
|
|
|
|
export function isBigFishGalleryEntry(
|
|
entry: PlatformWorldCardLike,
|
|
): entry is PlatformBigFishGalleryCard {
|
|
return 'sourceType' in entry && entry.sourceType === 'big-fish';
|
|
}
|
|
|
|
export function isMatch3DGalleryEntry(
|
|
entry: PlatformWorldCardLike,
|
|
): entry is PlatformMatch3DGalleryCard {
|
|
return 'sourceType' in entry && entry.sourceType === 'match3d';
|
|
}
|
|
|
|
export function mapPuzzleWorkToPlatformGalleryCard(
|
|
work: PuzzleWorkSummary,
|
|
): PlatformPuzzleGalleryCard {
|
|
return {
|
|
sourceType: 'puzzle',
|
|
workId: work.workId,
|
|
profileId: work.profileId,
|
|
publicWorkCode: buildPuzzlePublicWorkCode(work.profileId),
|
|
ownerUserId: work.ownerUserId,
|
|
authorDisplayName: work.authorDisplayName,
|
|
worldName: work.workTitle || work.levelName,
|
|
subtitle: '拼图关卡',
|
|
summaryText: work.workDescription || work.summary,
|
|
coverImageSrc: work.coverImageSrc,
|
|
coverSlides: buildPuzzleWorkCoverSlides(work),
|
|
themeTags: work.themeTags,
|
|
playCount: work.playCount ?? 0,
|
|
remixCount: work.remixCount ?? 0,
|
|
likeCount: work.likeCount ?? 0,
|
|
recentPlayCount7d: work.recentPlayCount7d ?? 0,
|
|
visibility: 'published',
|
|
publishedAt: work.publishedAt,
|
|
updatedAt: work.updatedAt,
|
|
};
|
|
}
|
|
|
|
export function mapMatch3DWorkToPlatformGalleryCard(
|
|
work: Match3DWorkSummary,
|
|
): PlatformMatch3DGalleryCard {
|
|
return {
|
|
sourceType: 'match3d',
|
|
workId: work.workId,
|
|
profileId: work.profileId,
|
|
publicWorkCode: buildMatch3DPublicWorkCode(work.profileId),
|
|
ownerUserId: work.ownerUserId,
|
|
authorDisplayName: '玩家',
|
|
worldName: work.gameName,
|
|
subtitle: '经典消除玩法',
|
|
summaryText: work.summary,
|
|
coverImageSrc: work.coverImageSrc ?? null,
|
|
themeTags: work.tags.length > 0 ? work.tags : [work.themeText, '抓大鹅'],
|
|
playCount: work.playCount ?? 0,
|
|
remixCount: 0,
|
|
likeCount: 0,
|
|
recentPlayCount7d: 0,
|
|
visibility: 'published',
|
|
publishedAt: work.publishedAt ?? null,
|
|
updatedAt: work.updatedAt,
|
|
};
|
|
}
|
|
|
|
export function mapBigFishWorkToPlatformGalleryCard(
|
|
work: BigFishWorkSummary,
|
|
): PlatformBigFishGalleryCard {
|
|
return {
|
|
sourceType: 'big-fish',
|
|
workId: work.workId,
|
|
profileId: work.sourceSessionId,
|
|
publicWorkCode: buildBigFishPublicWorkCode(work.sourceSessionId),
|
|
ownerUserId: work.ownerUserId,
|
|
authorDisplayName: work.authorDisplayName,
|
|
worldName: work.title,
|
|
subtitle: work.subtitle || '大鱼吃小鱼',
|
|
summaryText: work.summary,
|
|
coverImageSrc: work.coverImageSrc,
|
|
themeTags: ['大鱼', `${work.levelCount}级`],
|
|
playCount: work.playCount ?? 0,
|
|
remixCount: work.remixCount ?? 0,
|
|
likeCount: work.likeCount ?? 0,
|
|
recentPlayCount7d: work.recentPlayCount7d ?? 0,
|
|
visibility: 'published',
|
|
publishedAt: work.publishedAt ?? work.updatedAt,
|
|
updatedAt: work.updatedAt,
|
|
};
|
|
}
|
|
|
|
export function resolvePlatformWorldStats(entry: PlatformWorldCardLike) {
|
|
return {
|
|
playCount: 'playCount' in entry ? (entry.playCount ?? 0) : 0,
|
|
remixCount: 'remixCount' in entry ? (entry.remixCount ?? 0) : 0,
|
|
likeCount: 'likeCount' in entry ? (entry.likeCount ?? 0) : 0,
|
|
recentPlayCount7d:
|
|
'recentPlayCount7d' in entry ? (entry.recentPlayCount7d ?? 0) : 0,
|
|
publishedAt: entry.publishedAt ?? null,
|
|
updatedAt: entry.updatedAt ?? null,
|
|
};
|
|
}
|
|
|
|
export function resolvePlatformWorldCoverImage(entry: PlatformWorldCardLike) {
|
|
if (entry.coverImageSrc) {
|
|
return entry.coverImageSrc;
|
|
}
|
|
|
|
if (isLibraryWorldEntry(entry)) {
|
|
return resolveCustomWorldCampSceneImage(entry.profile) ?? '';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
export function resolvePlatformWorldCoverSlides(
|
|
entry: PlatformWorldCardLike,
|
|
): PlatformPuzzleCoverSlide[] {
|
|
const fallbackCoverImage = resolvePlatformWorldCoverImage(entry).trim();
|
|
const puzzleCoverSlides = isPuzzleGalleryEntry(entry)
|
|
? (entry.coverSlides ?? [])
|
|
: [];
|
|
const normalizedSlides = puzzleCoverSlides
|
|
.map((slide, index) => ({
|
|
id: slide.id.trim() || `cover-${index + 1}`,
|
|
imageSrc: slide.imageSrc.trim(),
|
|
label: slide.label.trim() || entry.worldName,
|
|
}))
|
|
.filter((slide) => Boolean(slide.imageSrc));
|
|
|
|
if (normalizedSlides.length > 0) {
|
|
return normalizedSlides;
|
|
}
|
|
|
|
return fallbackCoverImage
|
|
? [
|
|
{
|
|
id: 'cover',
|
|
imageSrc: fallbackCoverImage,
|
|
label: entry.worldName,
|
|
},
|
|
]
|
|
: [];
|
|
}
|
|
|
|
export function resolvePuzzleLevelFormalImageSrc(level: PuzzleDraftLevel) {
|
|
const selectedCandidate =
|
|
level.candidates.find(
|
|
(candidate) =>
|
|
candidate.selected ||
|
|
(level.selectedCandidateId
|
|
? candidate.candidateId === level.selectedCandidateId
|
|
: false),
|
|
) ??
|
|
level.candidates[level.candidates.length - 1] ??
|
|
null;
|
|
|
|
return (
|
|
selectedCandidate?.imageSrc?.trim() || level.coverImageSrc?.trim() || ''
|
|
);
|
|
}
|
|
|
|
export function buildPuzzleWorkCoverSlides(
|
|
work: PuzzleWorkSummary,
|
|
): PlatformPuzzleCoverSlide[] {
|
|
const slides: PlatformPuzzleCoverSlide[] = [];
|
|
const usedImageSrcSet = new Set<string>();
|
|
|
|
work.levels?.forEach((level, index) => {
|
|
const imageSrc = resolvePuzzleLevelFormalImageSrc(level);
|
|
if (!imageSrc || usedImageSrcSet.has(imageSrc)) {
|
|
return;
|
|
}
|
|
|
|
usedImageSrcSet.add(imageSrc);
|
|
slides.push({
|
|
id: level.levelId?.trim() || `puzzle-level-${index + 1}`,
|
|
imageSrc,
|
|
label: level.levelName?.trim() || `第 ${index + 1} 关`,
|
|
});
|
|
});
|
|
|
|
if (slides.length > 0) {
|
|
return slides;
|
|
}
|
|
|
|
const fallbackImageSrc = work.coverImageSrc?.trim() ?? '';
|
|
return fallbackImageSrc
|
|
? [
|
|
{
|
|
id: 'cover',
|
|
imageSrc: fallbackImageSrc,
|
|
label: work.levelName,
|
|
},
|
|
]
|
|
: [];
|
|
}
|
|
|
|
export function resolvePlatformWorldLeadPortrait(entry: PlatformWorldCardLike) {
|
|
if (!isLibraryWorldEntry(entry)) {
|
|
return '';
|
|
}
|
|
|
|
return buildCustomWorldPlayableCharacters(entry.profile)[0]?.portrait ?? '';
|
|
}
|
|
|
|
function limitPlatformDisplayText(value: string, maxLength: number) {
|
|
const normalized = value.trim();
|
|
const chars = Array.from(normalized);
|
|
if (chars.length <= maxLength) {
|
|
return normalized;
|
|
}
|
|
|
|
return chars.slice(0, maxLength).join('');
|
|
}
|
|
|
|
export function formatPlatformWorkDisplayName(value: string) {
|
|
return limitPlatformDisplayText(value, PLATFORM_WORK_NAME_DISPLAY_LIMIT);
|
|
}
|
|
|
|
export function formatPlatformWorkDisplayTag(value: string) {
|
|
return limitPlatformDisplayText(value, PLATFORM_WORK_TAG_DISPLAY_LIMIT);
|
|
}
|
|
|
|
export function formatPlatformWorkDisplayTags(
|
|
tags: string[],
|
|
limit = tags.length,
|
|
) {
|
|
return [
|
|
...new Set(
|
|
tags
|
|
.map((tag) => formatPlatformWorkDisplayTag(tag))
|
|
.filter(Boolean),
|
|
),
|
|
].slice(0, limit);
|
|
}
|
|
|
|
export function buildPlatformWorldDisplayTags(
|
|
entry: PlatformWorldCardLike,
|
|
limit = 3,
|
|
) {
|
|
return formatPlatformWorkDisplayTags(buildPlatformWorldTags(entry), limit);
|
|
}
|
|
|
|
export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
|
|
if (isBigFishGalleryEntry(entry)) {
|
|
return entry.themeTags.length > 0 ? entry.themeTags.slice(0, 3) : ['大鱼'];
|
|
}
|
|
|
|
if (isPuzzleGalleryEntry(entry)) {
|
|
return entry.themeTags.length > 0 ? entry.themeTags.slice(0, 3) : ['拼图'];
|
|
}
|
|
|
|
if (isMatch3DGalleryEntry(entry)) {
|
|
return entry.themeTags.length > 0 ? entry.themeTags.slice(0, 3) : ['抓大鹅'];
|
|
}
|
|
|
|
if (!isLibraryWorldEntry(entry)) {
|
|
return [
|
|
describePlatformThemeLabel(entry.themeMode),
|
|
`${entry.playableNpcCount} 角色`,
|
|
`${entry.landmarkCount} 地标`,
|
|
];
|
|
}
|
|
|
|
return [
|
|
...entry.profile.majorFactions.slice(0, 2),
|
|
...entry.profile.coreConflicts.slice(0, 1),
|
|
]
|
|
.map((value) => value.trim())
|
|
.filter(Boolean)
|
|
.slice(0, 3);
|
|
}
|
|
|
|
function parsePlatformWorldDate(value: string) {
|
|
const normalized = value.trim();
|
|
const numericTimestamp = normalized.match(/^(-?\d+(?:\.\d+)?)(?:Z)?$/u);
|
|
if (numericTimestamp?.[1]) {
|
|
const rawTimestamp = Number(numericTimestamp[1]);
|
|
if (Number.isFinite(rawTimestamp)) {
|
|
const absoluteTimestamp = Math.abs(rawTimestamp);
|
|
const timestampMs =
|
|
absoluteTimestamp >= 1_000_000_000_000_000
|
|
? rawTimestamp / 1000
|
|
: absoluteTimestamp >= 1_000_000_000_000
|
|
? rawTimestamp
|
|
: absoluteTimestamp >= 1_000_000_000
|
|
? rawTimestamp * 1000
|
|
: Number.NaN;
|
|
const date = new Date(timestampMs);
|
|
if (!Number.isNaN(date.getTime())) {
|
|
return date;
|
|
}
|
|
}
|
|
}
|
|
|
|
const date = new Date(normalized);
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
}
|
|
|
|
function formatPlatformDateOnly(date: Date) {
|
|
const year = date.getUTCFullYear();
|
|
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
export function formatPlatformWorldTime(value: string | null) {
|
|
if (!value) {
|
|
return '未发布';
|
|
}
|
|
|
|
const date = parsePlatformWorldDate(value);
|
|
if (!date) {
|
|
return value;
|
|
}
|
|
|
|
return formatPlatformDateOnly(date);
|
|
}
|
|
|
|
export function resolvePlatformPublicWorkCode(
|
|
entry: PlatformWorldCardLike,
|
|
): string | null {
|
|
if (isBigFishGalleryEntry(entry)) {
|
|
return entry.publicWorkCode;
|
|
}
|
|
|
|
if (isPuzzleGalleryEntry(entry)) {
|
|
return entry.publicWorkCode;
|
|
}
|
|
|
|
if (isMatch3DGalleryEntry(entry)) {
|
|
return entry.publicWorkCode;
|
|
}
|
|
|
|
return entry.publicWorkCode;
|
|
}
|
|
|
|
export function describePlatformThemeLabel(
|
|
themeMode: CustomWorldGalleryCard['themeMode'],
|
|
) {
|
|
switch (themeMode) {
|
|
case 'martial':
|
|
return '江湖';
|
|
case 'arcane':
|
|
return '灵脉';
|
|
case 'machina':
|
|
return '机巧';
|
|
case 'tide':
|
|
return '潮痕';
|
|
case 'rift':
|
|
return '裂界';
|
|
default:
|
|
return '回响';
|
|
}
|
|
}
|