fix: polish bark battle creation flow
This commit is contained in:
@@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { CreativeAgentSessionSnapshot } from '../../../packages/shared/src/contracts/creativeAgent';
|
||||
import type {
|
||||
@@ -43,7 +44,11 @@ import { ApiClientError } from '../../services/apiClient';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import {
|
||||
createBarkBattleDraft,
|
||||
generateAllBarkBattleImageAssets,
|
||||
listBarkBattleGallery,
|
||||
listBarkBattleWorks,
|
||||
publishBarkBattleWork,
|
||||
updateBarkBattleDraftConfig,
|
||||
} from '../../services/bark-battle-creation';
|
||||
import {
|
||||
createBigFishCreationSession,
|
||||
@@ -475,8 +480,12 @@ vi.mock('../../services/big-fish-runtime', () => ({
|
||||
|
||||
vi.mock('../../services/bark-battle-creation', () => ({
|
||||
createBarkBattleDraft: vi.fn(),
|
||||
generateAllBarkBattleImageAssets: vi.fn(),
|
||||
listBarkBattleGallery: vi.fn(),
|
||||
listBarkBattleWorks: vi.fn(),
|
||||
publishBarkBattleWork: vi.fn(),
|
||||
regenerateBarkBattleImageAsset: vi.fn(),
|
||||
updateBarkBattleDraftConfig: vi.fn(),
|
||||
uploadBarkBattleAsset: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -1000,11 +1009,10 @@ vi.mock('../bark-battle-creation/BarkBattleConfigEditor', () => ({
|
||||
onPreview: (payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
themePreset: string;
|
||||
playerDogSkinPreset: string;
|
||||
opponentDogSkinPreset: string;
|
||||
themeDescription: string;
|
||||
playerImageDescription: string;
|
||||
opponentImageDescription: string;
|
||||
difficultyPreset: 'normal';
|
||||
leaderboardEnabled: boolean;
|
||||
}) => void;
|
||||
}) => (
|
||||
<div className="bark-battle-config-editor-mock">
|
||||
@@ -1026,11 +1034,10 @@ vi.mock('../bark-battle-creation/BarkBattleConfigEditor', () => ({
|
||||
onPreview({
|
||||
title: '汪汪测试杯',
|
||||
description: '',
|
||||
themePreset: 'sunny-yard',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
themeDescription: '阳光草坪声浪竞技场',
|
||||
playerImageDescription: '戴红色围巾的柯基选手',
|
||||
opponentImageDescription: '蓝色护目镜哈士奇对手',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -1122,14 +1129,27 @@ vi.mock('../../games/bark-battle/ui/BarkBattleRuntimeShell', () => ({
|
||||
BarkBattleRuntimeShell: ({
|
||||
title,
|
||||
workId,
|
||||
runtimeMode,
|
||||
publishedConfig,
|
||||
onExit,
|
||||
}: {
|
||||
title?: string;
|
||||
workId?: string;
|
||||
runtimeMode?: string;
|
||||
publishedConfig?: { workId?: string; playerCharacterImageSrc?: string | null } | null;
|
||||
onExit?: () => void;
|
||||
}) => (
|
||||
<div className="bark-battle-runtime-shell-mock">
|
||||
<div>汪汪声浪运行态:{title ?? '未命名'} / {workId ?? 'missing-work'}</div>
|
||||
<div data-testid="bark-battle-runtime-mode">
|
||||
{runtimeMode ?? 'missing-mode'}
|
||||
</div>
|
||||
<div data-testid="bark-battle-runtime-work-id">
|
||||
{publishedConfig?.workId ?? 'missing-config-work'}
|
||||
</div>
|
||||
<div data-testid="bark-battle-runtime-player-src">
|
||||
{publishedConfig?.playerCharacterImageSrc ?? 'missing-player-src'}
|
||||
</div>
|
||||
<button type="button" onClick={onExit}>
|
||||
返回配置
|
||||
</button>
|
||||
@@ -1311,6 +1331,34 @@ function buildMockBabyObjectMatchDraft(
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockBarkBattleWork(
|
||||
overrides: Partial<BarkBattleWorkSummary> = {},
|
||||
): BarkBattleWorkSummary {
|
||||
return {
|
||||
workId: 'BB-C661A45F',
|
||||
draftId: 'bark-battle-draft-public-1',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试玩家',
|
||||
title: '汪汪公开杯',
|
||||
summary: '',
|
||||
themeDescription: '霓虹城市公园里的声浪擂台',
|
||||
playerImageDescription: '戴红围巾的柴犬主角',
|
||||
opponentImageDescription: '戴蓝色头带的哈士奇对手',
|
||||
playerCharacterImageSrc: '/generated-bark-battle/player.png',
|
||||
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
|
||||
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
|
||||
difficultyPreset: 'normal',
|
||||
status: 'published',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 0,
|
||||
finishCount: 0,
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
publishedAt: '2026-05-14T10:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockSquareHoleAgentSession(
|
||||
overrides: Partial<
|
||||
Parameters<typeof buildMockSquareHoleAgentSessionImpl>[0]
|
||||
@@ -2837,15 +2885,61 @@ beforeEach(() => {
|
||||
workId: 'bark-battle-work-1',
|
||||
title: '汪汪测试杯',
|
||||
description: '',
|
||||
themePreset: 'sunny-yard',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
themeDescription: '阳光草坪声浪竞技场',
|
||||
playerImageDescription: '戴红色围巾的柯基选手',
|
||||
opponentImageDescription: '蓝色护目镜哈士奇对手',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
configVersion: 1,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
});
|
||||
vi.mocked(generateAllBarkBattleImageAssets).mockResolvedValue({
|
||||
assets: {
|
||||
'player-character': {
|
||||
imageSrc: '/generated-bark-battle/player.png',
|
||||
assetId: 'asset-player',
|
||||
model: 'gpt-image-2-all',
|
||||
size: '1024*1024',
|
||||
taskId: 'task-player',
|
||||
prompt: 'player',
|
||||
},
|
||||
'opponent-character': {
|
||||
imageSrc: '/generated-bark-battle/opponent.png',
|
||||
assetId: 'asset-opponent',
|
||||
model: 'gpt-image-2-all',
|
||||
size: '1024*1024',
|
||||
taskId: 'task-opponent',
|
||||
prompt: 'opponent',
|
||||
},
|
||||
'ui-background': {
|
||||
imageSrc: '/generated-bark-battle/background.png',
|
||||
assetId: 'asset-background',
|
||||
model: 'gpt-image-2-all',
|
||||
size: '1024*1792',
|
||||
taskId: 'task-background',
|
||||
prompt: 'background',
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
vi.mocked(updateBarkBattleDraftConfig).mockImplementation(async (payload) => ({
|
||||
draftId: payload.draftId,
|
||||
workId: payload.workId ?? 'bark-battle-work-1',
|
||||
title: payload.title,
|
||||
description: payload.description,
|
||||
themeDescription: payload.themeDescription,
|
||||
playerImageDescription: payload.playerImageDescription,
|
||||
opponentImageDescription: payload.opponentImageDescription,
|
||||
playerCharacterImageSrc: payload.playerCharacterImageSrc,
|
||||
opponentCharacterImageSrc: payload.opponentCharacterImageSrc,
|
||||
uiBackgroundImageSrc: payload.uiBackgroundImageSrc,
|
||||
difficultyPreset: payload.difficultyPreset,
|
||||
configVersion: (payload.configVersion ?? 1) + 1,
|
||||
rulesetVersion: payload.rulesetVersion ?? 'bark-battle-ruleset-v1',
|
||||
updatedAt: '2026-05-14T10:01:00.000Z',
|
||||
}));
|
||||
vi.mocked(listBarkBattleWorks).mockResolvedValue({ items: [] });
|
||||
vi.mocked(listBarkBattleGallery).mockResolvedValue({ items: [] });
|
||||
vi.mocked(publishBarkBattleWork).mockResolvedValue({
|
||||
workId: 'bark-battle-work-1',
|
||||
draftId: 'bark-battle-draft-1',
|
||||
@@ -2854,11 +2948,10 @@ beforeEach(() => {
|
||||
playTypeId: 'bark-battle',
|
||||
title: '汪汪测试杯',
|
||||
description: '',
|
||||
themePreset: 'sunny-yard',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
themeDescription: '阳光草坪声浪竞技场',
|
||||
playerImageDescription: '戴红色围巾的柯基选手',
|
||||
opponentImageDescription: '蓝色护目镜哈士奇对手',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
publishedAt: '2026-05-14T10:00:00.000Z',
|
||||
});
|
||||
@@ -3233,6 +3326,9 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
|
||||
await openCreateTemplateHub(user);
|
||||
|
||||
expect(screen.getByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(screen.getByRole('tablist', { name: '选择模板' }).className).toContain(
|
||||
'scroll-px-3',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
@@ -3309,7 +3405,7 @@ test('create tab switches bark battle into the embedded config form', async () =
|
||||
expect(publishBarkBattleWork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('bark battle draft result can test before publish and return to the embedded form', async () => {
|
||||
test('bark battle draft result can test before publish and publish to work detail', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
@@ -3321,11 +3417,21 @@ test('bark battle draft result can test before publish and return to the embedde
|
||||
expect(createBarkBattleDraft).toHaveBeenCalledWith({
|
||||
title: '汪汪测试杯',
|
||||
description: '',
|
||||
themePreset: 'sunny-yard',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
themeDescription: '阳光草坪声浪竞技场',
|
||||
playerImageDescription: '戴红色围巾的柯基选手',
|
||||
opponentImageDescription: '蓝色护目镜哈士奇对手',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(updateBarkBattleDraftConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
draftId: 'bark-battle-draft-1',
|
||||
workId: 'bark-battle-work-1',
|
||||
playerCharacterImageSrc: '/generated-bark-battle/player.png',
|
||||
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
|
||||
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText(/汪汪声浪结果页:汪汪测试杯/u)).toBeTruthy();
|
||||
expect(await screen.findByText('作品ID:bark-battle-work-1')).toBeTruthy();
|
||||
@@ -3345,17 +3451,154 @@ test('bark battle draft result can test before publish and return to the embedde
|
||||
workId: 'bark-battle-work-1',
|
||||
publishedSnapshot: expect.objectContaining({
|
||||
title: '汪汪测试杯',
|
||||
leaderboardEnabled: true,
|
||||
themeDescription: '阳光草坪声浪竞技场',
|
||||
playerImageDescription: '戴红色围巾的柯基选手',
|
||||
opponentImageDescription: '蓝色护目镜哈士奇对手',
|
||||
}),
|
||||
});
|
||||
expect(await screen.findByText(/汪汪声浪运行态:汪汪测试杯/u)).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(window.location.pathname).toBe('/works/detail');
|
||||
expect(window.location.search).toBe('?work=BB-TLEWORK1');
|
||||
});
|
||||
expect(await screen.findByText('分享给朋友')).toBeTruthy();
|
||||
expect(screen.getByText(/作品号:BB-TLEWORK1/u)).toBeTruthy();
|
||||
expect(screen.queryByText(/汪汪声浪运行态:汪汪测试杯/u)).toBeNull();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回配置' }));
|
||||
test('direct bark battle runtime public code opens published runtime', async () => {
|
||||
const publicWork = buildMockBarkBattleWork();
|
||||
vi.mocked(listBarkBattleGallery).mockResolvedValueOnce({
|
||||
items: [publicWork],
|
||||
});
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/runtime/bark-battle?work=BB-C661A45F',
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
expect(await screen.findByText(/汪汪声浪运行态:汪汪公开杯/u)).toBeTruthy();
|
||||
expect(screen.getByTestId('bark-battle-runtime-mode').textContent).toBe(
|
||||
'published',
|
||||
);
|
||||
expect(screen.getByTestId('bark-battle-runtime-work-id').textContent).toBe(
|
||||
'BB-C661A45F',
|
||||
);
|
||||
expect(screen.getByTestId('bark-battle-runtime-player-src').textContent).toBe(
|
||||
'/generated-bark-battle/player.png',
|
||||
);
|
||||
expect(screen.queryByText('分享给朋友')).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
|
||||
test('bark battle form checks mud points before creating image assets', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getProfileDashboard).mockResolvedValue({
|
||||
walletBalance: 2,
|
||||
totalPlayTimeMs: 0,
|
||||
playedWorldCount: 0,
|
||||
updatedAt: '2026-05-14T10:00:00.000Z',
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
expect(await screen.findByText('汪汪声浪配置表单')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '汪汪声浪' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
await screen.findByText('泥点不足,本次需要 3 泥点,当前 2 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(createBarkBattleDraft).not.toHaveBeenCalled();
|
||||
expect(generateAllBarkBattleImageAssets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('bark battle draft is visible in draft shelf while image assets are generating', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(generateAllBarkBattleImageAssets).mockImplementation(
|
||||
() =>
|
||||
new Promise<Awaited<ReturnType<typeof generateAllBarkBattleImageAssets>>>(
|
||||
() => undefined,
|
||||
),
|
||||
);
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
expect(await screen.findByText('自动生成素材')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '返回编辑' }));
|
||||
await openDraftHub(user);
|
||||
|
||||
const panel = getPlatformTabPanel('saves');
|
||||
expect(
|
||||
await within(panel).findByRole('button', {
|
||||
name: /继续创作《汪汪测试杯》/u,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(1);
|
||||
expect(listBarkBattleWorks).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('published bark battle stays visible when refresh temporarily returns only the duplicate draft', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(listBarkBattleWorks).mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
workId: 'bark-battle-work-1',
|
||||
draftId: 'bark-battle-draft-1',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '测试玩家',
|
||||
title: '汪汪测试杯',
|
||||
summary: '',
|
||||
themeDescription: '阳光草坪声浪竞技场',
|
||||
playerImageDescription: '戴红色围巾的柯基选手',
|
||||
opponentImageDescription: '蓝色护目镜哈士奇对手',
|
||||
playerCharacterImageSrc: '/generated-bark-battle/player.png',
|
||||
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
|
||||
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
|
||||
difficultyPreset: 'normal',
|
||||
status: 'draft',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 0,
|
||||
updatedAt: '2026-05-14T10:01:00.000Z',
|
||||
publishedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '汪汪声浪' }));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
await waitFor(() => {
|
||||
expect(updateBarkBattleDraftConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
draftId: 'bark-battle-draft-1',
|
||||
workId: 'bark-battle-work-1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText(/汪汪声浪结果页:汪汪测试杯/u)).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '发布' }));
|
||||
await waitFor(() => {
|
||||
expect(window.location.pathname).toBe('/works/detail');
|
||||
});
|
||||
await user.click(await screen.findByRole('button', { name: '返回' }));
|
||||
|
||||
await openDraftHub(user);
|
||||
const panel = getPlatformTabPanel('saves');
|
||||
await user.click(within(panel).getByRole('button', { name: /已发布/u }));
|
||||
|
||||
expect(await within(panel).findByText('汪汪测试杯')).toBeTruthy();
|
||||
expect(within(panel).getByRole('button', { name: /查看详情《汪汪测试杯》/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('running match3d form generation can return to draft tab and reopen progress', async () => {
|
||||
@@ -4590,7 +4833,7 @@ test('completed match3d draft notice first opens trial then reopens result', asy
|
||||
).toHaveProperty('textContent', '1');
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
expect(await screen.findByText('抓大鹅结果页')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
await user.click(await screen.findByRole('button', { name: '返回' }));
|
||||
|
||||
await openDraftHub(user);
|
||||
expect(screen.queryByLabelText('新生成完成')).toBeNull();
|
||||
@@ -4638,7 +4881,7 @@ test('completed baby object match draft viewed immediately does not keep unread
|
||||
expect(screen.queryByText('宝贝识物结果页')).toBeNull();
|
||||
});
|
||||
expect(await screen.findByLabelText('物品 A')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
await user.click(await screen.findByRole('button', { name: '返回' }));
|
||||
|
||||
await openDraftHub(user);
|
||||
expect(
|
||||
|
||||
@@ -118,6 +118,7 @@ import {
|
||||
findPublicWorkForHistoryEntry,
|
||||
isEdutainmentEntryEnabled,
|
||||
} from '../platform-entry/platformEdutainmentVisibility';
|
||||
import { CustomWorldCoverArtwork } from '../CustomWorldCoverArtwork';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
|
||||
import {
|
||||
@@ -126,6 +127,7 @@ import {
|
||||
formatPlatformWorkDisplayName,
|
||||
formatPlatformWorkDisplayTag,
|
||||
formatPlatformWorldTime,
|
||||
isBarkBattleGalleryEntry,
|
||||
isBigFishGalleryEntry,
|
||||
isEdutainmentGalleryEntry,
|
||||
isMatch3DGalleryEntry,
|
||||
@@ -263,6 +265,7 @@ type PlatformCategoryKindFilter =
|
||||
| 'match3d'
|
||||
| 'square-hole'
|
||||
| 'visual-novel'
|
||||
| 'bark-battle'
|
||||
| 'big-fish'
|
||||
| 'custom-world';
|
||||
type PlatformCategorySortMode = 'composite' | 'latest' | 'play' | 'like';
|
||||
@@ -302,6 +305,7 @@ const PLATFORM_CATEGORY_KIND_FILTERS: Array<{
|
||||
{ id: 'match3d', label: '抓鹅' },
|
||||
{ id: 'square-hole', label: '方洞' },
|
||||
{ id: 'visual-novel', label: '视觉' },
|
||||
{ id: 'bark-battle', label: '汪汪' },
|
||||
{ id: 'big-fish', label: '大鱼' },
|
||||
{ id: 'custom-world', label: 'RPG' },
|
||||
];
|
||||
@@ -415,6 +419,43 @@ function ResolvedAssetBackdrop({
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformWorkCoverArtwork({
|
||||
entry,
|
||||
imageSrc,
|
||||
fallbackSrc,
|
||||
alt,
|
||||
className,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
imageSrc?: string | null;
|
||||
fallbackSrc?: string | null;
|
||||
alt: string;
|
||||
className: string;
|
||||
}) {
|
||||
if (isBarkBattleGalleryEntry(entry)) {
|
||||
return (
|
||||
<CustomWorldCoverArtwork
|
||||
imageSrc={imageSrc}
|
||||
fallbackImageSrc={fallbackSrc}
|
||||
title={entry.worldName}
|
||||
fallbackLabel="封面"
|
||||
renderMode={entry.coverRenderMode}
|
||||
characterImageSrcs={entry.coverCharacterImageSrcs}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResolvedAssetBackdrop
|
||||
src={imageSrc}
|
||||
fallbackSrc={fallbackSrc}
|
||||
alt={alt}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({ title, detail }: { title: string; detail: string }) {
|
||||
return (
|
||||
<div className="mb-3">
|
||||
@@ -609,8 +650,9 @@ function WorldCard({
|
||||
>
|
||||
<div className="platform-public-work-card__cover relative aspect-video overflow-hidden">
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
<PlatformWorkCoverArtwork
|
||||
entry={entry}
|
||||
imageSrc={coverImage}
|
||||
fallbackSrc={fallbackAssetCoverImage}
|
||||
alt={entry.worldName}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
@@ -713,8 +755,9 @@ function RecommendCoverOnlyCard({
|
||||
className="platform-recommend-cover-only"
|
||||
>
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
<PlatformWorkCoverArtwork
|
||||
entry={entry}
|
||||
imageSrc={coverImage}
|
||||
fallbackSrc={fallbackCoverImage}
|
||||
alt={entry.worldName}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
@@ -873,8 +916,9 @@ function RecommendRuntimePreviewCard({
|
||||
data-preview-position={position}
|
||||
>
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
<PlatformWorkCoverArtwork
|
||||
entry={entry}
|
||||
imageSrc={coverImage}
|
||||
alt=""
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
@@ -1258,8 +1302,9 @@ function DesktopTrendingItem({
|
||||
>
|
||||
<div className="relative h-[5.5rem] w-[4.3rem] shrink-0 overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-[rgba(255,255,255,0.66)]">
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
<PlatformWorkCoverArtwork
|
||||
entry={entry}
|
||||
imageSrc={coverImage}
|
||||
alt={entry.worldName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
@@ -1339,8 +1384,9 @@ function PlatformRankingItem({
|
||||
<div className="platform-ranking-item__rank">{rank}</div>
|
||||
<div className="platform-ranking-item__cover">
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
<PlatformWorkCoverArtwork
|
||||
entry={entry}
|
||||
imageSrc={coverImage}
|
||||
alt={entry.worldName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
@@ -1406,8 +1452,9 @@ function PlatformCategoryGameItem({
|
||||
>
|
||||
<div className="platform-category-game-item__cover">
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
<PlatformWorkCoverArtwork
|
||||
entry={entry}
|
||||
imageSrc={coverImage}
|
||||
alt={entry.worldName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
@@ -1732,9 +1779,11 @@ function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
|
||||
? 'square-hole'
|
||||
: isVisualNovelGalleryEntry(entry)
|
||||
? 'visual-novel'
|
||||
: isEdutainmentGalleryEntry(entry)
|
||||
? `edutainment:${entry.templateId}`
|
||||
: 'rpg';
|
||||
: isBarkBattleGalleryEntry(entry)
|
||||
? 'bark-battle'
|
||||
: isEdutainmentGalleryEntry(entry)
|
||||
? `edutainment:${entry.templateId}`
|
||||
: 'rpg';
|
||||
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
|
||||
}
|
||||
|
||||
@@ -1846,9 +1895,11 @@ function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
|
||||
? '方洞'
|
||||
: isVisualNovelGalleryEntry(entry)
|
||||
? '视觉'
|
||||
: isEdutainmentGalleryEntry(entry)
|
||||
? entry.templateName
|
||||
: describePlatformThemeLabel(entry.themeMode);
|
||||
: isBarkBattleGalleryEntry(entry)
|
||||
? '汪汪'
|
||||
: isEdutainmentGalleryEntry(entry)
|
||||
? entry.templateName
|
||||
: describePlatformThemeLabel(entry.themeMode);
|
||||
return formatPlatformWorkDisplayTag(kind);
|
||||
}
|
||||
|
||||
@@ -2016,6 +2067,10 @@ function getPlatformCategoryKindFilter(entry: PlatformPublicGalleryCard) {
|
||||
return 'visual-novel';
|
||||
}
|
||||
|
||||
if (isBarkBattleGalleryEntry(entry)) {
|
||||
return 'bark-battle';
|
||||
}
|
||||
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
return 'big-fish';
|
||||
}
|
||||
@@ -5949,12 +6004,21 @@ export function RpgEntryHomeView({
|
||||
className={`${HERO_SURFACE_CLASS} relative block overflow-hidden px-7 py-6 text-left`}
|
||||
>
|
||||
{desktopHeroCover ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={desktopHeroCover}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-34"
|
||||
/>
|
||||
desktopHeroEntry ? (
|
||||
<PlatformWorkCoverArtwork
|
||||
entry={desktopHeroEntry}
|
||||
imageSrc={desktopHeroCover}
|
||||
alt=""
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-34"
|
||||
/>
|
||||
) : (
|
||||
<ResolvedAssetBackdrop
|
||||
src={desktopHeroCover}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-34"
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
|
||||
<div className="relative z-10 flex min-h-[24rem] flex-col justify-between">
|
||||
@@ -5998,10 +6062,10 @@ export function RpgEntryHomeView({
|
||||
>
|
||||
<div className="relative aspect-[1.35/1] overflow-hidden">
|
||||
{coverImage ? (
|
||||
<ResolvedAssetBackdrop
|
||||
src={coverImage}
|
||||
<PlatformWorkCoverArtwork
|
||||
entry={entry}
|
||||
imageSrc={coverImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
formatPlatformWorkDisplayName,
|
||||
formatPlatformWorkDisplayTags,
|
||||
formatPlatformWorldTime,
|
||||
isBarkBattleGalleryEntry,
|
||||
isEdutainmentGalleryEntry,
|
||||
isVisualNovelGalleryEntry,
|
||||
mapBabyObjectMatchDraftToPlatformGalleryCard,
|
||||
mapBarkBattleWorkToPlatformGalleryCard,
|
||||
mapVisualNovelWorkToPlatformGalleryCard,
|
||||
type PlatformEdutainmentGalleryCard,
|
||||
type PlatformPuzzleGalleryCard,
|
||||
@@ -235,3 +237,98 @@ test('maps baby object match draft to edutainment public card', () => {
|
||||
expect(card.coverImageSrc).toBe('/apple.png');
|
||||
expect(card.themeTags[0]).toBe('寓教于乐');
|
||||
});
|
||||
|
||||
test('maps bark battle work to BB public card with scene roles cover', () => {
|
||||
const card = mapBarkBattleWorkToPlatformGalleryCard({
|
||||
workId: 'bark-battle-work-abcdef12',
|
||||
draftId: 'bark-battle-draft-1',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '玩家',
|
||||
title: '公园声浪赛',
|
||||
summary: '柯基和哈士奇比拼声浪。',
|
||||
themeDescription: '傍晚公园擂台',
|
||||
playerImageDescription: '红围巾柯基',
|
||||
opponentImageDescription: '蓝头带哈士奇',
|
||||
playerCharacterImageSrc: '/generated-bark-battle/player.png',
|
||||
opponentCharacterImageSrc: '/generated-bark-battle/opponent.png',
|
||||
uiBackgroundImageSrc: '/generated-bark-battle/background.png',
|
||||
difficultyPreset: 'hard',
|
||||
status: 'published',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 9,
|
||||
recentPlayCount7d: 4,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: '2026-05-20T00:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(isBarkBattleGalleryEntry(card)).toBe(true);
|
||||
expect(card.publicWorkCode).toBe('BB-ABCDEF12');
|
||||
expect(resolvePlatformPublicWorkCode(card)).toBe('BB-ABCDEF12');
|
||||
expect(card.coverImageSrc).toBe('/generated-bark-battle/background.png');
|
||||
expect(card.coverRenderMode).toBe('scene_with_roles');
|
||||
expect(card.coverCharacterImageSrcs).toEqual([
|
||||
'/generated-bark-battle/player.png',
|
||||
'/generated-bark-battle/opponent.png',
|
||||
]);
|
||||
expect(buildPlatformWorldDisplayTags(card, 3)).toEqual([
|
||||
'汪汪声浪',
|
||||
'高能',
|
||||
'傍晚公园',
|
||||
]);
|
||||
});
|
||||
|
||||
test('maps bark battle public card cover from character or reference fallback', () => {
|
||||
const characterCoverCard = mapBarkBattleWorkToPlatformGalleryCard({
|
||||
workId: 'BB-COVER001',
|
||||
draftId: 'bark-battle-draft-cover',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '玩家',
|
||||
title: '角色封面赛',
|
||||
summary: '',
|
||||
themeDescription: '草地声浪挑战',
|
||||
playerImageDescription: '柯基选手',
|
||||
opponentImageDescription: '哈士奇对手',
|
||||
playerCharacterImageSrc: '/bark/player-cover.png',
|
||||
opponentCharacterImageSrc: '/bark/opponent-cover.png',
|
||||
uiBackgroundImageSrc: null,
|
||||
difficultyPreset: 'normal',
|
||||
status: 'published',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 1,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: '2026-05-20T00:00:00.000Z',
|
||||
});
|
||||
const fallbackCoverCard = mapBarkBattleWorkToPlatformGalleryCard({
|
||||
workId: 'BB-COVER002',
|
||||
draftId: 'bark-battle-draft-cover-fallback',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '玩家',
|
||||
title: '默认封面赛',
|
||||
summary: '',
|
||||
themeDescription: '夜市声浪挑战',
|
||||
playerImageDescription: '柴犬选手',
|
||||
opponentImageDescription: '机器人对手',
|
||||
playerCharacterImageSrc: null,
|
||||
opponentCharacterImageSrc: null,
|
||||
uiBackgroundImageSrc: null,
|
||||
difficultyPreset: 'easy',
|
||||
status: 'published',
|
||||
generationStatus: 'ready',
|
||||
publishReady: true,
|
||||
playCount: 1,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: '2026-05-20T00:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(characterCoverCard.coverImageSrc).toBe('/bark/player-cover.png');
|
||||
expect(characterCoverCard.coverCharacterImageSrcs).toEqual([
|
||||
'/bark/player-cover.png',
|
||||
'/bark/opponent-cover.png',
|
||||
]);
|
||||
expect(fallbackCoverCard.coverImageSrc).toBe(
|
||||
'/creation-type-references/bark-battle.webp',
|
||||
);
|
||||
expect(fallbackCoverCard.publicWorkCode).toBe('BB-COVER002');
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import { BABY_OBJECT_MATCH_EDUTAINMENT_TAG } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
@@ -22,6 +23,7 @@ import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets'
|
||||
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
||||
import {
|
||||
buildBabyObjectMatchPublicWorkCode,
|
||||
buildBarkBattlePublicWorkCode,
|
||||
buildBigFishPublicWorkCode,
|
||||
buildMatch3DPublicWorkCode,
|
||||
buildPuzzlePublicWorkCode,
|
||||
@@ -43,6 +45,7 @@ export type PlatformWorldCardLike =
|
||||
| PlatformSquareHoleGalleryCard
|
||||
| PlatformPuzzleGalleryCard
|
||||
| PlatformVisualNovelGalleryCard
|
||||
| PlatformBarkBattleGalleryCard
|
||||
| PlatformEdutainmentGalleryCard;
|
||||
|
||||
export type PlatformPuzzleGalleryCard = {
|
||||
@@ -196,6 +199,34 @@ export type PlatformEdutainmentGalleryCard = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type PlatformBarkBattleGalleryCard = {
|
||||
sourceType: 'bark-battle';
|
||||
workId: string;
|
||||
profileId: string;
|
||||
sourceSessionId?: string | null;
|
||||
publicWorkCode: string;
|
||||
ownerUserId: string;
|
||||
authorPublicUserCode: string | null;
|
||||
authorDisplayName: string;
|
||||
worldName: string;
|
||||
subtitle: string;
|
||||
summaryText: string;
|
||||
coverImageSrc: string | null;
|
||||
coverRenderMode: 'image' | 'scene_with_roles';
|
||||
coverCharacterImageSrcs: string[];
|
||||
themeTags: string[];
|
||||
themeMode: CustomWorldGalleryCard['themeMode'];
|
||||
playableNpcCount: number;
|
||||
landmarkCount: number;
|
||||
playCount?: number;
|
||||
remixCount?: number;
|
||||
likeCount?: number;
|
||||
recentPlayCount7d?: number;
|
||||
visibility: 'published';
|
||||
publishedAt: string | null;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type PlatformPublicGalleryCard =
|
||||
| CustomWorldGalleryCard
|
||||
| PlatformBigFishGalleryCard
|
||||
@@ -203,6 +234,7 @@ export type PlatformPublicGalleryCard =
|
||||
| PlatformSquareHoleGalleryCard
|
||||
| PlatformPuzzleGalleryCard
|
||||
| PlatformVisualNovelGalleryCard
|
||||
| PlatformBarkBattleGalleryCard
|
||||
| PlatformEdutainmentGalleryCard;
|
||||
|
||||
export function isLibraryWorldEntry(
|
||||
@@ -247,6 +279,12 @@ export function isEdutainmentGalleryEntry(
|
||||
return 'sourceType' in entry && entry.sourceType === 'edutainment';
|
||||
}
|
||||
|
||||
export function isBarkBattleGalleryEntry(
|
||||
entry: PlatformWorldCardLike,
|
||||
): entry is PlatformBarkBattleGalleryCard {
|
||||
return 'sourceType' in entry && entry.sourceType === 'bark-battle';
|
||||
}
|
||||
|
||||
export function mapPuzzleWorkToPlatformGalleryCard(
|
||||
work: PuzzleWorkSummary,
|
||||
): PlatformPuzzleGalleryCard {
|
||||
@@ -422,6 +460,64 @@ export function mapBabyObjectMatchDraftToPlatformGalleryCard(
|
||||
};
|
||||
}
|
||||
|
||||
export function mapBarkBattleWorkToPlatformGalleryCard(
|
||||
work: BarkBattleWorkSummary,
|
||||
): PlatformBarkBattleGalleryCard {
|
||||
const playerCharacterImageSrc = normalizePlatformOptionalImageSrc(
|
||||
work.playerCharacterImageSrc,
|
||||
);
|
||||
const opponentCharacterImageSrc = normalizePlatformOptionalImageSrc(
|
||||
work.opponentCharacterImageSrc,
|
||||
);
|
||||
const backgroundImageSrc = normalizePlatformOptionalImageSrc(
|
||||
work.uiBackgroundImageSrc,
|
||||
);
|
||||
const coverImageSrc =
|
||||
backgroundImageSrc ??
|
||||
playerCharacterImageSrc ??
|
||||
opponentCharacterImageSrc ??
|
||||
'/creation-type-references/bark-battle.webp';
|
||||
const coverCharacterImageSrcs = [
|
||||
playerCharacterImageSrc,
|
||||
opponentCharacterImageSrc,
|
||||
].filter((imageSrc): imageSrc is string => Boolean(imageSrc));
|
||||
const canRenderSceneWithRoles =
|
||||
Boolean(backgroundImageSrc) && coverCharacterImageSrcs.length >= 2;
|
||||
|
||||
return {
|
||||
sourceType: 'bark-battle',
|
||||
workId: work.workId,
|
||||
profileId: work.workId,
|
||||
sourceSessionId: work.draftId ?? null,
|
||||
publicWorkCode: buildBarkBattlePublicWorkCode(work.workId),
|
||||
ownerUserId: work.ownerUserId,
|
||||
authorPublicUserCode: null,
|
||||
authorDisplayName: work.authorDisplayName,
|
||||
worldName: work.title.trim() || '汪汪声浪大作战',
|
||||
subtitle: `汪汪声浪 · ${describeBarkBattleDifficultyLabel(
|
||||
work.difficultyPreset,
|
||||
)}`,
|
||||
summaryText:
|
||||
work.summary.trim() ||
|
||||
work.themeDescription.trim() ||
|
||||
'用声音能量挑战对手。',
|
||||
coverImageSrc,
|
||||
coverRenderMode: canRenderSceneWithRoles ? 'scene_with_roles' : 'image',
|
||||
coverCharacterImageSrcs,
|
||||
themeTags: buildBarkBattleThemeTags(work),
|
||||
themeMode: 'martial',
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
playCount: work.playCount ?? 0,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
recentPlayCount7d: work.recentPlayCount7d ?? 0,
|
||||
visibility: 'published',
|
||||
publishedAt: work.publishedAt ?? null,
|
||||
updatedAt: work.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePlatformWorldStats(entry: PlatformWorldCardLike) {
|
||||
return {
|
||||
playCount: 'playCount' in entry ? (entry.playCount ?? 0) : 0,
|
||||
@@ -473,6 +569,10 @@ export function resolvePlatformWorldFallbackCoverImage(
|
||||
return '/creation-type-references/creative-agent.webp';
|
||||
}
|
||||
|
||||
if (isBarkBattleGalleryEntry(entry)) {
|
||||
return '/creation-type-references/bark-battle.webp';
|
||||
}
|
||||
|
||||
return '/creation-type-references/rpg.webp';
|
||||
}
|
||||
|
||||
@@ -634,6 +734,12 @@ export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
|
||||
: [entry.templateName];
|
||||
}
|
||||
|
||||
if (isBarkBattleGalleryEntry(entry)) {
|
||||
return entry.themeTags.length > 0
|
||||
? entry.themeTags.slice(0, 3)
|
||||
: ['汪汪声浪'];
|
||||
}
|
||||
|
||||
if (!isLibraryWorldEntry(entry)) {
|
||||
return [
|
||||
describePlatformThemeLabel(entry.themeMode),
|
||||
@@ -724,6 +830,10 @@ export function resolvePlatformPublicWorkCode(
|
||||
return entry.publicWorkCode;
|
||||
}
|
||||
|
||||
if (isBarkBattleGalleryEntry(entry)) {
|
||||
return entry.publicWorkCode;
|
||||
}
|
||||
|
||||
return entry.publicWorkCode;
|
||||
}
|
||||
|
||||
@@ -745,3 +855,31 @@ export function describePlatformThemeLabel(
|
||||
return '回响';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePlatformOptionalImageSrc(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function describeBarkBattleDifficultyLabel(
|
||||
difficulty: BarkBattleWorkSummary['difficultyPreset'],
|
||||
) {
|
||||
switch (difficulty) {
|
||||
case 'easy':
|
||||
return '轻松';
|
||||
case 'hard':
|
||||
return '高能';
|
||||
default:
|
||||
return '普通';
|
||||
}
|
||||
}
|
||||
|
||||
function buildBarkBattleThemeTags(work: BarkBattleWorkSummary) {
|
||||
return [
|
||||
'汪汪声浪',
|
||||
describeBarkBattleDifficultyLabel(work.difficultyPreset),
|
||||
work.themeDescription,
|
||||
]
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user