feat: add puzzle clear template runtime
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user