fix: tighten public work type routing

This commit is contained in:
2026-06-07 14:49:36 +08:00
parent 8f460feb41
commit 8131894bb5
21 changed files with 611 additions and 228 deletions

View File

@@ -395,6 +395,7 @@ import {
buildPlatformPublicGalleryCardKey,
isBarkBattleGalleryEntry,
isBigFishGalleryEntry,
isCustomWorldGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isMatch3DGalleryEntry,
@@ -701,6 +702,10 @@ function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
function getPlatformRecommendRuntimeKind(
entry: PlatformPublicGalleryCard,
): RecommendRuntimeKind {
if (isCustomWorldGalleryEntry(entry)) {
return 'rpg';
}
if (isBigFishGalleryEntry(entry)) {
return 'big-fish';
}
@@ -741,7 +746,7 @@ function getPlatformRecommendRuntimeKind(
return 'edutainment';
}
return 'rpg';
throw new Error('未知公开作品类型,无法启动推荐玩法。');
}
function resolveRecommendEntryShareStage(
@@ -758,6 +763,14 @@ function resolveRecommendEntryShareStage(
return 'work-detail';
}
function resolveUnsupportedPublicWorkActionMessage(
entry: PlatformPublicGalleryCard,
actionLabel: string,
) {
const sourceType = 'sourceType' in entry ? entry.sourceType : 'custom-world';
return `作品类型 ${sourceType} 暂不支持${actionLabel}`;
}
function isRecommendRuntimeReadyForEntry(
entry: PlatformPublicGalleryCard,
state: RecommendRuntimeState,
@@ -800,8 +813,11 @@ function isRecommendRuntimeReadyForEntry(
if (expectedKind === 'edutainment') {
return Boolean(state.babyObjectMatchDraft);
}
if (expectedKind === 'rpg') {
return true;
}
return true;
throw new Error('未知推荐玩法类型。');
}
function isSamePlatformPublicGalleryEntry(
@@ -834,7 +850,10 @@ function mergePlatformPublicGalleryEntries(
function mapRpgGalleryCardToPublicWorkDetail(
entry: CustomWorldGalleryCard,
): PlatformPublicGalleryCard {
return entry;
return {
...entry,
sourceType: 'custom-world',
};
}
function mapPuzzleWorkToPublicWorkDetail(
@@ -13521,54 +13540,55 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isEdutainmentGalleryEntry(entry)) {
setPublicWorkDetailError('宝贝识物点赞将在后续版本开放。');
if (isCustomWorldGalleryEntry(entry)) {
void likeRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((updatedEntry) => {
setSelectedDetailEntry((current) =>
current?.profileId === updatedEntry.profileId
? updatedEntry
: current,
);
platformBootstrap.setPublishedGalleryEntries((current) =>
current.map((item) =>
item.profileId === updatedEntry.profileId
? updatedEntry
: item,
),
);
syncUpdatedPublicWorkDetail(
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '点赞 RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (
isPuzzleClearGalleryEntry(entry) ||
isJumpHopGalleryEntry(entry) ||
isWoodenFishGalleryEntry(entry) ||
isMatch3DGalleryEntry(entry) ||
isEdutainmentGalleryEntry(entry) ||
isBarkBattleGalleryEntry(entry) ||
isSquareHoleGalleryEntry(entry) ||
isVisualNovelGalleryEntry(entry)
) {
setPublicWorkDetailError(
resolveUnsupportedPublicWorkActionMessage(entry, '点赞'),
);
setIsPublicWorkDetailBusy(false);
return;
}
if (isBarkBattleGalleryEntry(entry)) {
setPublicWorkDetailError('汪汪声浪点赞将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isSquareHoleGalleryEntry(entry)) {
setPublicWorkDetailError('方洞挑战点赞将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isVisualNovelGalleryEntry(entry)) {
setPublicWorkDetailError('视觉小说点赞将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
void likeRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((updatedEntry) => {
setSelectedDetailEntry((current) =>
current?.profileId === updatedEntry.profileId
? updatedEntry
: current,
);
platformBootstrap.setPublishedGalleryEntries((current) =>
current.map((item) =>
item.profileId === updatedEntry.profileId ? updatedEntry : item,
),
);
syncUpdatedPublicWorkDetail(
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '点赞 RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
setPublicWorkDetailError('未知公开作品类型,无法点赞。');
setIsPublicWorkDetailBusy(false);
});
},
[
@@ -14189,7 +14209,12 @@ export function PlatformEntryFlowShellImpl({
return;
}
void openRpgPublicWorkDetail(entry);
if (isCustomWorldGalleryEntry(entry)) {
void openRpgPublicWorkDetail(entry);
return;
}
setPublicWorkDetailError('未知公开作品类型,无法打开作品详情。');
},
[
openPuzzlePublicWorkDetail,
@@ -15506,14 +15531,6 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isJumpHopGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startJumpHopRunFromProfile(selectedPublicWorkDetail.profileId, {
returnStage: 'work-detail',
});
return;
}
if (isWoodenFishGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startWoodenFishRunFromProfile(selectedPublicWorkDetail.profileId, {
@@ -15588,6 +15605,11 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (!isCustomWorldGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError('未知公开作品类型,无法进入玩法。');
return;
}
const launchEntry =
selectedDetailEntry?.profileId === selectedPublicWorkDetail.profileId
? selectedDetailEntry
@@ -15759,8 +15781,10 @@ export function PlatformEntryFlowShellImpl({
embedded: true,
},
);
} else {
} else if (isCustomWorldGalleryEntry(entry)) {
started = true;
} else {
throw new Error('未知公开作品类型,无法启动推荐玩法。');
}
if (!isCurrentStartRequest()) {
@@ -16403,74 +16427,49 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (isPuzzleClearGalleryEntry(entry)) {
setPublicWorkDetailError('拼消消作品改造将在后续版本开放。');
if (
isPuzzleClearGalleryEntry(entry) ||
isMatch3DGalleryEntry(entry) ||
isSquareHoleGalleryEntry(entry) ||
isJumpHopGalleryEntry(entry) ||
isWoodenFishGalleryEntry(entry) ||
isVisualNovelGalleryEntry(entry) ||
isEdutainmentGalleryEntry(entry) ||
isBarkBattleGalleryEntry(entry)
) {
setPublicWorkDetailError(
resolveUnsupportedPublicWorkActionMessage(entry, '改造'),
);
setIsPublicWorkDetailBusy(false);
return;
}
if (isMatch3DGalleryEntry(entry)) {
setPublicWorkDetailError('抓大鹅作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
if (isCustomWorldGalleryEntry(entry)) {
void remixRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((response) => {
const nextEntry = response.entry;
setSelectedDetailEntry(nextEntry);
platformBootstrap.setSavedCustomWorldEntries([
nextEntry,
...platformBootstrap.savedCustomWorldEntries.filter(
(entry) => entry.profileId !== nextEntry.profileId,
),
]);
void detailNavigation.openSavedCustomWorldEditor(nextEntry);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, 'Remix RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (isSquareHoleGalleryEntry(entry)) {
setPublicWorkDetailError('方洞挑战作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isJumpHopGalleryEntry(entry)) {
setPublicWorkDetailError('跳一跳作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isWoodenFishGalleryEntry(entry)) {
setPublicWorkDetailError('敲木鱼作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isVisualNovelGalleryEntry(entry)) {
setPublicWorkDetailError('视觉小说作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isEdutainmentGalleryEntry(entry)) {
setPublicWorkDetailError('宝贝识物作品改造将在创作链路接入后开放。');
setIsPublicWorkDetailBusy(false);
return;
}
if (isBarkBattleGalleryEntry(entry)) {
setPublicWorkDetailError('汪汪声浪作品改造将在后续版本开放。');
setIsPublicWorkDetailBusy(false);
return;
}
void remixRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
.then((response) => {
const nextEntry = response.entry;
setSelectedDetailEntry(nextEntry);
platformBootstrap.setSavedCustomWorldEntries([
nextEntry,
...platformBootstrap.savedCustomWorldEntries.filter(
(entry) => entry.profileId !== nextEntry.profileId,
),
]);
void detailNavigation.openSavedCustomWorldEditor(nextEntry);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, 'Remix RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
setPublicWorkDetailError('未知公开作品类型,无法改造。');
setIsPublicWorkDetailBusy(false);
});
},
[
@@ -16630,6 +16629,11 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (!isCustomWorldGalleryEntry(entry)) {
setPublicWorkDetailError('未知公开作品类型,无法编辑。');
return;
}
const editEntry =
selectedDetailEntry?.profileId === entry.profileId
? selectedDetailEntry
@@ -16737,6 +16741,7 @@ export function PlatformEntryFlowShellImpl({
const entry =
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
const card = {
sourceType: 'custom-world',
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
publicWorkCode: entry.publicWorkCode,
@@ -16755,7 +16760,7 @@ export function PlatformEntryFlowShellImpl({
playCount: entry.playCount ?? 0,
remixCount: entry.remixCount ?? 0,
likeCount: entry.likeCount ?? 0,
} satisfies CustomWorldGalleryCard;
} satisfies PlatformPublicGalleryCard;
if (!canExposePublicWork(card)) {
throw new Error(EDUTAINMENT_HIDDEN_MESSAGE);
}

View File

@@ -24,7 +24,11 @@ import {
formatPlatformWorkDisplayTags,
formatPlatformWorldTime,
isBarkBattleGalleryEntry,
isCustomWorldGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isPuzzleClearGalleryEntry,
isWoodenFishGalleryEntry,
type PlatformPublicGalleryCard,
resolvePlatformWorkAuthorDisplayName,
resolvePlatformPublicWorkCode,
@@ -57,9 +61,18 @@ function getSourceLabel(entry: PlatformPublicGalleryCard) {
if ('sourceType' in entry && entry.sourceType === 'puzzle') {
return '拼图';
}
if (isPuzzleClearGalleryEntry(entry)) {
return '拼消消';
}
if ('sourceType' in entry && entry.sourceType === 'big-fish') {
return '大鱼吃小鱼';
}
if (isJumpHopGalleryEntry(entry)) {
return '跳一跳';
}
if (isWoodenFishGalleryEntry(entry)) {
return '敲木鱼';
}
if ('sourceType' in entry && entry.sourceType === 'match3d') {
return '抓大鹅';
}
@@ -75,7 +88,11 @@ function getSourceLabel(entry: PlatformPublicGalleryCard) {
if (isEdutainmentGalleryEntry(entry)) {
return entry.templateName;
}
return 'RPG';
if (isCustomWorldGalleryEntry(entry)) {
return 'RPG';
}
throw new Error('未知公开作品类型。');
}
function getAuthorAvatarLabel(authorDisplayName: string) {

View File

@@ -1,5 +1,6 @@
import { afterEach, expect, test, vi } from 'vitest';
import type { CreationEntryTypeConfig } from '../../services/creationEntryConfigService';
import {
derivePlatformCreationTypes,
groupVisiblePlatformCreationTypes,
@@ -81,7 +82,7 @@ test('database entry config controls visibility open state and display order', (
test('visible platform creation types hide invisible cards and put locked cards last', () => {
const cards = derivePlatformCreationTypes([
{
id: 'hidden',
id: 'airp',
title: '隐藏',
subtitle: '隐藏',
badge: '隐藏',
@@ -95,7 +96,7 @@ test('visible platform creation types hide invisible cards and put locked cards
updatedAtMicros: 1,
},
{
id: 'locked',
id: 'visual-novel',
title: '锁定',
subtitle: '锁定',
badge: '即将开放',
@@ -109,7 +110,7 @@ test('visible platform creation types hide invisible cards and put locked cards
updatedAtMicros: 1,
},
{
id: 'open',
id: 'rpg',
title: '开放',
subtitle: '开放',
badge: '可创建',
@@ -125,13 +126,13 @@ test('visible platform creation types hide invisible cards and put locked cards
]);
expect(getVisiblePlatformCreationTypes(cards).map((item) => item.id)).toEqual(
['open', 'locked'],
['rpg', 'visual-novel'],
);
expect(isPlatformCreationTypeVisible(cards, 'hidden')).toBe(false);
expect(isPlatformCreationTypeVisible(cards, 'open')).toBe(true);
expect(isPlatformCreationTypeOpen(cards, 'hidden')).toBe(false);
expect(isPlatformCreationTypeOpen(cards, 'locked')).toBe(false);
expect(isPlatformCreationTypeOpen(cards, 'open')).toBe(true);
expect(isPlatformCreationTypeVisible(cards, 'airp')).toBe(false);
expect(isPlatformCreationTypeVisible(cards, 'rpg')).toBe(true);
expect(isPlatformCreationTypeOpen(cards, 'airp')).toBe(false);
expect(isPlatformCreationTypeOpen(cards, 'visual-novel')).toBe(false);
expect(isPlatformCreationTypeOpen(cards, 'rpg')).toBe(true);
expect(
cards.every((item) =>
item.imageSrc.startsWith('/creation-type-references/'),
@@ -288,7 +289,7 @@ test('groups visible platform creation types by backend category metadata', () =
updatedAtMicros: 1,
},
{
id: 'hidden',
id: 'airp',
title: '隐藏入口',
subtitle: '隐藏',
badge: '隐藏',
@@ -319,7 +320,7 @@ test('groups visible platform creation types by backend category metadata', () =
test('falls back when backend creation type category metadata is missing', () => {
const cards = derivePlatformCreationTypes([
{
id: 'legacy-entry',
id: 'creative-agent',
title: '历史入口',
subtitle: '旧数据缺少分类字段',
badge: '可创建',
@@ -336,7 +337,7 @@ test('falls back when backend creation type category metadata is missing', () =>
expect(cards[0]).toEqual(
expect.objectContaining({
id: 'legacy-entry',
id: 'creative-agent',
categoryId: 'recommended',
categoryLabel: '热门推荐',
}),
@@ -348,3 +349,24 @@ test('falls back when backend creation type category metadata is missing', () =>
}),
]);
});
test('throws when backend sends an unknown creation type id', () => {
const unknownEntry = {
id: 'unknown-play',
title: '未知玩法',
subtitle: '未知',
badge: '未知',
imageSrc: '/creation-type-references/puzzle.webp',
visible: true,
open: true,
sortOrder: 10,
categoryId: 'recommended',
categoryLabel: '热门推荐',
categorySortOrder: 20,
updatedAtMicros: 1,
} as unknown as CreationEntryTypeConfig;
expect(() => derivePlatformCreationTypes([unknownEntry])).toThrow(
'未知创作类型unknown-play',
);
});

View File

@@ -1,7 +1,11 @@
import {
assertPlatformCreationTypeId,
type PlatformCreationTypeId,
} from '../../../packages/shared/src/contracts/playTypes';
import type { CreationEntryTypeConfig } from '../../services/creationEntryConfigService';
import { isEdutainmentEntryEnabled } from './platformEdutainmentVisibility';
export type PlatformCreationTypeId = string;
export type { PlatformCreationTypeId };
export type PlatformCreationTypeCard = {
id: PlatformCreationTypeId;
@@ -117,21 +121,25 @@ export function derivePlatformCreationTypes(
): PlatformCreationTypeCard[] {
const orderedCards = [...creationTypes]
.sort((left, right) => left.sortOrder - right.sortOrder)
.map((item) => ({
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
imageSrc: item.imageSrc,
locked: !item.open,
categoryId: normalizeCategoryId(item.categoryId),
categoryLabel: normalizeCategoryLabel(item.categoryLabel),
categorySortOrder: item.categorySortOrder,
sortOrder: item.sortOrder,
hidden:
!item.visible ||
(item.id === 'baby-object-match' && !isEdutainmentEntryEnabled()),
}));
.map((item) => {
const id = assertPlatformCreationTypeId(item.id);
return {
id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
imageSrc: item.imageSrc,
locked: !item.open,
categoryId: normalizeCategoryId(item.categoryId),
categoryLabel: normalizeCategoryLabel(item.categoryLabel),
categorySortOrder: item.categorySortOrder,
sortOrder: item.sortOrder,
hidden:
!item.visible ||
(id === 'baby-object-match' && !isEdutainmentEntryEnabled()),
};
});
return [
...orderedCards.filter((item) => !item.hidden && !item.locked),

View File

@@ -1,6 +1,8 @@
import {
buildPlatformPublicGalleryCardKey,
isEdutainmentGalleryEntry,
type PlatformPublicGalleryCard,
resolvePlatformPublicWorkSourceType,
} from '../rpg-entry/rpgEntryWorldPresentation';
const MS_PER_DAY = 86_400_000;
@@ -70,19 +72,11 @@ function getRecommendationMetric(
}
function getRecommendationSourceType(entry: PlatformPublicGalleryCard) {
if ('sourceType' in entry) {
if (
entry.sourceType === 'edutainment' &&
'templateId' in entry &&
entry.templateId
) {
return `edutainment:${entry.templateId}`;
}
return entry.sourceType;
if (isEdutainmentGalleryEntry(entry)) {
return `edutainment:${entry.templateId}`;
}
return 'rpg';
return resolvePlatformPublicWorkSourceType(entry);
}
function getRecommendationThemeTags(entry: PlatformPublicGalleryCard) {

View File

@@ -42,7 +42,10 @@ import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
} from '../../../packages/shared/src/contracts/runtime';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import type {
WoodenFishGalleryCardResponse,
WoodenFishWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/woodenFish';
import { normalizeCustomWorldProfileRecord } from '../../data/customWorldLibrary';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import {
@@ -155,6 +158,7 @@ import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetail as getRpgEntryWorldGalleryDetailFromClient,
getRpgEntryWorldGalleryDetailByCode,
likeRpgEntryWorldGallery,
recordRpgEntryWorldGalleryPlay,
remixRpgEntryWorldGallery,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
@@ -538,6 +542,7 @@ const rpgEntryLibraryServiceMocks = vi.hoisted(() => ({
getRpgEntryWorldGalleryDetail: vi.fn(),
getRpgEntryWorldGalleryDetailByCode: vi.fn(),
getRpgEntryWorldLibraryDetail: vi.fn(),
likeRpgEntryWorldGallery: vi.fn(),
listRpgEntryWorldGallery: vi.fn(),
listRpgEntryWorldLibrary: vi.fn(),
publishRpgEntryWorldProfile: vi.fn(),
@@ -7365,6 +7370,75 @@ test('home recommendation share opens publish share modal', async () => {
.toBeTruthy();
});
test('home recommendation wooden fish like does not call RPG gallery like', async () => {
const user = userEvent.setup();
const publishedWoodenFishWork: WoodenFishGalleryCardResponse = {
publicWorkCode: 'WF-3A9EC89B',
workId: 'wooden-fish-work-like-1',
profileId: 'wooden-fish-profile-like-1',
ownerUserId: 'wooden-fish-user-1',
authorDisplayName: '木鱼作者',
workTitle: '莲台木鱼',
workDescription: '推荐页里的敲木鱼作品。',
coverImageSrc: null,
themeTags: ['敲木鱼'],
publicationStatus: 'published',
playCount: 0,
updatedAt: '2026-04-25T09:00:00.000Z',
publishedAt: '2026-04-25T09:00:00.000Z',
generationStatus: 'ready',
};
vi.mocked(woodenFishClient.listGallery).mockResolvedValue({
items: [publishedWoodenFishWork],
hasMore: false,
nextCursor: null,
});
vi.mocked(woodenFishClient.startRun).mockResolvedValue({
run: {
runId: 'wooden-fish-run-like-1',
profileId: publishedWoodenFishWork.profileId,
ownerUserId: publishedWoodenFishWork.ownerUserId,
status: 'playing',
totalTapCount: 0,
wordCounters: [],
startedAtMs: 1,
updatedAtMs: 1,
finishedAtMs: null,
},
});
vi.mocked(likeRpgEntryWorldGallery).mockResolvedValue(
buildMockRpgGalleryDetail({
ownerUserId: 'custom-world-user-1',
profileId: 'custom-world-profile-1',
publicWorkCode: 'CW-00000001',
authorPublicUserCode: 'SY-00000001',
visibility: 'published',
publishedAt: '2026-04-25T09:00:00.000Z',
updatedAt: '2026-04-25T09:00:00.000Z',
authorDisplayName: 'RPG 作者',
worldName: '不应被点赞的 RPG',
subtitle: '错误分流',
summaryText: 'WF 点赞不应进入这里。',
coverImageSrc: null,
themeMode: 'mythic',
playableNpcCount: 0,
landmarkCount: 0,
likeCount: 1,
}),
);
render(<TestWrapper withAuth />);
const meta = await screen.findByLabelText('莲台木鱼 作品信息');
await user.click(within(meta).getByRole('button', { name: '点赞 0' }));
expect(likeRpgEntryWorldGallery).not.toHaveBeenCalled();
expect(
await screen.findByText('作品类型 wooden-fish 暂不支持点赞。'),
).toBeTruthy();
});
test('home recommendation keeps logged-in puzzle start on default auth instead of guest token', async () => {
const publishedPuzzleWork = {
workId: 'puzzle-work-public-2',
@@ -7467,12 +7541,6 @@ test('logged out home recommendation next starts the next puzzle work', async ()
/>,
);
const recommendNavButton = document.querySelector<HTMLButtonElement>(
'.platform-bottom-nav [aria-label="推荐"]',
);
expect(recommendNavButton).toBeTruthy();
await user.click(recommendNavButton!);
await waitFor(() => {
expect(startPuzzleRun).toHaveBeenCalledWith(
{

View File

@@ -144,6 +144,7 @@ import {
formatPlatformWorldTime,
isBarkBattleGalleryEntry,
isBigFishGalleryEntry,
isCustomWorldGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isMatch3DGalleryEntry,
@@ -373,11 +374,15 @@ type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
type PlatformCategoryKindFilter =
| 'all'
| 'puzzle'
| 'puzzle-clear'
| 'jump-hop'
| 'wooden-fish'
| 'match3d'
| 'square-hole'
| 'visual-novel'
| 'bark-battle'
| 'big-fish'
| 'edutainment'
| 'custom-world';
type PlatformCategorySortMode = 'composite' | 'latest' | 'play' | 'like';
@@ -413,11 +418,15 @@ const PLATFORM_CATEGORY_KIND_FILTERS: Array<{
}> = [
{ id: 'all', label: '全部' },
{ id: 'puzzle', label: '拼图' },
{ id: 'puzzle-clear', label: '拼消' },
{ id: 'jump-hop', label: '跳一跳' },
{ id: 'wooden-fish', label: '木鱼' },
{ id: 'match3d', label: '抓鹅' },
{ id: 'square-hole', label: '方洞' },
{ id: 'visual-novel', label: '视觉' },
{ id: 'bark-battle', label: '汪汪' },
{ id: 'big-fish', label: '大鱼' },
{ id: 'edutainment', label: EDUTAINMENT_WORK_TAG },
{ id: 'custom-world', label: 'RPG' },
];
const PLATFORM_CATEGORY_SORT_OPTIONS: Array<{
@@ -2192,6 +2201,18 @@ function getPlatformCategoryKindFilter(entry: PlatformPublicGalleryCard) {
return 'puzzle';
}
if (isPuzzleClearGalleryEntry(entry)) {
return 'puzzle-clear';
}
if (isJumpHopGalleryEntry(entry)) {
return 'jump-hop';
}
if (isWoodenFishGalleryEntry(entry)) {
return 'wooden-fish';
}
if (isMatch3DGalleryEntry(entry)) {
return 'match3d';
}
@@ -2212,7 +2233,15 @@ function getPlatformCategoryKindFilter(entry: PlatformPublicGalleryCard) {
return 'big-fish';
}
return 'custom-world';
if (isEdutainmentGalleryEntry(entry)) {
return 'edutainment';
}
if (isCustomWorldGalleryEntry(entry)) {
return 'custom-world';
}
throw new Error('未知公开作品类型。');
}
function matchesPlatformCategoryKindFilter(

View File

@@ -23,6 +23,7 @@ import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
} from '../../../packages/shared/src/contracts/runtime';
import type { PublicWorkSourceType } from '../../../packages/shared/src/contracts/playTypes';
import type {
SquareHoleHoleOption,
SquareHoleShapeOption,
@@ -55,8 +56,12 @@ export const PLATFORM_WORK_TAG_DISPLAY_LIMIT = 4;
export const EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID = 'baby-object-match';
export const EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME = '宝贝识物';
export type PlatformCustomWorldGalleryCard = CustomWorldGalleryCard & {
sourceType?: 'custom-world';
};
export type PlatformWorldCardLike =
| CustomWorldGalleryCard
| PlatformCustomWorldGalleryCard
| CustomWorldLibraryEntry<CustomWorldProfile>
| PlatformBigFishGalleryCard
| PlatformMatch3DGalleryCard
@@ -319,7 +324,7 @@ export type PlatformBarkBattleGalleryCard = {
};
export type PlatformPublicGalleryCard =
| CustomWorldGalleryCard
| PlatformCustomWorldGalleryCard
| PlatformBigFishGalleryCard
| PlatformMatch3DGalleryCard
| PlatformSquareHoleGalleryCard
@@ -337,6 +342,14 @@ export function isLibraryWorldEntry(
return 'profile' in entry;
}
export function isCustomWorldGalleryEntry(
entry: PlatformWorldCardLike,
): entry is PlatformCustomWorldGalleryCard {
return !isLibraryWorldEntry(entry) && !('sourceType' in entry)
? true
: 'sourceType' in entry && entry.sourceType === 'custom-world';
}
export function isPuzzleGalleryEntry(
entry: PlatformWorldCardLike,
): entry is PlatformPuzzleGalleryCard {
@@ -397,28 +410,62 @@ export function isBarkBattleGalleryEntry(
return 'sourceType' in entry && entry.sourceType === 'bark-battle';
}
export function resolvePlatformPublicWorkSourceType(
entry: PlatformPublicGalleryCard,
): PublicWorkSourceType {
if (isCustomWorldGalleryEntry(entry)) {
return 'custom-world';
}
if (isBigFishGalleryEntry(entry)) {
return 'big-fish';
}
if (isPuzzleGalleryEntry(entry)) {
return 'puzzle';
}
if (isPuzzleClearGalleryEntry(entry)) {
return 'puzzle-clear';
}
if (isJumpHopGalleryEntry(entry)) {
return 'jump-hop';
}
if (isWoodenFishGalleryEntry(entry)) {
return 'wooden-fish';
}
if (isMatch3DGalleryEntry(entry)) {
return 'match3d';
}
if (isSquareHoleGalleryEntry(entry)) {
return 'square-hole';
}
if (isVisualNovelGalleryEntry(entry)) {
return 'visual-novel';
}
if (isBarkBattleGalleryEntry(entry)) {
return 'bark-battle';
}
if (isEdutainmentGalleryEntry(entry)) {
return 'edutainment';
}
throw new Error('未知公开作品类型。');
}
export function buildPlatformPublicGalleryCardKey(
entry: PlatformPublicGalleryCard,
) {
const kind = isBigFishGalleryEntry(entry)
? 'big-fish'
: isPuzzleGalleryEntry(entry)
? 'puzzle'
: isJumpHopGalleryEntry(entry)
? 'jump-hop'
: isWoodenFishGalleryEntry(entry)
? 'wooden-fish'
: isMatch3DGalleryEntry(entry)
? 'match3d'
: isSquareHoleGalleryEntry(entry)
? 'square-hole'
: isVisualNovelGalleryEntry(entry)
? 'visual-novel'
: isBarkBattleGalleryEntry(entry)
? 'bark-battle'
: isEdutainmentGalleryEntry(entry)
? `edutainment:${entry.templateId}`
: 'rpg';
const kind = isEdutainmentGalleryEntry(entry)
? `edutainment:${entry.templateId}`
: resolvePlatformPublicWorkSourceType(entry);
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
}
@@ -868,7 +915,11 @@ export function resolvePlatformWorldFallbackCoverImage(
return '/creation-type-references/bark-battle.webp';
}
return '/creation-type-references/rpg.webp';
if (isCustomWorldGalleryEntry(entry) || isLibraryWorldEntry(entry)) {
return '/creation-type-references/rpg.webp';
}
throw new Error('未知公开作品类型。');
}
export function resolvePlatformWorldCoverSlides(

View File

@@ -16,6 +16,7 @@ describe('unified creation specs', () => {
'jump-hop',
'match3d',
'puzzle',
'puzzle-clear',
'rpg',
'square-hole',
'visual-novel',
@@ -47,6 +48,11 @@ describe('unified creation specs', () => {
generationStage: 'puzzle-generating',
resultStage: 'puzzle-result',
});
expect(getUnifiedCreationSpec('puzzle-clear')).toMatchObject({
workspaceStage: 'puzzle-clear-workspace',
generationStage: 'puzzle-clear-generating',
resultStage: 'puzzle-clear-result',
});
expect(getUnifiedCreationSpec('match3d')).toMatchObject({
title: '抓大鹅',
workspaceStage: 'match3d-agent-workspace',

View File

@@ -2,11 +2,13 @@ import type {
CreationEntryTypeConfig,
UnifiedCreationSpec,
} from '../../services/creationEntryConfigService';
import type { PlatformCreationTypeId } from '../../../packages/shared/src/contracts/playTypes';
export const UNIFIED_CREATION_PLAY_IDS = [
'rpg',
'big-fish',
'puzzle',
'puzzle-clear',
'match3d',
'jump-hop',
'wooden-fish',
@@ -15,7 +17,7 @@ export const UNIFIED_CREATION_PLAY_IDS = [
'visual-novel',
'baby-object-match',
'creative-agent',
] as const;
] as const satisfies readonly PlatformCreationTypeId[];
export type UnifiedCreationPlayId =
(typeof UNIFIED_CREATION_PLAY_IDS)[number];
@@ -82,6 +84,33 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record<
},
],
},
'puzzle-clear': {
playId: 'puzzle-clear',
title: '想做个什么玩法?',
workspaceStage: 'puzzle-clear-workspace',
generationStage: 'puzzle-clear-generating',
resultStage: 'puzzle-clear-result',
fields: [
{
id: 'title',
kind: 'text',
label: '作品标题',
required: true,
},
{
id: 'themePrompt',
kind: 'text',
label: '主题',
required: true,
},
{
id: 'backgroundReferenceImage',
kind: 'image',
label: '参考图',
required: false,
},
],
},
match3d: {
playId: 'match3d',
title: '抓大鹅',