import { filterGeneralPublicWorks } from '../platform-entry/platformEdutainmentVisibility'; import { getPlatformPublicGalleryEntryKey } from '../platform-entry/platformPublicGalleryFlow'; import { buildPlatformWorldDisplayTags, isBarkBattleGalleryEntry, isBigFishGalleryEntry, isJumpHopGalleryEntry, isMatch3DGalleryEntry, isPuzzleGalleryEntry, isSquareHoleGalleryEntry, isVisualNovelGalleryEntry, isWoodenFishGalleryEntry, type PlatformPublicGalleryCard, type PlatformWorldCardLike, } from './rpgEntryWorldPresentation'; export type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like'; export type PlatformCategoryKindFilter = | 'all' | 'puzzle' | 'match3d' | 'square-hole' | 'visual-novel' | 'bark-battle' | 'big-fish' | 'jump-hop' | 'wooden-fish' | 'custom-world'; export type PlatformCategorySortMode = 'composite' | 'latest' | 'play' | 'like'; export type PlatformPublicCategoryGroup = { tag: string; entries: PlatformPublicGalleryCard[]; }; export function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) { return getPlatformPublicGalleryEntryKey(entry); } export function buildPublicCategoryGroups( featuredEntries: PlatformPublicGalleryCard[], latestEntries: PlatformPublicGalleryCard[], ): PlatformPublicCategoryGroup[] { const publicEntryMap = new Map(); filterGeneralPublicWorks([...featuredEntries, ...latestEntries]).forEach( (entry) => { publicEntryMap.set(buildPublicGalleryCardKey(entry), entry); }, ); const categoryMap = new Map(); Array.from(publicEntryMap.values()).forEach((entry) => { const tags = buildPlatformWorldDisplayTags(entry, 3); const normalizedTags = tags.length > 0 ? tags : ['回响']; normalizedTags.forEach((tag) => { const entries = categoryMap.get(tag) ?? []; entries.push(entry); categoryMap.set(tag, entries); }); }); return Array.from(categoryMap.entries()) .map(([tag, entries]) => ({ tag, entries })) .sort((left, right) => { if (right.entries.length !== left.entries.length) { return right.entries.length - left.entries.length; } return left.tag.localeCompare(right.tag, 'zh-CN'); }); } export function getPlatformPublicEntries( featuredEntries: PlatformPublicGalleryCard[], latestEntries: PlatformPublicGalleryCard[], ) { const entryMap = new Map(); filterGeneralPublicWorks([...featuredEntries, ...latestEntries]).forEach( (entry) => { entryMap.set(buildPublicGalleryCardKey(entry), entry); }, ); return Array.from(entryMap.values()); } export function getAllPlatformPublicEntries( featuredEntries: PlatformPublicGalleryCard[], latestEntries: PlatformPublicGalleryCard[], ) { const entryMap = new Map(); [...featuredEntries, ...latestEntries].forEach((entry) => { entryMap.set(buildPublicGalleryCardKey(entry), entry); }); return Array.from(entryMap.values()); } function normalizePlatformSearchText(value: string | null | undefined) { return (value ?? '').trim().toLocaleLowerCase('zh-CN'); } function normalizePlatformCompactSearchText(value: string | null | undefined) { return normalizePlatformSearchText(value).replace(/[\s_-]+/gu, ''); } export function getPlatformSearchableWorkIds( entry: PlatformPublicGalleryCard, ) { const ids = [entry.publicWorkCode, entry.profileId]; if ('workId' in entry) { ids.push(entry.workId); } return ids.filter((value): value is string => Boolean(value?.trim())); } function buildPlatformWorkSearchText(entry: PlatformPublicGalleryCard) { return [ ...getPlatformSearchableWorkIds(entry), entry.worldName, entry.authorDisplayName, entry.summaryText, entry.subtitle, ].join(' '); } function matchesPlatformWorkSearch( entry: PlatformPublicGalleryCard, keyword: string, ) { const normalizedKeyword = normalizePlatformSearchText(keyword); const compactKeyword = normalizePlatformCompactSearchText(keyword); if (!normalizedKeyword) { return false; } const normalizedSearchText = normalizePlatformSearchText( buildPlatformWorkSearchText(entry), ); if (normalizedSearchText.includes(normalizedKeyword)) { return true; } return ( Boolean(compactKeyword) && normalizePlatformCompactSearchText( buildPlatformWorkSearchText(entry), ).includes(compactKeyword) ); } export function filterPlatformWorkSearchResults( entries: PlatformPublicGalleryCard[], keyword: string, ) { return entries .filter((entry) => matchesPlatformWorkSearch(entry, keyword)) .sort((left, right) => { const leftCode = getPlatformSearchableWorkIds(left)[0] ?? ''; const rightCode = getPlatformSearchableWorkIds(right)[0] ?? ''; const normalizedKeyword = normalizePlatformSearchText(keyword); const leftNameStarts = normalizePlatformSearchText( left.worldName, ).startsWith(normalizedKeyword); const rightNameStarts = normalizePlatformSearchText( right.worldName, ).startsWith(normalizedKeyword); if (leftNameStarts !== rightNameStarts) { return leftNameStarts ? -1 : 1; } const compactKeyword = normalizePlatformCompactSearchText(keyword); const leftCodeStarts = normalizePlatformCompactSearchText(leftCode).startsWith(compactKeyword); const rightCodeStarts = normalizePlatformCompactSearchText(rightCode).startsWith( compactKeyword, ); if (leftCodeStarts !== rightCodeStarts) { return leftCodeStarts ? -1 : 1; } return getPlatformWorldTimestamp(right) - getPlatformWorldTimestamp(left); }); } export function isExactPublicWorkCodeSearch( entries: PlatformPublicGalleryCard[], keyword: string, ) { const normalizedKeyword = normalizePlatformSearchText(keyword); return entries.some( (entry) => Boolean(entry.publicWorkCode?.trim()) && normalizePlatformSearchText(entry.publicWorkCode) === normalizedKeyword, ); } export function getPlatformWorldTimestamp(entry: PlatformWorldCardLike) { const rawTime = entry.publishedAt ?? entry.updatedAt; return parsePlatformEntryTimestamp(rawTime); } function isSameLocalCalendarDay(left: Date, right: Date) { return ( left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate() ); } function isPlatformEntryPublishedToday( entry: PlatformPublicGalleryCard, now = new Date(), ) { const publishedAtTimestamp = parsePlatformEntryTimestamp(entry.publishedAt); if (publishedAtTimestamp <= 0) { return false; } return isSameLocalCalendarDay(new Date(publishedAtTimestamp), now); } export function filterTodayPublishedEntries( entries: PlatformPublicGalleryCard[], now = new Date(), ) { return entries.filter((entry) => isPlatformEntryPublishedToday(entry, now)); } export function getPlatformWorldLikeCount(entry: PlatformWorldCardLike) { return Math.max(0, Math.round(('likeCount' in entry && entry.likeCount) || 0)); } export function getPlatformWorldPlayCount(entry: PlatformWorldCardLike) { return Math.max(0, Math.round(('playCount' in entry && entry.playCount) || 0)); } export function getPlatformWorldRemixCount(entry: PlatformWorldCardLike) { return Math.max( 0, Math.round(('remixCount' in entry && entry.remixCount) || 0), ); } function getPlatformWorldRecentPlayCount(entry: PlatformWorldCardLike) { return Math.max( 0, Math.round(('recentPlayCount7d' in entry && entry.recentPlayCount7d) || 0), ); } function sortEntriesByMetric( entries: PlatformPublicGalleryCard[], getMetric: (entry: PlatformPublicGalleryCard) => number, ) { return [...entries].sort((left, right) => { const metricDiff = getMetric(right) - getMetric(left); if (metricDiff !== 0) { return metricDiff; } return getPlatformWorldTimestamp(right) - getPlatformWorldTimestamp(left); }); } export function buildPlatformRankingEntries( entries: PlatformPublicGalleryCard[], tab: PlatformRankingTab, ) { if (tab === 'hot') { return sortEntriesByMetric(entries, getPlatformWorldPlayCount); } if (tab === 'remix') { return sortEntriesByMetric(entries, getPlatformWorldRemixCount); } if (tab === 'like') { return sortEntriesByMetric(entries, getPlatformWorldLikeCount); } return sortEntriesByMetric(entries, getPlatformWorldRecentPlayCount); } export function getPlatformRankingMetricValue( entry: PlatformPublicGalleryCard, tab: PlatformRankingTab, ) { if (tab === 'remix') { return getPlatformWorldRemixCount(entry); } if (tab === 'like') { return getPlatformWorldLikeCount(entry); } if (tab === 'new') { return getPlatformWorldRecentPlayCount(entry); } return getPlatformWorldPlayCount(entry); } function getPlatformCategoryCompositeScore(entry: PlatformPublicGalleryCard) { // 分类频道只使用公开读模型已经返回的指标做前端排序,不在 UI 层伪造评分数据。 return ( getPlatformWorldPlayCount(entry) + getPlatformWorldRemixCount(entry) + getPlatformWorldLikeCount(entry) + getPlatformWorldRecentPlayCount(entry) ); } export function getPlatformCategoryKindFilter( entry: PlatformPublicGalleryCard, ): Exclude { if (isPuzzleGalleryEntry(entry)) { return 'puzzle'; } if (isMatch3DGalleryEntry(entry)) { return 'match3d'; } if (isSquareHoleGalleryEntry(entry)) { return 'square-hole'; } if (isVisualNovelGalleryEntry(entry)) { return 'visual-novel'; } if (isBarkBattleGalleryEntry(entry)) { return 'bark-battle'; } if (isBigFishGalleryEntry(entry)) { return 'big-fish'; } if (isJumpHopGalleryEntry(entry)) { return 'jump-hop'; } if (isWoodenFishGalleryEntry(entry)) { return 'wooden-fish'; } return 'custom-world'; } export function matchesPlatformCategoryKindFilter( entry: PlatformPublicGalleryCard, kindFilter: PlatformCategoryKindFilter, ) { return ( kindFilter === 'all' || getPlatformCategoryKindFilter(entry) === kindFilter ); } export function sortPlatformCategoryEntries( entries: PlatformPublicGalleryCard[], sortMode: PlatformCategorySortMode, ) { return [...entries].sort((left, right) => { if (sortMode === 'latest') { return getPlatformWorldTimestamp(right) - getPlatformWorldTimestamp(left); } const metricDiff = sortMode === 'play' ? getPlatformWorldPlayCount(right) - getPlatformWorldPlayCount(left) : sortMode === 'like' ? getPlatformWorldLikeCount(right) - getPlatformWorldLikeCount(left) : getPlatformCategoryCompositeScore(right) - getPlatformCategoryCompositeScore(left); if (metricDiff !== 0) { return metricDiff; } return getPlatformWorldTimestamp(right) - getPlatformWorldTimestamp(left); }); } export function getPlatformCategoryPrimaryMetric( entry: PlatformPublicGalleryCard, ) { const likeCount = getPlatformWorldLikeCount(entry); if (likeCount > 0) { return { label: '点赞', value: likeCount }; } const recentPlayCount = getPlatformWorldRecentPlayCount(entry); if (recentPlayCount > 0) { return { label: '近7日', value: recentPlayCount }; } return { label: '游玩', value: getPlatformWorldPlayCount(entry) }; } export function parsePlatformEntryTimestamp(value: string | null | undefined) { if (!value) { return 0; } 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; return Number.isNaN(timestampMs) ? 0 : timestampMs; } } const timestamp = new Date(normalized).getTime(); return Number.isNaN(timestamp) ? 0 : timestamp; }