Files
Genarrative/src/components/rpg-entry/rpgEntryWorldPresentation.ts
五香丸子 df24467e1d
Some checks failed
CI / verify (push) Has been cancelled
Integrate Match3D Q1 flow
2026-05-01 14:33:18 +08:00

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 '回响';
}
}