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

398 lines
11 KiB
TypeScript

import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
import {
buildBigFishPublicWorkCode,
buildMatch3DPublicWorkCode,
buildPuzzlePublicWorkCode,
} from '../../services/publicWorkCode';
import type { CustomWorldProfile } from '../../types';
export type CreationWorkShelfKind = 'rpg' | 'big-fish' | 'match3d' | 'puzzle';
export type CreationWorkShelfStatus = 'draft' | 'published';
export type CreationWorkShelfBadgeTone = 'warm' | 'success' | 'neutral';
export type CreationWorkShelfBadge = {
id: string;
label: string;
tone: CreationWorkShelfBadgeTone;
};
export type CreationWorkShelfMetricId =
| 'play-count'
| 'remix-count'
| 'like-count';
export type CreationWorkShelfMetricTone = 'play' | 'remix' | 'like';
export type CreationWorkShelfMetric = {
id: CreationWorkShelfMetricId;
label: string;
value: number;
unit: string;
tone: CreationWorkShelfMetricTone;
};
export type CreationWorkShelfPointIncentive = {
totalHalfPoints: number;
totalPoints: number;
claimablePoints: number;
};
export type CreationWorkShelfSource =
| {
kind: 'rpg';
item: CustomWorldWorkSummary;
}
| {
kind: 'big-fish';
item: BigFishWorkSummary;
}
| {
kind: 'match3d';
item: Match3DWorkSummary;
}
| {
kind: 'puzzle';
item: PuzzleWorkSummary;
};
export type CreationWorkShelfItem = {
id: string;
kind: CreationWorkShelfKind;
status: CreationWorkShelfStatus;
title: string;
summary: string;
updatedAt: string;
coverImageSrc: string | null;
coverRenderMode: 'image' | 'scene_with_roles';
coverCharacterImageSrcs: string[];
publicWorkCode: string | null;
sharePath: string | null;
openActionLabel: string;
canDelete: boolean;
canShare: boolean;
badges: CreationWorkShelfBadge[];
metrics: CreationWorkShelfMetric[];
pointIncentive?: CreationWorkShelfPointIncentive;
source: CreationWorkShelfSource;
};
export function buildCreationWorkShelfItems(params: {
rpgItems: CustomWorldWorkSummary[];
rpgLibraryEntries?: CustomWorldLibraryEntry<CustomWorldProfile>[];
bigFishItems: BigFishWorkSummary[];
match3dItems?: Match3DWorkSummary[];
puzzleItems: PuzzleWorkSummary[];
canDeleteRpg?: boolean;
canDeleteBigFish?: boolean;
canDeleteMatch3D?: boolean;
canDeletePuzzle?: boolean;
}) {
const {
rpgItems,
rpgLibraryEntries = [],
bigFishItems,
match3dItems = [],
puzzleItems,
canDeleteRpg = false,
canDeleteBigFish = false,
canDeleteMatch3D = false,
canDeletePuzzle = false,
} = params;
return [
...rpgItems.map((item) =>
mapRpgWorkToShelfItem(item, canDeleteRpg, rpgLibraryEntries),
),
...bigFishItems.map((item) =>
mapBigFishWorkToShelfItem(item, canDeleteBigFish),
),
...match3dItems.map((item) =>
mapMatch3DWorkToShelfItem(item, canDeleteMatch3D),
),
...puzzleItems.map((item) =>
mapPuzzleWorkToShelfItem(item, canDeletePuzzle),
),
].sort(
(left, right) =>
getShelfItemTime(right.updatedAt) - getShelfItemTime(left.updatedAt),
);
}
function mapRpgWorkToShelfItem(
item: CustomWorldWorkSummary,
canDelete: boolean,
libraryEntries: CustomWorldLibraryEntry<CustomWorldProfile>[],
): CreationWorkShelfItem {
const isDraft = item.status === 'draft';
const libraryEntry = item.profileId
? libraryEntries.find((entry) => entry.profileId === item.profileId)
: null;
const publicWorkCode =
item.status === 'published' ? (libraryEntry?.publicWorkCode ?? null) : null;
const badges: CreationWorkShelfBadge[] = [
buildStatusBadge(item.status),
{ id: 'type', label: 'RPG', tone: 'neutral' },
];
const metrics = buildPublishedMetrics({
playCount: libraryEntry?.playCount,
remixCount: libraryEntry?.remixCount,
likeCount: libraryEntry?.likeCount,
});
return {
id: item.workId,
kind: 'rpg',
status: item.status,
title: item.title,
summary: item.summary,
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: item.coverRenderMode ?? 'image',
coverCharacterImageSrcs: item.coverCharacterImageSrcs ?? [],
publicWorkCode,
sharePath:
publicWorkCode && item.status === 'published'
? buildPublicWorkStagePath('work-detail', publicWorkCode)
: null,
openActionLabel: isDraft
? item.playableNpcCount > 0 || item.landmarkCount > 0
? '继续完善'
: '继续创作'
: '查看详情',
canDelete,
canShare: item.status === 'published' && Boolean(publicWorkCode),
badges,
metrics: isDraft ? [] : metrics,
source: { kind: 'rpg', item },
};
}
function mapBigFishWorkToShelfItem(
item: BigFishWorkSummary,
canDelete: boolean,
): CreationWorkShelfItem {
const isPublished = item.status === 'published';
const publicWorkCode = isPublished
? buildBigFishPublicWorkCode(item.sourceSessionId)
: null;
return {
id: item.workId,
kind: 'big-fish',
status: item.status,
title: item.title,
summary: item.summary,
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode,
sharePath:
publicWorkCode && isPublished
? buildPublicWorkStagePath('big-fish-runtime', publicWorkCode)
: null,
openActionLabel: item.status === 'draft' ? '继续创作' : '查看详情',
canDelete,
canShare: isPublished && Boolean(publicWorkCode),
badges: [
buildStatusBadge(item.status),
{ id: 'type', label: '大鱼', tone: 'neutral' },
],
metrics: isPublished
? buildPublishedMetrics({
playCount: item.playCount,
remixCount: item.remixCount,
likeCount: item.likeCount,
})
: [],
source: { kind: 'big-fish', item },
};
}
function mapMatch3DWorkToShelfItem(
item: Match3DWorkSummary,
canDelete: boolean,
): CreationWorkShelfItem {
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
const publicWorkCode =
status === 'published' ? buildMatch3DPublicWorkCode(item.profileId) : null;
return {
id: item.workId,
kind: 'match3d',
status,
title: item.gameName,
summary: item.summary,
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode,
sharePath:
publicWorkCode && status === 'published'
? buildPublicWorkStagePath('work-detail', publicWorkCode)
: null,
openActionLabel: status === 'published' ? '查看详情' : '继续创作',
canDelete,
canShare: status === 'published' && Boolean(publicWorkCode),
badges: [
buildStatusBadge(status),
{ id: 'type', label: '抓鹅', tone: 'neutral' },
],
metrics:
status === 'published'
? buildPublishedMetrics({
playCount: item.playCount,
remixCount: 0,
likeCount: 0,
})
: [],
source: { kind: 'match3d', item },
};
}
function mapPuzzleWorkToShelfItem(
item: PuzzleWorkSummary,
canDelete: boolean,
): CreationWorkShelfItem {
const status = item.publicationStatus;
const publicWorkCode =
status === 'published' ? buildPuzzlePublicWorkCode(item.profileId) : null;
return {
id: item.workId,
kind: 'puzzle',
status,
title: item.workTitle?.trim() || item.levelName.trim() || '未命名拼图',
summary:
item.workDescription?.trim() ||
item.summary.trim() ||
(status === 'draft' ? '未填写作品描述' : ''),
updatedAt: item.updatedAt,
coverImageSrc: item.coverImageSrc ?? null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode,
sharePath:
publicWorkCode && status === 'published'
? buildPublicWorkStagePath('puzzle-gallery-detail', publicWorkCode)
: null,
openActionLabel:
status === 'published' && !item.sourceSessionId ? '查看详情' : '继续创作',
canDelete,
canShare: status === 'published' && Boolean(publicWorkCode),
badges: [
buildStatusBadge(status),
{ id: 'type', label: '拼图', tone: 'neutral' },
],
metrics:
status === 'published'
? buildPublishedMetrics({
playCount: item.playCount,
remixCount: item.remixCount,
likeCount: item.likeCount,
})
: [],
pointIncentive:
status === 'published'
? {
totalHalfPoints: normalizeMetricCount(
item.pointIncentiveTotalHalfPoints,
),
totalPoints: normalizePointIncentiveTotal(
item.pointIncentiveTotalPoints,
item.pointIncentiveTotalHalfPoints,
),
claimablePoints: normalizeMetricCount(
item.pointIncentiveClaimablePoints,
),
}
: undefined,
source: { kind: 'puzzle', item },
};
}
function buildPublishedMetrics(params: {
playCount?: number | null;
remixCount?: number | null;
likeCount?: number | null;
}): CreationWorkShelfMetric[] {
return [
{
id: 'play-count',
label: '游玩',
value: normalizeMetricCount(params.playCount),
unit: '次',
tone: 'play',
},
{
id: 'remix-count',
label: '改造',
value: normalizeMetricCount(params.remixCount),
unit: '次',
tone: 'remix',
},
{
id: 'like-count',
label: '点赞',
value: normalizeMetricCount(params.likeCount),
unit: '赞',
tone: 'like',
},
];
}
export function normalizeMetricCount(value?: number | null) {
return Math.max(0, Math.floor(value ?? 0));
}
export function formatCreationMetricCount(value?: number | null) {
const normalized = Math.max(0, Math.floor(value ?? 0));
if (normalized >= 10000) {
const wanValue = normalized / 10000;
return `${Number.isInteger(wanValue) ? wanValue.toFixed(0) : wanValue.toFixed(1)}`;
}
return `${normalized}`;
}
export function formatCreationPointIncentiveTotal(value?: number | null) {
const normalized = Math.max(0, value ?? 0);
return Number.isInteger(normalized)
? normalized.toFixed(0)
: normalized.toFixed(1);
}
function normalizePointIncentiveTotal(
totalPoints?: number | null,
totalHalfPoints?: number | null,
) {
if (Number.isFinite(totalPoints)) {
return Math.max(0, totalPoints ?? 0);
}
return normalizeMetricCount(totalHalfPoints) / 2;
}
function buildStatusBadge(
status: CreationWorkShelfStatus,
): CreationWorkShelfBadge {
return {
id: 'status',
label: status === 'draft' ? '草稿' : '已发布',
tone: status === 'draft' ? 'warm' : 'success',
};
}
function getShelfItemTime(value: string) {
const timestamp = new Date(value).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}