feat: add puzzle clear template runtime

This commit is contained in:
2026-06-03 22:11:46 +08:00
parent 6e74cf5add
commit 1b5e098225
148 changed files with 19588 additions and 241 deletions

View File

@@ -143,6 +143,7 @@ import {
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isMatch3DGalleryEntry,
isPuzzleClearGalleryEntry,
isPuzzleGalleryEntry,
isSquareHoleGalleryEntry,
isVisualNovelGalleryEntry,
@@ -1908,6 +1909,9 @@ function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
if (isPuzzleGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('拼图');
}
if (isPuzzleClearGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('拼消消');
}
if (isMatch3DGalleryEntry(entry)) {
return formatPlatformWorkDisplayTag('抓大鹅');
}

View File

@@ -10,10 +10,12 @@ import {
formatPlatformWorldTime,
isBarkBattleGalleryEntry,
isEdutainmentGalleryEntry,
isPuzzleClearGalleryEntry,
isVisualNovelGalleryEntry,
isWoodenFishGalleryEntry,
mapBabyObjectMatchDraftToPlatformGalleryCard,
mapBarkBattleWorkToPlatformGalleryCard,
mapPuzzleClearWorkToPlatformGalleryCard,
mapVisualNovelWorkToPlatformGalleryCard,
mapWoodenFishWorkToPlatformGalleryCard,
type PlatformEdutainmentGalleryCard,
@@ -198,6 +200,35 @@ test('maps wooden fish work to platform gallery card with WF public code', () =>
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual(['敲木鱼']);
});
test('maps puzzle clear work to platform gallery card with PC public code', () => {
const card = mapPuzzleClearWorkToPlatformGalleryCard({
runtimeKind: 'puzzle-clear',
workId: 'puzzle-clear-work-1',
profileId: 'puzzle-clear-profile-12345678',
ownerUserId: 'user-1',
sourceSessionId: 'puzzle-clear-session-1',
workTitle: '星港拼消消',
workDescription: '霓虹星港主题。',
themePrompt: '霓虹星港',
coverImageSrc: '/generated-puzzle-clear-assets/profile/atlas.png',
publicationStatus: 'published',
playCount: 6,
updatedAt: '2026-05-30T00:00:00.000Z',
publishedAt: '2026-05-30T00:00:00.000Z',
publishReady: true,
generationStatus: 'ready',
});
expect(isPuzzleClearGalleryEntry(card)).toBe(true);
expect(card.sourceType).toBe('puzzle-clear');
expect(card.publicWorkCode).toBe('PC-12345678');
expect(resolvePlatformPublicWorkCode(card)).toBe('PC-12345678');
expect(resolvePlatformWorldFallbackCoverImage(card)).toBe(
'/creation-type-references/puzzle.webp',
);
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual(['拼消消', '霓虹星港']);
});
test('resolves public work author from display name and public user code before stored author name', () => {
const card = mapWoodenFishWorkToPlatformGalleryCard({
publicWorkCode: 'WF-AUTHOR1',

View File

@@ -7,6 +7,11 @@ import type {
JumpHopGalleryCardResponse,
JumpHopWorkProfileResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import type {
PuzzleClearGalleryCardResponse,
PuzzleClearWorkProfileResponse,
PuzzleClearWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/puzzleClear';
import type {
Match3DGeneratedBackgroundAsset,
Match3DGeneratedItemAsset,
@@ -36,6 +41,7 @@ import {
buildBigFishPublicWorkCode,
buildJumpHopPublicWorkCode,
buildMatch3DPublicWorkCode,
buildPuzzleClearPublicWorkCode,
buildPuzzlePublicWorkCode,
buildSquareHolePublicWorkCode,
buildVisualNovelPublicWorkCode,
@@ -56,6 +62,7 @@ export type PlatformWorldCardLike =
| PlatformMatch3DGalleryCard
| PlatformSquareHoleGalleryCard
| PlatformPuzzleGalleryCard
| PlatformPuzzleClearGalleryCard
| PlatformJumpHopGalleryCard
| PlatformWoodenFishGalleryCard
| PlatformVisualNovelGalleryCard
@@ -213,6 +220,29 @@ export type PlatformJumpHopGalleryCard = {
stylePreset?: string;
};
export type PlatformPuzzleClearGalleryCard = {
sourceType: 'puzzle-clear';
workId: string;
profileId: string;
sourceSessionId?: string | null;
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;
themePrompt: string;
};
export type PlatformWoodenFishGalleryCard = {
sourceType: 'wooden-fish';
workId: string;
@@ -294,6 +324,7 @@ export type PlatformPublicGalleryCard =
| PlatformMatch3DGalleryCard
| PlatformSquareHoleGalleryCard
| PlatformPuzzleGalleryCard
| PlatformPuzzleClearGalleryCard
| PlatformJumpHopGalleryCard
| PlatformWoodenFishGalleryCard
| PlatformVisualNovelGalleryCard
@@ -342,6 +373,12 @@ export function isJumpHopGalleryEntry(
return 'sourceType' in entry && entry.sourceType === 'jump-hop';
}
export function isPuzzleClearGalleryEntry(
entry: PlatformWorldCardLike,
): entry is PlatformPuzzleClearGalleryCard {
return 'sourceType' in entry && entry.sourceType === 'puzzle-clear';
}
export function isWoodenFishGalleryEntry(
entry: PlatformWorldCardLike,
): entry is PlatformWoodenFishGalleryCard {
@@ -548,6 +585,68 @@ export function mapJumpHopWorkToPlatformGalleryCard(
};
}
function normalizePuzzleClearThemeTags(summary: PuzzleClearWorkSummaryResponse) {
const themePrompt = summary.themePrompt.trim();
return [
'拼消消',
...(themePrompt ? [themePrompt] : []),
];
}
function getPuzzleClearRecentPlayCount(
summary: PuzzleClearGalleryCardResponse | PuzzleClearWorkSummaryResponse,
) {
if (!('recentPlayCount7d' in summary)) {
return 0;
}
return typeof summary.recentPlayCount7d === 'number'
? summary.recentPlayCount7d
: 0;
}
export function mapPuzzleClearWorkToPlatformGalleryCard(
work:
| PuzzleClearGalleryCardResponse
| PuzzleClearWorkSummaryResponse
| PuzzleClearWorkProfileResponse,
): PlatformPuzzleClearGalleryCard {
const summary = 'summary' in work ? work.summary : work;
return {
sourceType: 'puzzle-clear',
workId: summary.workId,
profileId: summary.profileId,
sourceSessionId:
'sourceSessionId' in summary ? (summary.sourceSessionId ?? null) : null,
publicWorkCode:
'publicWorkCode' in summary &&
typeof summary.publicWorkCode === 'string' &&
summary.publicWorkCode.trim()
? summary.publicWorkCode
: buildPuzzleClearPublicWorkCode(summary.profileId),
ownerUserId: summary.ownerUserId,
authorDisplayName:
'authorDisplayName' in summary &&
typeof summary.authorDisplayName === 'string' &&
summary.authorDisplayName.trim()
? summary.authorDisplayName
: '玩家',
worldName: summary.workTitle.trim() || '拼消消',
subtitle: '拼消消',
summaryText: summary.workDescription,
coverImageSrc: summary.coverImageSrc ?? null,
themeTags: normalizePuzzleClearThemeTags(summary),
playCount: summary.playCount ?? 0,
remixCount: 0,
likeCount: 0,
recentPlayCount7d: getPuzzleClearRecentPlayCount(summary),
visibility: 'published',
publishedAt: summary.publishedAt ?? null,
updatedAt: summary.updatedAt,
themePrompt: summary.themePrompt,
};
}
export function mapWoodenFishWorkToPlatformGalleryCard(
work: WoodenFishGalleryCardResponse | WoodenFishWorkProfileResponse,
): PlatformWoodenFishGalleryCard {
@@ -708,6 +807,10 @@ export function resolvePlatformWorldFallbackCoverImage(
return '/creation-type-references/puzzle.webp';
}
if (isPuzzleClearGalleryEntry(entry)) {
return '/creation-type-references/puzzle.webp';
}
if (isMatch3DGalleryEntry(entry)) {
return '/creation-type-references/match3d.webp';
}
@@ -869,6 +972,9 @@ export function resolvePlatformWorkAuthorDisplayName(
) {
const displayName = authorSummary?.displayName?.trim();
const publicUserCode = authorSummary?.publicUserCode?.trim();
if (displayName && publicUserCode) {
return `${displayName} · ${publicUserCode}`;
}
return displayName || publicUserCode || entry.authorDisplayName.trim() || '玩家';
}
@@ -889,6 +995,12 @@ export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
return entry.themeTags.length > 0 ? entry.themeTags.slice(0, 3) : ['拼图'];
}
if (isPuzzleClearGalleryEntry(entry)) {
return entry.themeTags.length > 0
? entry.themeTags.slice(0, 3)
: ['拼消消'];
}
if (isMatch3DGalleryEntry(entry)) {
return entry.themeTags.length > 0
? entry.themeTags.slice(0, 3)
@@ -1003,6 +1115,10 @@ export function resolvePlatformPublicWorkCode(
return entry.publicWorkCode;
}
if (isPuzzleClearGalleryEntry(entry)) {
return entry.publicWorkCode;
}
if (isMatch3DGalleryEntry(entry)) {
return entry.publicWorkCode;
}
@@ -1079,4 +1195,4 @@ function buildBarkBattleThemeTags(work: BarkBattleWorkSummary) {
.map((tag) => tag.trim())
.filter(Boolean)
.slice(0, 3);
}
}