feat(jump-hop): redesign sling platform gameplay
This commit is contained in:
@@ -13,6 +13,10 @@ import type {
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
CustomWorldWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
JumpHopRuntimeRunSnapshotResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type {
|
||||
BabyObjectMatchDraft,
|
||||
CreateBabyObjectMatchDraftRequest,
|
||||
@@ -63,6 +67,7 @@ import {
|
||||
submitBigFishInput,
|
||||
} from '../../services/big-fish-runtime';
|
||||
import { listBigFishWorks } from '../../services/big-fish-works';
|
||||
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
|
||||
import {
|
||||
type CreationEntryConfig,
|
||||
fetchCreationEntryConfig,
|
||||
@@ -167,6 +172,7 @@ import {
|
||||
} from '../../services/square-hole-works';
|
||||
import { listVisualNovelGallery } from '../../services/visual-novel-runtime';
|
||||
import { listVisualNovelWorks } from '../../services/visual-novel-works';
|
||||
import { woodenFishClient } from '../../services/wooden-fish/woodenFishClient';
|
||||
import { type CustomWorldProfile, WorldType } from '../../types';
|
||||
import {
|
||||
AuthUiContext,
|
||||
@@ -579,6 +585,42 @@ vi.mock('../../services/puzzle-runtime', () => ({
|
||||
usePuzzleRuntimeProp: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
|
||||
jumpHopClient: {
|
||||
getGalleryDetail: vi.fn(),
|
||||
getLeaderboard: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getWorkDetail: vi.fn(),
|
||||
listGallery: vi.fn(),
|
||||
listWorks: vi.fn(),
|
||||
publishWork: vi.fn(),
|
||||
restartRun: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
submitJump: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
|
||||
woodenFishClient: {
|
||||
checkpointRun: vi.fn(),
|
||||
createSession: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
finishRun: vi.fn(),
|
||||
getGalleryDetail: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
getWorkDetail: vi.fn(),
|
||||
listGallery: vi.fn(async () => ({
|
||||
hasMore: false,
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
})),
|
||||
listWorks: vi.fn(async () => ({ items: [] })),
|
||||
publishWork: vi.fn(),
|
||||
restartRun: vi.fn(),
|
||||
startRun: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/rpg-entry/rpgEntryLibraryClient', () => ({
|
||||
...rpgEntryLibraryServiceMocks,
|
||||
}));
|
||||
@@ -2518,6 +2560,12 @@ beforeEach(() => {
|
||||
vi.mocked(deleteRpgEntryWorldProfile).mockResolvedValue([]);
|
||||
vi.mocked(listVisualNovelGallery).mockResolvedValue({ works: [] });
|
||||
vi.mocked(listVisualNovelWorks).mockResolvedValue({ works: [] });
|
||||
vi.mocked(woodenFishClient.listGallery).mockResolvedValue({
|
||||
hasMore: false,
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
});
|
||||
vi.mocked(woodenFishClient.listWorks).mockResolvedValue({ items: [] });
|
||||
vi.mocked(listLocalBabyObjectMatchDrafts).mockResolvedValue([]);
|
||||
vi.mocked(deleteLocalBabyObjectMatchDraft).mockResolvedValue([]);
|
||||
vi.mocked(saveBabyObjectMatchDraft).mockImplementation(async (payload) => ({
|
||||
@@ -4557,7 +4605,7 @@ test('match3d result trial passes generated models into first runtime mount', as
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-draft-1',
|
||||
{},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -4650,7 +4698,7 @@ test('match3d result trial passes generated 2D image views into first runtime mo
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-draft-2d-1',
|
||||
{},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -5680,10 +5728,10 @@ test('opening a compiled draft with a missing agent session falls back to draft
|
||||
await waitFor(() => {
|
||||
expect(fallbackDraftPanel.getAttribute('aria-hidden')).toBe('false');
|
||||
expect(
|
||||
within(fallbackDraftPanel).getByText(
|
||||
screen.getAllByText(
|
||||
'这份共创草稿已失效,已为你返回草稿列表,请重新开始创作。',
|
||||
),
|
||||
).toBeTruthy();
|
||||
).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(window.location.search).toBe('');
|
||||
@@ -5799,6 +5847,213 @@ test('logged out public detail gates puzzle start and remix before real actions'
|
||||
expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('logged out public jump-hop detail starts runtime without requireAuth', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
const publishedJumpHopWork: JumpHopWorkProfileResponse = {
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: 'jump-hop-work-public-1',
|
||||
profileId: 'jump-hop-profile-public-12345678',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'jump-hop-session-public-1',
|
||||
themeText: '云上方块',
|
||||
workTitle: '云上方块跳一跳',
|
||||
workDescription: '在云层地块之间连续弹跳。',
|
||||
themeTags: ['云层', '跳跃'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'published',
|
||||
playCount: 3,
|
||||
updatedAt: '2026-05-29T10:00:00.000Z',
|
||||
publishedAt: '2026-05-29T10:00:00.000Z',
|
||||
publishReady: true,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
draft: {
|
||||
templateId: 'jump-hop',
|
||||
templateName: '跳一跳',
|
||||
profileId: 'jump-hop-profile-public-12345678',
|
||||
themeText: '云上方块',
|
||||
workTitle: '云上方块跳一跳',
|
||||
workDescription: '在云层地块之间连续弹跳。',
|
||||
themeTags: ['云层', '跳跃'],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'paper-toy',
|
||||
defaultCharacter: {
|
||||
characterId: 'builtin-default',
|
||||
displayName: '默认角色',
|
||||
modelKind: 'builtin-three',
|
||||
bodyColor: '#df7f40',
|
||||
accentColor: '#2563eb',
|
||||
},
|
||||
characterPrompt: '',
|
||||
tilePrompt: '云上方块',
|
||||
endMoodPrompt: null,
|
||||
characterAsset: null,
|
||||
tileAtlasAsset: null,
|
||||
tileAssets: [],
|
||||
path: null,
|
||||
coverComposite: null,
|
||||
generationStatus: 'ready',
|
||||
},
|
||||
path: {
|
||||
seed: 'jump-hop-public-seed',
|
||||
difficulty: 'standard',
|
||||
platforms: [
|
||||
{
|
||||
platformId: 'platform-0',
|
||||
tileType: 'start',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
landingRadius: 0.7,
|
||||
perfectRadius: 0.25,
|
||||
scoreValue: 1,
|
||||
},
|
||||
{
|
||||
platformId: 'platform-1',
|
||||
tileType: 'normal',
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
landingRadius: 0.7,
|
||||
perfectRadius: 0.25,
|
||||
scoreValue: 1,
|
||||
},
|
||||
{
|
||||
platformId: 'platform-2',
|
||||
tileType: 'normal',
|
||||
x: -1,
|
||||
y: 2,
|
||||
width: 1,
|
||||
height: 1,
|
||||
landingRadius: 0.7,
|
||||
perfectRadius: 0.25,
|
||||
scoreValue: 1,
|
||||
},
|
||||
],
|
||||
finishIndex: 2,
|
||||
cameraPreset: 'portrait-top-down',
|
||||
scoring: {
|
||||
chargeToDistanceRatio: 1,
|
||||
maxChargeMs: 1800,
|
||||
hitBonus: 0,
|
||||
perfectBonus: 0,
|
||||
},
|
||||
},
|
||||
defaultCharacter: {
|
||||
characterId: 'builtin-default',
|
||||
displayName: '默认角色',
|
||||
modelKind: 'builtin-three',
|
||||
bodyColor: '#df7f40',
|
||||
accentColor: '#2563eb',
|
||||
},
|
||||
characterAsset: {
|
||||
assetId: 'builtin-character',
|
||||
imageSrc: '',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: '',
|
||||
generationProvider: 'builtin',
|
||||
prompt: '',
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'tile-atlas-1',
|
||||
imageSrc: '/generated-jump-hop-assets/public/atlas.png',
|
||||
imageObjectKey: 'generated-jump-hop-assets/public/atlas.png',
|
||||
assetObjectId: 'asset-tile-atlas-1',
|
||||
generationProvider: 'gpt-image-2',
|
||||
prompt: '云上方块',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
tileAssets: [],
|
||||
};
|
||||
const publishedJumpHopRun: JumpHopRuntimeRunSnapshotResponse = {
|
||||
runId: 'jump-hop-run-public-1',
|
||||
profileId: publishedJumpHopWork.summary.profileId,
|
||||
ownerUserId: '',
|
||||
status: 'playing',
|
||||
currentPlatformIndex: 0,
|
||||
successfulJumpCount: 0,
|
||||
durationMs: 0,
|
||||
score: 0,
|
||||
combo: 0,
|
||||
path: publishedJumpHopWork.path,
|
||||
lastJump: null,
|
||||
startedAtMs: 1_779_999_000_000,
|
||||
finishedAtMs: null,
|
||||
};
|
||||
|
||||
window.history.replaceState(null, '', '/works/detail?work=JH-12345678');
|
||||
vi.mocked(jumpHopClient.listGallery).mockResolvedValue({
|
||||
items: [
|
||||
{
|
||||
publicWorkCode: 'JH-12345678',
|
||||
workId: publishedJumpHopWork.summary.workId,
|
||||
profileId: publishedJumpHopWork.summary.profileId,
|
||||
ownerUserId: publishedJumpHopWork.summary.ownerUserId,
|
||||
authorDisplayName: '跳跃作者',
|
||||
themeText: publishedJumpHopWork.summary.themeText,
|
||||
workTitle: publishedJumpHopWork.summary.workTitle,
|
||||
workDescription: publishedJumpHopWork.summary.workDescription,
|
||||
coverImageSrc: null,
|
||||
themeTags: publishedJumpHopWork.summary.themeTags,
|
||||
difficulty: publishedJumpHopWork.summary.difficulty,
|
||||
stylePreset: publishedJumpHopWork.summary.stylePreset,
|
||||
publicationStatus: publishedJumpHopWork.summary.publicationStatus,
|
||||
playCount: publishedJumpHopWork.summary.playCount,
|
||||
updatedAt: publishedJumpHopWork.summary.updatedAt,
|
||||
publishedAt: publishedJumpHopWork.summary.publishedAt,
|
||||
generationStatus: publishedJumpHopWork.summary.generationStatus,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
nextCursor: null,
|
||||
});
|
||||
vi.mocked(jumpHopClient.getWorkDetail).mockResolvedValue({
|
||||
item: publishedJumpHopWork,
|
||||
});
|
||||
vi.mocked(jumpHopClient.startRun).mockResolvedValue({
|
||||
run: publishedJumpHopRun,
|
||||
});
|
||||
vi.mocked(jumpHopClient.getLeaderboard).mockResolvedValue({
|
||||
profileId: publishedJumpHopWork.summary.profileId,
|
||||
items: [],
|
||||
viewerBest: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
authValue={createAuthValue({
|
||||
user: null,
|
||||
canAccessProtectedData: false,
|
||||
openLoginModal: () => {},
|
||||
requireAuth,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('详情')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '启动' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(jumpHopClient.startRun).toHaveBeenCalledWith(
|
||||
publishedJumpHopWork.summary.profileId,
|
||||
expect.objectContaining({
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
runtimeMode: 'published',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(requireAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('owned public puzzle detail edits original draft instead of remixing', async () => {
|
||||
const user = userEvent.setup();
|
||||
const ownedPuzzleWork = {
|
||||
@@ -7239,8 +7494,8 @@ test('embedded puzzle form maps raw bearer token errors to user-facing auth copy
|
||||
expect(createPuzzleAgentSession).toHaveBeenCalledTimes(1);
|
||||
expect(createCreativeAgentSession).not.toHaveBeenCalled();
|
||||
expect(
|
||||
await screen.findByText('当前登录状态已失效,请重新登录后继续。'),
|
||||
).toBeTruthy();
|
||||
(await screen.findAllByText('当前登录状态已失效,请重新登录后继续。')).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -8350,7 +8605,7 @@ test('public code search opens a published Match3D work by M3 code and starts ru
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-public-1',
|
||||
{},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -8422,7 +8677,7 @@ test('published Match3D runtime receives persisted generated models', async () =
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-model-1',
|
||||
{},
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -10697,8 +10952,9 @@ test('creation hub published work card reveals delete action after card action r
|
||||
publishedCard.focus();
|
||||
await user.keyboard('{ArrowLeft}');
|
||||
|
||||
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '删除' }));
|
||||
const deleteButtons = screen.getAllByRole('button', { name: '删除' });
|
||||
expect(deleteButtons.length).toBeGreaterThan(0);
|
||||
await user.click(deleteButtons[0]!);
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: '删除作品' });
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||
|
||||
Reference in New Issue
Block a user