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

@@ -6,6 +6,7 @@ import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contra
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleClearWorkSummaryResponse } from '../../../packages/shared/src/contracts/puzzleClear';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
@@ -68,6 +69,9 @@ type CustomWorldCreationHubProps = {
woodenFishItems?: WoodenFishWorkSummaryResponse[];
onOpenWoodenFishDetail?: ((item: WoodenFishWorkSummaryResponse) => void) | null;
onDeleteWoodenFish?: ((item: WoodenFishWorkSummaryResponse) => void) | null;
puzzleClearItems?: PuzzleClearWorkSummaryResponse[];
onOpenPuzzleClearDetail?: ((item: PuzzleClearWorkSummaryResponse) => void) | null;
onDeletePuzzleClear?: ((item: PuzzleClearWorkSummaryResponse) => void) | null;
puzzleItems?: PuzzleWorkSummary[];
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
onDeletePuzzle?: ((item: PuzzleWorkSummary) => void) | null;
@@ -181,6 +185,9 @@ export function CustomWorldCreationHub({
woodenFishItems = [],
onOpenWoodenFishDetail = null,
onDeleteWoodenFish = null,
puzzleClearItems = [],
onOpenPuzzleClearDetail = null,
onDeletePuzzleClear = null,
puzzleItems = [],
onOpenPuzzleDetail,
onDeletePuzzle = null,
@@ -215,6 +222,7 @@ export function CustomWorldCreationHub({
squareHoleItems: isSquareHoleCreationVisible ? squareHoleItems : [],
jumpHopItems,
woodenFishItems,
puzzleClearItems,
puzzleItems,
babyObjectMatchItems,
barkBattleItems,
@@ -226,6 +234,7 @@ export function CustomWorldCreationHub({
isSquareHoleCreationVisible && Boolean(onDeleteSquareHole),
canDeleteJumpHop: Boolean(onDeleteJumpHop),
canDeleteWoodenFish: Boolean(onDeleteWoodenFish),
canDeletePuzzleClear: Boolean(onDeletePuzzleClear),
canDeletePuzzle: Boolean(onDeletePuzzle),
canDeleteBabyObjectMatch: Boolean(onDeleteBabyObjectMatch),
canDeleteBarkBattle: Boolean(onDeleteBarkBattle),
@@ -243,6 +252,8 @@ export function CustomWorldCreationHub({
onDeleteJumpHop: onDeleteJumpHop ?? undefined,
onOpenWoodenFishDetail: onOpenWoodenFishDetail ?? undefined,
onDeleteWoodenFish: onDeleteWoodenFish ?? undefined,
onOpenPuzzleClearDetail: onOpenPuzzleClearDetail ?? undefined,
onDeletePuzzleClear: onDeletePuzzleClear ?? undefined,
onOpenPuzzleDetail,
onDeletePuzzle: onDeletePuzzle ?? undefined,
onClaimPuzzlePointIncentive: onClaimPuzzlePointIncentive ?? undefined,
@@ -271,6 +282,7 @@ export function CustomWorldCreationHub({
onDeleteVisualNovel,
onDeleteJumpHop,
onDeleteWoodenFish,
onDeletePuzzleClear,
onClaimPuzzlePointIncentive,
onOpenBigFishDetail,
onOpenDraft,
@@ -281,8 +293,10 @@ export function CustomWorldCreationHub({
onOpenSquareHoleDetail,
onOpenVisualNovelDetail,
onOpenWoodenFishDetail,
onOpenPuzzleClearDetail,
onEnterPublished,
getWorkState,
puzzleClearItems,
puzzleItems,
rpgLibraryEntries,
onOpenSquareHoleDetail,
@@ -342,6 +356,9 @@ export function CustomWorldCreationHub({
case 'wooden-fish':
onOpenWoodenFishDetail?.(item.source.item);
return;
case 'puzzle-clear':
onOpenPuzzleClearDetail?.(item.source.item);
return;
case 'rpg':
if (item.status === 'draft') {
onOpenDraft(item.source.item);

View File

@@ -6,6 +6,7 @@ import {
Trash2,
} from 'lucide-react';
import {
default as React,
type CSSProperties,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent,
@@ -61,6 +62,7 @@ const CREATION_WORK_KIND_FALLBACK_COVER: Record<CreationWorkShelfKind, string> =
'square-hole': '/creation-type-references/square-hole.webp',
'jump-hop': '/creation-type-references/jump-hop.webp',
'wooden-fish': '/wooden-fish/default-hit-object.png',
'puzzle-clear': '/creation-type-references/puzzle.webp',
puzzle: '/creation-type-references/puzzle.webp',
'baby-object-match': '/creation-type-references/creative-agent.webp',
'bark-battle': '/creation-type-references/bark-battle.webp',

View File

@@ -97,6 +97,47 @@ test('buildCreationWorkShelfItems maps wooden fish items with WF public code', (
expect(onOpenWoodenFishDetail).toHaveBeenCalledWith(woodenFishWork);
});
test('buildCreationWorkShelfItems maps puzzle clear items with PC public code', () => {
const onOpenPuzzleClearDetail = vi.fn();
const puzzleClearWork = {
runtimeKind: 'puzzle-clear' as const,
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' as const,
};
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
puzzleClearItems: [puzzleClearWork],
onOpenPuzzleClearDetail,
});
items[0]?.actions.open();
expect(items).toHaveLength(1);
expect(items[0]?.kind).toBe('puzzle-clear');
expect(items[0]?.status).toBe('published');
expect(items[0]?.publicWorkCode).toBe('PC-12345678');
expect(items[0]?.sharePath).toContain('/works/detail?work=PC-12345678');
expect(items[0]?.openActionLabel).toBe('查看详情');
expect(items[0]?.badges.some((badge) => badge.label === '拼消消')).toBe(true);
expect(items[0]?.metrics.find((metric) => metric.id === 'play-count')?.value).toBe(6);
expect(onOpenPuzzleClearDetail).toHaveBeenCalledWith(puzzleClearWork);
});
test('buildCreationWorkShelfItems keeps published bark battle over duplicate draft', () => {
const items = buildCreationWorkShelfItems({
rpgItems: [],

View File

@@ -3,6 +3,7 @@ import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleClearWorkSummaryResponse } from '../../../packages/shared/src/contracts/puzzleClear';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
@@ -17,6 +18,7 @@ import {
buildBigFishPublicWorkCode,
buildJumpHopPublicWorkCode,
buildMatch3DPublicWorkCode,
buildPuzzleClearPublicWorkCode,
buildPuzzlePublicWorkCode,
buildSquareHolePublicWorkCode,
buildVisualNovelPublicWorkCode,
@@ -37,6 +39,7 @@ export type CreationWorkShelfKind =
| 'square-hole'
| 'jump-hop'
| 'wooden-fish'
| 'puzzle-clear'
| 'puzzle'
| 'baby-object-match'
| 'bark-battle'
@@ -97,6 +100,10 @@ export type CreationWorkShelfSource =
kind: 'wooden-fish';
item: WoodenFishWorkSummaryResponse;
}
| {
kind: 'puzzle-clear';
item: PuzzleClearWorkSummaryResponse;
}
| {
kind: 'puzzle';
item: PuzzleWorkSummary;
@@ -153,6 +160,7 @@ export function buildCreationWorkShelfItems(params: {
squareHoleItems?: SquareHoleWorkSummary[];
jumpHopItems?: JumpHopWorkSummaryResponse[];
woodenFishItems?: WoodenFishWorkSummaryResponse[];
puzzleClearItems?: PuzzleClearWorkSummaryResponse[];
puzzleItems: PuzzleWorkSummary[];
babyObjectMatchItems?: BabyObjectMatchDraft[];
barkBattleItems?: BarkBattleWorkSummary[];
@@ -163,6 +171,7 @@ export function buildCreationWorkShelfItems(params: {
canDeleteSquareHole?: boolean;
canDeleteJumpHop?: boolean;
canDeleteWoodenFish?: boolean;
canDeletePuzzleClear?: boolean;
canDeletePuzzle?: boolean;
canDeleteBabyObjectMatch?: boolean;
canDeleteBarkBattle?: boolean;
@@ -180,6 +189,8 @@ export function buildCreationWorkShelfItems(params: {
onDeleteJumpHop?: (item: JumpHopWorkSummaryResponse) => void;
onOpenWoodenFishDetail?: (item: WoodenFishWorkSummaryResponse) => void;
onDeleteWoodenFish?: (item: WoodenFishWorkSummaryResponse) => void;
onOpenPuzzleClearDetail?: (item: PuzzleClearWorkSummaryResponse) => void;
onDeletePuzzleClear?: (item: PuzzleClearWorkSummaryResponse) => void;
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
onDeletePuzzle?: (item: PuzzleWorkSummary) => void;
onClaimPuzzlePointIncentive?: (item: PuzzleWorkSummary) => void;
@@ -201,6 +212,7 @@ export function buildCreationWorkShelfItems(params: {
squareHoleItems = [],
jumpHopItems = [],
woodenFishItems = [],
puzzleClearItems = [],
puzzleItems,
babyObjectMatchItems = [],
barkBattleItems = [],
@@ -211,6 +223,7 @@ export function buildCreationWorkShelfItems(params: {
canDeleteSquareHole = false,
canDeleteJumpHop = false,
canDeleteWoodenFish = false,
canDeletePuzzleClear = false,
canDeletePuzzle = false,
canDeleteBabyObjectMatch = false,
canDeleteBarkBattle = false,
@@ -228,6 +241,8 @@ export function buildCreationWorkShelfItems(params: {
onDeleteJumpHop,
onOpenWoodenFishDetail,
onDeleteWoodenFish,
onOpenPuzzleClearDetail,
onDeletePuzzleClear,
onOpenPuzzleDetail,
onDeletePuzzle,
onClaimPuzzlePointIncentive,
@@ -278,6 +293,12 @@ export function buildCreationWorkShelfItems(params: {
onDelete: onDeleteWoodenFish,
}),
),
...puzzleClearItems.map((item) =>
mapPuzzleClearWorkToShelfItem(item, canDeletePuzzleClear, {
onOpen: onOpenPuzzleClearDetail,
onDelete: onDeletePuzzleClear,
}),
),
...puzzleItems.map((item) =>
mapPuzzleWorkToShelfItem(item, canDeletePuzzle, {
onOpen: onOpenPuzzleDetail,
@@ -884,6 +905,56 @@ function mapWoodenFishWorkToShelfItem(
};
}
function mapPuzzleClearWorkToShelfItem(
item: PuzzleClearWorkSummaryResponse,
canDelete: boolean,
adapter: WorkShelfAdapter<PuzzleClearWorkSummaryResponse>,
): CreationWorkShelfItem {
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
const publicWorkCode =
status === 'published'
? buildPuzzleClearPublicWorkCode(item.profileId)
: null;
const title = item.workTitle.trim() || '拼消消';
const summary =
item.workDescription.trim() || (status === 'draft' ? '未填写作品描述' : '');
return {
id: item.workId,
kind: 'puzzle-clear',
status,
title,
summary,
authorDisplayName: resolveAuthorDisplayName(item),
updatedAt: item.updatedAt,
coverImageSrc: normalizeCoverImageSrc(item.coverImageSrc),
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,
})
: [],
actions: buildWorkShelfActions(item, adapter),
source: { kind: 'puzzle-clear', item },
};
}
function resolveAuthorDisplayName(
...sources: Array<unknown>
@@ -1097,6 +1168,8 @@ function isPersistedCreationWorkGenerating(item: CreationWorkShelfItem) {
return isPersistedPuzzleDraftGenerating(item.source.item);
case 'wooden-fish':
return item.source.item.generationStatus === 'generating';
case 'puzzle-clear':
return item.source.item.generationStatus === 'generating';
case 'bark-battle':
return isPersistedBarkBattleDraftGenerating(item.source.item);
default: