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 { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime'; import { buildPublicWorkStagePath } from '../../routing/appPageRoutes'; import { buildBigFishPublicWorkCode, buildMatch3DPublicWorkCode, buildPuzzlePublicWorkCode, buildSquareHolePublicWorkCode, } from '../../services/publicWorkCode'; import type { CustomWorldProfile } from '../../types'; export type CreationWorkShelfKind = | 'rpg' | 'big-fish' | 'match3d' | 'square-hole' | '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: 'square-hole'; item: SquareHoleWorkSummary; } | { 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[]; bigFishItems: BigFishWorkSummary[]; match3dItems?: Match3DWorkSummary[]; squareHoleItems?: SquareHoleWorkSummary[]; puzzleItems: PuzzleWorkSummary[]; canDeleteRpg?: boolean; canDeleteBigFish?: boolean; canDeleteMatch3D?: boolean; canDeleteSquareHole?: boolean; canDeletePuzzle?: boolean; }) { const { rpgItems, rpgLibraryEntries = [], bigFishItems, match3dItems = [], squareHoleItems = [], puzzleItems, canDeleteRpg = false, canDeleteBigFish = false, canDeleteMatch3D = false, canDeleteSquareHole = false, canDeletePuzzle = false, } = params; return [ ...rpgItems.map((item) => mapRpgWorkToShelfItem(item, canDeleteRpg, rpgLibraryEntries), ), ...bigFishItems.map((item) => mapBigFishWorkToShelfItem(item, canDeleteBigFish), ), ...match3dItems.map((item) => mapMatch3DWorkToShelfItem(item, canDeleteMatch3D), ), ...squareHoleItems.map((item) => mapSquareHoleWorkToShelfItem(item, canDeleteSquareHole), ), ...puzzleItems.map((item) => mapPuzzleWorkToShelfItem(item, canDeletePuzzle), ), ].sort( (left, right) => getShelfItemTime(right.updatedAt) - getShelfItemTime(left.updatedAt), ); } function mapRpgWorkToShelfItem( item: CustomWorldWorkSummary, canDelete: boolean, libraryEntries: CustomWorldLibraryEntry[], ): 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 mapSquareHoleWorkToShelfItem( item: SquareHoleWorkSummary, canDelete: boolean, ): CreationWorkShelfItem { const status = item.publicationStatus === 'published' ? 'published' : 'draft'; const publicWorkCode = status === 'published' ? buildSquareHolePublicWorkCode(item.profileId) : null; return { id: item.workId, kind: 'square-hole', 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: 'square-hole', 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; }