feat: complete bark battle playable demo
This commit is contained in:
@@ -7,8 +7,8 @@ import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/p
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import type {
|
||||
PlatformCreationTypeCard,
|
||||
PlatformCreationTypeId,
|
||||
@@ -185,6 +185,20 @@ export function CustomWorldCreationHub({
|
||||
canDeleteSquareHole: Boolean(onDeleteSquareHole),
|
||||
canDeletePuzzle: Boolean(onDeletePuzzle),
|
||||
canDeleteVisualNovel: Boolean(onDeleteVisualNovel),
|
||||
onOpenRpgDraft: onOpenDraft,
|
||||
onEnterRpgPublished: onEnterPublished,
|
||||
onDeleteRpg: onDeletePublished ?? undefined,
|
||||
onOpenBigFishDetail,
|
||||
onDeleteBigFish: onDeleteBigFish ?? undefined,
|
||||
onOpenMatch3DDetail,
|
||||
onDeleteMatch3D: onDeleteMatch3D ?? undefined,
|
||||
onOpenSquareHoleDetail,
|
||||
onDeleteSquareHole: onDeleteSquareHole ?? undefined,
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle: onDeletePuzzle ?? undefined,
|
||||
onClaimPuzzlePointIncentive: onClaimPuzzlePointIncentive ?? undefined,
|
||||
onOpenVisualNovelDetail: onOpenVisualNovelDetail ?? undefined,
|
||||
onDeleteVisualNovel: onDeleteVisualNovel ?? undefined,
|
||||
}),
|
||||
[
|
||||
bigFishItems,
|
||||
@@ -196,6 +210,14 @@ export function CustomWorldCreationHub({
|
||||
onDeletePublished,
|
||||
onDeletePuzzle,
|
||||
onDeleteVisualNovel,
|
||||
onClaimPuzzlePointIncentive,
|
||||
onOpenBigFishDetail,
|
||||
onOpenDraft,
|
||||
onOpenMatch3DDetail,
|
||||
onOpenPuzzleDetail,
|
||||
onOpenSquareHoleDetail,
|
||||
onOpenVisualNovelDetail,
|
||||
onEnterPublished,
|
||||
puzzleItems,
|
||||
rpgLibraryEntries,
|
||||
squareHoleItems,
|
||||
@@ -222,89 +244,16 @@ export function CustomWorldCreationHub({
|
||||
[activeFilter, shelfItems],
|
||||
);
|
||||
|
||||
function handleOpenShelfItem(item: CreationWorkShelfItem) {
|
||||
switch (item.source.kind) {
|
||||
case 'puzzle':
|
||||
onOpenPuzzleDetail?.(item.source.item);
|
||||
return;
|
||||
case 'visual-novel':
|
||||
onOpenVisualNovelDetail?.(item.source.item);
|
||||
return;
|
||||
case 'big-fish':
|
||||
onOpenBigFishDetail?.(item.source.item);
|
||||
return;
|
||||
case 'match3d':
|
||||
onOpenMatch3DDetail?.(item.source.item);
|
||||
return;
|
||||
case 'square-hole':
|
||||
onOpenSquareHoleDetail?.(item.source.item);
|
||||
return;
|
||||
case 'rpg':
|
||||
if (item.status === 'draft') {
|
||||
onOpenDraft(item.source.item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.source.item.profileId) {
|
||||
onEnterPublished(item.source.item.profileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildDeleteAction(item: CreationWorkShelfItem) {
|
||||
if (!item.canDelete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (item.source.kind) {
|
||||
case 'puzzle': {
|
||||
const sourceItem = item.source.item;
|
||||
return () => {
|
||||
onDeletePuzzle?.(sourceItem);
|
||||
};
|
||||
}
|
||||
case 'visual-novel': {
|
||||
const sourceItem = item.source.item;
|
||||
return () => {
|
||||
onDeleteVisualNovel?.(sourceItem);
|
||||
};
|
||||
}
|
||||
case 'big-fish': {
|
||||
const sourceItem = item.source.item;
|
||||
return () => {
|
||||
onDeleteBigFish?.(sourceItem);
|
||||
};
|
||||
}
|
||||
case 'match3d': {
|
||||
const sourceItem = item.source.item;
|
||||
return () => {
|
||||
onDeleteMatch3D?.(sourceItem);
|
||||
};
|
||||
}
|
||||
case 'square-hole': {
|
||||
const sourceItem = item.source.item;
|
||||
return () => {
|
||||
onDeleteSquareHole?.(sourceItem);
|
||||
};
|
||||
}
|
||||
case 'rpg': {
|
||||
const sourceItem = item.source.item;
|
||||
return () => {
|
||||
onDeletePublished?.(sourceItem);
|
||||
};
|
||||
}
|
||||
}
|
||||
return item.actions.delete ?? null;
|
||||
}
|
||||
|
||||
function buildPointIncentiveAction(item: CreationWorkShelfItem) {
|
||||
if (item.source.kind !== 'puzzle' || !onClaimPuzzlePointIncentive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceItem = item.source.item;
|
||||
return () => {
|
||||
onClaimPuzzlePointIncentive(sourceItem);
|
||||
};
|
||||
return item.actions.claimPointIncentive ?? null;
|
||||
}
|
||||
|
||||
const showStartCard = mode !== 'works-only';
|
||||
@@ -373,7 +322,7 @@ export function CustomWorldCreationHub({
|
||||
previousMetricValues={
|
||||
metricSnapshot[buildWorkMetricCacheItemKey(item)]
|
||||
}
|
||||
onOpen={() => handleOpenShelfItem(item)}
|
||||
onOpen={item.actions.open}
|
||||
onDelete={buildDeleteAction(item)}
|
||||
deleteBusy={deletingWorkId === item.id}
|
||||
onClaimPointIncentive={buildPointIncentiveAction(item)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { buildCreationWorkShelfItems } from './creationWorkShelf';
|
||||
|
||||
@@ -45,3 +45,39 @@ test('buildCreationWorkShelfItems maps visual novel items with VN public code',
|
||||
expect(items[1]?.status).toBe('draft');
|
||||
expect(items[1]?.publicWorkCode).toBeNull();
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems attaches open and delete actions through shelf adapters', () => {
|
||||
const onOpenPuzzleDetail = vi.fn();
|
||||
const onDeletePuzzle = vi.fn();
|
||||
const puzzleWork = {
|
||||
workId: 'puzzle:work-action',
|
||||
profileId: 'puzzle-profile-action',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试作者',
|
||||
levelName: '动作拼图',
|
||||
summary: '验证作品架动作 Adapter。',
|
||||
themeTags: [],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft' as const,
|
||||
updatedAt: '2026-05-08T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
playCount: 0,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
publishReady: false,
|
||||
};
|
||||
|
||||
const [item] = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [puzzleWork],
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle,
|
||||
});
|
||||
|
||||
item?.actions.open();
|
||||
item?.actions.delete?.();
|
||||
|
||||
expect(onOpenPuzzleDetail).toHaveBeenCalledWith(puzzleWork);
|
||||
expect(onDeletePuzzle).toHaveBeenCalledWith(puzzleWork);
|
||||
});
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/
|
||||
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 { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
||||
import {
|
||||
buildBigFishPublicWorkCode,
|
||||
@@ -79,6 +79,12 @@ export type CreationWorkShelfSource =
|
||||
item: VisualNovelWorkSummary;
|
||||
};
|
||||
|
||||
export type CreationWorkShelfActions = {
|
||||
open: () => void;
|
||||
delete?: () => void;
|
||||
claimPointIncentive?: () => void;
|
||||
};
|
||||
|
||||
export type CreationWorkShelfItem = {
|
||||
id: string;
|
||||
kind: CreationWorkShelfKind;
|
||||
@@ -97,6 +103,7 @@ export type CreationWorkShelfItem = {
|
||||
badges: CreationWorkShelfBadge[];
|
||||
metrics: CreationWorkShelfMetric[];
|
||||
pointIncentive?: CreationWorkShelfPointIncentive;
|
||||
actions: CreationWorkShelfActions;
|
||||
source: CreationWorkShelfSource;
|
||||
};
|
||||
|
||||
@@ -114,6 +121,20 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteSquareHole?: boolean;
|
||||
canDeletePuzzle?: boolean;
|
||||
canDeleteVisualNovel?: boolean;
|
||||
onOpenRpgDraft?: (item: CustomWorldWorkSummary) => void;
|
||||
onEnterRpgPublished?: (profileId: string) => void;
|
||||
onDeleteRpg?: (item: CustomWorldWorkSummary) => void;
|
||||
onOpenBigFishDetail?: (item: BigFishWorkSummary) => void;
|
||||
onDeleteBigFish?: (item: BigFishWorkSummary) => void;
|
||||
onOpenMatch3DDetail?: (item: Match3DWorkSummary) => void;
|
||||
onDeleteMatch3D?: (item: Match3DWorkSummary) => void;
|
||||
onOpenSquareHoleDetail?: (item: SquareHoleWorkSummary) => void;
|
||||
onDeleteSquareHole?: (item: SquareHoleWorkSummary) => void;
|
||||
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
|
||||
onDeletePuzzle?: (item: PuzzleWorkSummary) => void;
|
||||
onClaimPuzzlePointIncentive?: (item: PuzzleWorkSummary) => void;
|
||||
onOpenVisualNovelDetail?: (item: VisualNovelWorkSummary) => void;
|
||||
onDeleteVisualNovel?: (item: VisualNovelWorkSummary) => void;
|
||||
}) {
|
||||
const {
|
||||
rpgItems,
|
||||
@@ -129,26 +150,60 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteSquareHole = false,
|
||||
canDeletePuzzle = false,
|
||||
canDeleteVisualNovel = false,
|
||||
onOpenRpgDraft,
|
||||
onEnterRpgPublished,
|
||||
onDeleteRpg,
|
||||
onOpenBigFishDetail,
|
||||
onDeleteBigFish,
|
||||
onOpenMatch3DDetail,
|
||||
onDeleteMatch3D,
|
||||
onOpenSquareHoleDetail,
|
||||
onDeleteSquareHole,
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle,
|
||||
onClaimPuzzlePointIncentive,
|
||||
onOpenVisualNovelDetail,
|
||||
onDeleteVisualNovel,
|
||||
} = params;
|
||||
|
||||
return [
|
||||
...rpgItems.map((item) =>
|
||||
mapRpgWorkToShelfItem(item, canDeleteRpg, rpgLibraryEntries),
|
||||
mapRpgWorkToShelfItem(item, canDeleteRpg, rpgLibraryEntries, {
|
||||
onOpenDraft: onOpenRpgDraft,
|
||||
onEnterPublished: onEnterRpgPublished,
|
||||
onDelete: onDeleteRpg,
|
||||
}),
|
||||
),
|
||||
...bigFishItems.map((item) =>
|
||||
mapBigFishWorkToShelfItem(item, canDeleteBigFish),
|
||||
mapBigFishWorkToShelfItem(item, canDeleteBigFish, {
|
||||
onOpen: onOpenBigFishDetail,
|
||||
onDelete: onDeleteBigFish,
|
||||
}),
|
||||
),
|
||||
...match3dItems.map((item) =>
|
||||
mapMatch3DWorkToShelfItem(item, canDeleteMatch3D),
|
||||
mapMatch3DWorkToShelfItem(item, canDeleteMatch3D, {
|
||||
onOpen: onOpenMatch3DDetail,
|
||||
onDelete: onDeleteMatch3D,
|
||||
}),
|
||||
),
|
||||
...squareHoleItems.map((item) =>
|
||||
mapSquareHoleWorkToShelfItem(item, canDeleteSquareHole),
|
||||
mapSquareHoleWorkToShelfItem(item, canDeleteSquareHole, {
|
||||
onOpen: onOpenSquareHoleDetail,
|
||||
onDelete: onDeleteSquareHole,
|
||||
}),
|
||||
),
|
||||
...puzzleItems.map((item) =>
|
||||
mapPuzzleWorkToShelfItem(item, canDeletePuzzle),
|
||||
mapPuzzleWorkToShelfItem(item, canDeletePuzzle, {
|
||||
onOpen: onOpenPuzzleDetail,
|
||||
onDelete: onDeletePuzzle,
|
||||
onClaimPointIncentive: onClaimPuzzlePointIncentive,
|
||||
}),
|
||||
),
|
||||
...visualNovelItems.map((item) =>
|
||||
mapVisualNovelWorkToShelfItem(item, canDeleteVisualNovel),
|
||||
mapVisualNovelWorkToShelfItem(item, canDeleteVisualNovel, {
|
||||
onOpen: onOpenVisualNovelDetail,
|
||||
onDelete: onDeleteVisualNovel,
|
||||
}),
|
||||
),
|
||||
].sort(
|
||||
(left, right) =>
|
||||
@@ -156,10 +211,26 @@ export function buildCreationWorkShelfItems(params: {
|
||||
);
|
||||
}
|
||||
|
||||
type RpgWorkShelfAdapter = {
|
||||
onOpenDraft?: (item: CustomWorldWorkSummary) => void;
|
||||
onEnterPublished?: (profileId: string) => void;
|
||||
onDelete?: (item: CustomWorldWorkSummary) => void;
|
||||
};
|
||||
|
||||
type WorkShelfAdapter<TItem> = {
|
||||
onOpen?: (item: TItem) => void;
|
||||
onDelete?: (item: TItem) => void;
|
||||
};
|
||||
|
||||
type PuzzleWorkShelfAdapter = WorkShelfAdapter<PuzzleWorkSummary> & {
|
||||
onClaimPointIncentive?: (item: PuzzleWorkSummary) => void;
|
||||
};
|
||||
|
||||
function mapRpgWorkToShelfItem(
|
||||
item: CustomWorldWorkSummary,
|
||||
canDelete: boolean,
|
||||
libraryEntries: CustomWorldLibraryEntry<CustomWorldProfile>[],
|
||||
adapter: RpgWorkShelfAdapter,
|
||||
): CreationWorkShelfItem {
|
||||
const isDraft = item.status === 'draft';
|
||||
const libraryEntry = item.profileId
|
||||
@@ -200,6 +271,7 @@ function mapRpgWorkToShelfItem(
|
||||
: '查看详情',
|
||||
canDelete,
|
||||
canShare: item.status === 'published' && Boolean(publicWorkCode),
|
||||
actions: buildRpgWorkShelfActions(item, adapter),
|
||||
badges,
|
||||
metrics: isDraft ? [] : metrics,
|
||||
source: { kind: 'rpg', item },
|
||||
@@ -209,6 +281,7 @@ function mapRpgWorkToShelfItem(
|
||||
function mapBigFishWorkToShelfItem(
|
||||
item: BigFishWorkSummary,
|
||||
canDelete: boolean,
|
||||
adapter: WorkShelfAdapter<BigFishWorkSummary>,
|
||||
): CreationWorkShelfItem {
|
||||
const isPublished = item.status === 'published';
|
||||
const publicWorkCode = isPublished
|
||||
@@ -233,6 +306,7 @@ function mapBigFishWorkToShelfItem(
|
||||
openActionLabel: item.status === 'draft' ? '继续创作' : '查看详情',
|
||||
canDelete,
|
||||
canShare: isPublished && Boolean(publicWorkCode),
|
||||
actions: buildWorkShelfActions(item, adapter),
|
||||
badges: [
|
||||
buildStatusBadge(item.status),
|
||||
{ id: 'type', label: '大鱼', tone: 'neutral' },
|
||||
@@ -251,6 +325,7 @@ function mapBigFishWorkToShelfItem(
|
||||
function mapMatch3DWorkToShelfItem(
|
||||
item: Match3DWorkSummary,
|
||||
canDelete: boolean,
|
||||
adapter: WorkShelfAdapter<Match3DWorkSummary>,
|
||||
): CreationWorkShelfItem {
|
||||
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
|
||||
const publicWorkCode =
|
||||
@@ -274,6 +349,7 @@ function mapMatch3DWorkToShelfItem(
|
||||
openActionLabel: status === 'published' ? '查看详情' : '继续创作',
|
||||
canDelete,
|
||||
canShare: status === 'published' && Boolean(publicWorkCode),
|
||||
actions: buildWorkShelfActions(item, adapter),
|
||||
badges: [
|
||||
buildStatusBadge(status),
|
||||
{ id: 'type', label: '抓鹅', tone: 'neutral' },
|
||||
@@ -293,6 +369,7 @@ function mapMatch3DWorkToShelfItem(
|
||||
function mapPuzzleWorkToShelfItem(
|
||||
item: PuzzleWorkSummary,
|
||||
canDelete: boolean,
|
||||
adapter: PuzzleWorkShelfAdapter,
|
||||
): CreationWorkShelfItem {
|
||||
const status = item.publicationStatus;
|
||||
const publicWorkCode =
|
||||
@@ -320,6 +397,7 @@ function mapPuzzleWorkToShelfItem(
|
||||
status === 'published' && !item.sourceSessionId ? '查看详情' : '继续创作',
|
||||
canDelete,
|
||||
canShare: status === 'published' && Boolean(publicWorkCode),
|
||||
actions: buildPuzzleWorkShelfActions(item, adapter),
|
||||
badges: [
|
||||
buildStatusBadge(status),
|
||||
{ id: 'type', label: '拼图', tone: 'neutral' },
|
||||
@@ -354,6 +432,7 @@ function mapPuzzleWorkToShelfItem(
|
||||
function mapVisualNovelWorkToShelfItem(
|
||||
item: VisualNovelWorkSummary,
|
||||
canDelete: boolean,
|
||||
adapter: WorkShelfAdapter<VisualNovelWorkSummary>,
|
||||
): CreationWorkShelfItem {
|
||||
const status =
|
||||
item.publishStatus === 'published' ? 'published' : 'draft';
|
||||
@@ -394,6 +473,7 @@ function mapVisualNovelWorkToShelfItem(
|
||||
likeCount: 0,
|
||||
})
|
||||
: [],
|
||||
actions: buildWorkShelfActions(item, adapter),
|
||||
source: { kind: 'visual-novel', item },
|
||||
};
|
||||
}
|
||||
@@ -401,6 +481,7 @@ function mapVisualNovelWorkToShelfItem(
|
||||
function mapSquareHoleWorkToShelfItem(
|
||||
item: SquareHoleWorkSummary,
|
||||
canDelete: boolean,
|
||||
adapter: WorkShelfAdapter<SquareHoleWorkSummary>,
|
||||
): CreationWorkShelfItem {
|
||||
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
|
||||
const publicWorkCode =
|
||||
@@ -438,10 +519,65 @@ function mapSquareHoleWorkToShelfItem(
|
||||
likeCount: 0,
|
||||
})
|
||||
: [],
|
||||
actions: buildWorkShelfActions(item, adapter),
|
||||
source: { kind: 'square-hole', item },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function buildWorkShelfActions<TItem>(
|
||||
item: TItem,
|
||||
adapter: WorkShelfAdapter<TItem>,
|
||||
): CreationWorkShelfActions {
|
||||
return {
|
||||
open: () => {
|
||||
adapter.onOpen?.(item);
|
||||
},
|
||||
delete: adapter.onDelete
|
||||
? () => {
|
||||
adapter.onDelete?.(item);
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPuzzleWorkShelfActions(
|
||||
item: PuzzleWorkSummary,
|
||||
adapter: PuzzleWorkShelfAdapter,
|
||||
): CreationWorkShelfActions {
|
||||
return {
|
||||
...buildWorkShelfActions(item, adapter),
|
||||
claimPointIncentive: adapter.onClaimPointIncentive
|
||||
? () => {
|
||||
adapter.onClaimPointIncentive?.(item);
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRpgWorkShelfActions(
|
||||
item: CustomWorldWorkSummary,
|
||||
adapter: RpgWorkShelfAdapter,
|
||||
): CreationWorkShelfActions {
|
||||
return {
|
||||
open: () => {
|
||||
if (item.status === 'draft') {
|
||||
adapter.onOpenDraft?.(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.profileId) {
|
||||
adapter.onEnterPublished?.(item.profileId);
|
||||
}
|
||||
},
|
||||
delete: adapter.onDelete
|
||||
? () => {
|
||||
adapter.onDelete?.(item);
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPublishedMetrics(params: {
|
||||
playCount?: number | null;
|
||||
remixCount?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user