feat(jump-hop): optimize generated assets and runtime background

This commit is contained in:
2026-06-04 22:34:19 +08:00
parent c442c3c3f0
commit 0041b95f72
17 changed files with 1160 additions and 200 deletions

View File

@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { act, fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { beforeEach, expect, test, vi } from 'vitest';
import type {
@@ -229,7 +229,31 @@ test('跳一跳蓄力时角色沿拖拽方向拉伸', async () => {
);
});
test('跳一跳运行态需要三维场景宿主和排行榜面板', () => {
test('跳一跳运行态游玩中只保留得分并隐藏常驻排行榜', () => {
const runtimeRequestOptions = {
runtimeGuestToken: 'runtime-guest-token',
};
render(
<JumpHopRuntimeShell
profile={buildProfile()}
run={buildRun()}
runtimeRequestOptions={runtimeRequestOptions}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
expect(useJumpHopLeaderboard).not.toHaveBeenCalled();
expect(screen.getByTestId('jump-hop-three-scene')).toBeTruthy();
expect(screen.queryByTestId('jump-hop-runtime-leaderboard')).toBeNull();
expect(screen.queryByRole('button', { name: /重开/ })).toBeNull();
expect(screen.queryByText('进行中')).toBeNull();
expect(screen.queryByText('00:00')).toBeNull();
expect(screen.queryByRole('button', { name: /^起跳$/ })).toBeNull();
});
test('跳一跳运行态失败后在弹窗中展示排行榜', () => {
const runtimeRequestOptions = {
runtimeGuestToken: 'runtime-guest-token',
};
@@ -255,7 +279,7 @@ test('跳一跳运行态需要三维场景宿主和排行榜面板', () => {
render(
<JumpHopRuntimeShell
profile={buildProfile()}
run={buildRun()}
run={buildFailedRun()}
runtimeRequestOptions={runtimeRequestOptions}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
@@ -266,12 +290,12 @@ test('跳一跳运行态需要三维场景宿主和排行榜面板', () => {
'jump-hop-profile-test',
runtimeRequestOptions,
);
expect(screen.getByTestId('jump-hop-three-scene')).toBeTruthy();
expect(screen.getByTestId('jump-hop-runtime-leaderboard')).toBeTruthy();
expect(screen.getByText('player-1')).toBeTruthy();
expect(screen.getByText('8 跳')).toBeTruthy();
expect(screen.getByText('00:08')).toBeTruthy();
expect(screen.queryByRole('button', { name: /^起跳$/ })).toBeNull();
expect(screen.getByRole('dialog', { name: '失败' })).toBeTruthy();
const leaderboard = screen.getByTestId('jump-hop-runtime-leaderboard');
expect(leaderboard).toBeTruthy();
expect(within(leaderboard).getByText('player-1')).toBeTruthy();
expect(within(leaderboard).getByText('8 跳')).toBeTruthy();
expect(within(leaderboard).getByText('00:08')).toBeTruthy();
});
test('跳一跳角色层永远压在地块层之上', () => {
@@ -356,6 +380,7 @@ test('跳一跳运行态直接渲染生成的地块切片图片', () => {
const tileImages = screen.getAllByTestId('jump-hop-tile-image');
expect(tileImages).toHaveLength(3);
expect(document.querySelector('.jump-hop-runtime__fallback-tile')).toBeNull();
const generatedReadUrlCalls = vi
.mocked(useResolvedAssetReadUrl)
.mock.calls.filter(([source]) =>
@@ -379,6 +404,25 @@ test('跳一跳运行态直接渲染生成的地块切片图片', () => {
}
});
test('跳一跳运行态提前预加载下一屏地块且不在真实图片加载前露出原型方块', () => {
render(
<JumpHopRuntimeShell
profile={buildProfile({ tileAssets: buildTileAssets() })}
run={buildRunWithExtraPreviewPlatform()}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
expect(screen.getAllByTestId('jump-hop-tile-image')).toHaveLength(3);
expect(document.querySelector('.jump-hop-runtime__fallback-tile')).toBeNull();
const preloadImages = screen.getAllByTestId('jump-hop-tile-preload-image');
expect(preloadImages.length).toBeGreaterThan(0);
expect(preloadImages[0]?.getAttribute('src')).toContain(
'/generated-jump-hop-assets/jump-hop-profile-test/tile-',
);
});
test('跳一跳运行态首块地块落在中下方并且后续两块向中央和上方展开', () => {
render(
<JumpHopRuntimeShell
@@ -679,6 +723,14 @@ test('跳一跳松手后先播放飞行动画再切换到下一块地块', async
expect(styleText).toMatch(
/data-platform-advancing='true'\]\s+\.jump-hop-runtime__platform[\s\S]*transform 1440ms cubic-bezier/,
);
expect(document.querySelector('.jump-hop-runtime__fallback-tile')).toBeNull();
const advancingCharacterRule = styleText.match(
/\.jump-hop-runtime__stage\[data-platform-advancing='true'\]\s+\.jump-hop-runtime__character\s*\{(?<body>[\s\S]*?)\}/,
)?.groups?.body;
expect(advancingCharacterRule).toContain('transform 120ms ease');
expect(advancingCharacterRule).toContain('opacity 160ms ease');
expect(advancingCharacterRule).not.toContain('left');
expect(advancingCharacterRule).not.toContain('top');
expect(screen.getByTestId('jump-hop-three-scene').parentElement).toBe(
cameraLayer,
);
@@ -823,6 +875,50 @@ function buildRun(): JumpHopRuntimeRunSnapshotResponse {
};
}
function buildFailedRun(): JumpHopRuntimeRunSnapshotResponse {
return {
...buildRun(),
status: 'failed',
successfulJumpCount: 8,
durationMs: 8123,
score: 8,
combo: 0,
lastJump: {
chargeMs: 420,
jumpDistance: 1.62,
targetPlatformIndex: 1,
landedX: 0,
landedY: 0,
result: 'miss',
},
finishedAtMs: 9123,
};
}
function buildRunWithExtraPreviewPlatform(): JumpHopRuntimeRunSnapshotResponse {
const run = buildRun();
return {
...run,
path: {
...run.path,
platforms: [
...run.path.platforms,
{
platformId: 'p3',
tileType: 'normal',
x: 0.5,
y: 3.6,
width: 1,
height: 1,
landingRadius: 0.5,
perfectRadius: 0.2,
scoreValue: 1,
},
],
},
};
}
function buildTileAssets() {
return Array.from({ length: 25 }, (_, index) => {
const tileNumber = String(index + 1).padStart(2, '0');
@@ -845,6 +941,8 @@ function buildTileAssets() {
function buildProfile(options: {
tileAssets?: JumpHopWorkProfileResponse['tileAssets'];
coverComposite?: string | null;
coverImageSrc?: string | null;
} = {}): JumpHopWorkProfileResponse {
const characterAsset = {
assetId: 'builtin',
@@ -869,7 +967,7 @@ function buildProfile(options: {
themeTags: ['测试'],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
coverImageSrc: null,
coverImageSrc: options.coverImageSrc ?? null,
publicationStatus: 'draft',
playCount: 0,
updatedAt: '2026-05-27T00:00:00Z',
@@ -901,7 +999,7 @@ function buildProfile(options: {
tileAtlasAsset: characterAsset,
tileAssets: options.tileAssets ?? [],
path: buildRun().path,
coverComposite: null,
coverComposite: options.coverComposite ?? null,
generationStatus: 'ready',
},
path: buildRun().path,
@@ -917,3 +1015,46 @@ function buildProfile(options: {
tileAssets: options.tileAssets ?? [],
};
}
test('跳一跳运行态使用 image2 背景底图铺满舞台底层', () => {
const backgroundSource =
'/generated-jump-hop-assets/jump-hop-profile-test/background/image.png';
render(
<JumpHopRuntimeShell
profile={buildProfile({ coverComposite: backgroundSource })}
run={buildRun()}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
const backgroundImage = screen.getByTestId('jump-hop-stage-background-image');
expect(backgroundImage.getAttribute('src')).toBe(backgroundSource);
const backdrop = document.querySelector('.jump-hop-runtime__scene-backdrop');
expect(backdrop?.getAttribute('data-has-background')).toBe('true');
expect(useResolvedAssetReadUrl).toHaveBeenCalledWith(
backgroundSource,
expect.objectContaining({
refreshKey: backgroundSource,
}),
);
});
test('跳一跳运行态忽略旧 cover composite 占位背景', () => {
render(
<JumpHopRuntimeShell
profile={buildProfile({
coverComposite:
'/generated-jump-hop-assets/jump-hop-profile-test/cover-composite.png',
})}
run={buildRun()}
onJump={vi.fn().mockResolvedValue(undefined)}
onRestart={() => {}}
/>,
);
expect(screen.queryByTestId('jump-hop-stage-background-image')).toBeNull();
const backdrop = document.querySelector('.jump-hop-runtime__scene-backdrop');
expect(backdrop?.getAttribute('data-has-background')).toBe('false');
});

View File

@@ -1,4 +1,4 @@
import { ArrowLeft, Loader2, RotateCcw } from 'lucide-react';
import { ArrowLeft, Loader2 } from 'lucide-react';
import {
type CSSProperties,
type Dispatch,
@@ -29,6 +29,7 @@ import {
getJumpHopRunDurationMs,
getJumpHopStatusLabel,
getJumpHopTileTone,
selectJumpHopTileAsset,
type JumpHopCharacterVisualPosition,
type JumpHopVisiblePlatform,
resolveJumpHopCharacterCanvasPosition,
@@ -66,6 +67,7 @@ const JUMP_HOP_LANDING_RECOIL_DURATION_MS = 560;
const JUMP_HOP_PLATFORM_ADVANCE_DURATION_MS = 1440;
const JUMP_HOP_TAONIER_CHARACTER_IMAGE_SRC =
'/branding/jump-hop-taonier-character.png';
const JUMP_HOP_TILE_PRELOAD_LOOKAHEAD_COUNT = 3;
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
@@ -192,6 +194,21 @@ function IsometricFallbackTile({
);
}
function getJumpHopTileAssetRefreshKey(asset: JumpHopTileAsset | null) {
return asset?.assetObjectId || asset?.imageObjectKey || asset?.tileId || null;
}
function isJumpHopGeneratedBackgroundSource(source: string | null | undefined) {
const value = source?.trim() ?? '';
if (!value) {
return false;
}
return !(
value.startsWith('/generated-jump-hop-assets/') &&
(value.endsWith('/cover-composite.png') || value.includes('/cover-composite-'))
);
}
function JumpHopTileImage({
asset,
platform,
@@ -199,8 +216,7 @@ function JumpHopTileImage({
asset: JumpHopTileAsset | null;
platform: JumpHopVisiblePlatform['platform'];
}) {
const assetRefreshKey =
asset?.assetObjectId || asset?.imageObjectKey || asset?.tileId || null;
const assetRefreshKey = getJumpHopTileAssetRefreshKey(asset);
const { resolvedUrl } = useResolvedAssetReadUrl(asset?.imageSrc, {
refreshKey: assetRefreshKey,
});
@@ -212,12 +228,13 @@ function JumpHopTileImage({
setHasError(false);
}, [resolvedUrl]);
const shouldShowFallback = !resolvedUrl || !isLoaded || hasError;
const shouldShowImage = Boolean(resolvedUrl && !hasError);
const shouldShowFallback = !shouldShowImage;
return (
<div className="jump-hop-runtime__tile-image-stack">
{shouldShowFallback ? <IsometricFallbackTile platform={platform} /> : null}
{resolvedUrl && !hasError ? (
{shouldShowImage ? (
<img
src={resolvedUrl}
alt=""
@@ -225,7 +242,7 @@ function JumpHopTileImage({
data-testid="jump-hop-tile-image"
data-tile-id={asset?.tileId ?? asset?.sourceAtlasCell}
className="jump-hop-runtime__tile-image"
data-loaded={isLoaded ? 'true' : 'false'}
data-loaded={isLoaded || shouldShowImage ? 'true' : 'false'}
onLoad={() => {
setIsLoaded(true);
}}
@@ -238,6 +255,28 @@ function JumpHopTileImage({
);
}
function JumpHopTilePreloadImage({ asset }: { asset: JumpHopTileAsset }) {
const assetRefreshKey = getJumpHopTileAssetRefreshKey(asset);
const { resolvedUrl } = useResolvedAssetReadUrl(asset.imageSrc, {
refreshKey: assetRefreshKey,
});
if (!resolvedUrl) {
return null;
}
return (
<img
src={resolvedUrl}
alt=""
aria-hidden="true"
draggable={false}
data-testid="jump-hop-tile-preload-image"
className="jump-hop-runtime__tile-preload-image"
/>
);
}
function hasJumpHopWebGLSupport() {
if (import.meta.env.MODE === 'test') {
return false;
@@ -573,6 +612,16 @@ export function JumpHopRuntimeShell({
const displayRunRef = useRef(displayRun);
const visiblePlatformsRef = useRef<JumpHopVisiblePlatform[]>([]);
const tileAssetsRef = useRef(profile?.tileAssets);
const stageBackgroundSource = [
profile?.draft.coverComposite,
profile?.summary.coverImageSrc,
].find(isJumpHopGeneratedBackgroundSource);
const { resolvedUrl: stageBackgroundUrl } = useResolvedAssetReadUrl(
stageBackgroundSource,
{
refreshKey: stageBackgroundSource,
},
);
useEffect(() => {
activeRunRef.current = activeRun;
@@ -612,7 +661,7 @@ export function JumpHopRuntimeShell({
const platformRenderItems = useMemo(() => {
const exitingItems = platformAdvanceExitingPlatforms.map((item) => ({
...item,
renderKey: `${item.platform.platformId}-exiting`,
renderKey: item.platform.platformId,
advanceState: 'exiting' as const,
}));
const visibleItems = visiblePlatforms.map((item) => ({
@@ -627,6 +676,47 @@ export function JumpHopRuntimeShell({
platformAdvanceExitingPlatforms,
visiblePlatforms,
]);
const preloadTileAssets = useMemo(() => {
const path = stageRun?.path;
const tileAssets = profile?.tileAssets;
const platforms = path?.platforms ?? [];
const startIndex =
(stageRun?.currentPlatformIndex ?? 0) + visiblePlatforms.length;
const assets = new Map<string, JumpHopTileAsset>();
for (
let index = startIndex;
index <
Math.min(
platforms.length,
startIndex + JUMP_HOP_TILE_PRELOAD_LOOKAHEAD_COUNT,
);
index += 1
) {
const platform = platforms[index];
if (!platform) {
continue;
}
const asset = selectJumpHopTileAsset(
tileAssets,
path?.seed ?? null,
index,
platform.platformId,
);
if (!asset) {
continue;
}
const key = getJumpHopTileAssetRefreshKey(asset) ?? asset.imageSrc;
assets.set(key, asset);
}
return [...assets.values()];
}, [
profile?.tileAssets,
stageRun?.currentPlatformIndex,
stageRun?.path,
visiblePlatforms.length,
]);
const showLandingAssist =
import.meta.env.MODE !== 'production' && isCharging && !isJumpAnimating;
const characterPosition = getJumpHopCharacterVisualPosition(
@@ -753,6 +843,7 @@ export function JumpHopRuntimeShell({
const jumpFeedbackForDisplay = getJumpHopJumpFeedbackLabel(stageRun);
const isSettled =
stageRun?.status === 'failed' || stageRun?.status === 'cleared';
const shouldShowFailureLeaderboard = stageRun?.status === 'failed';
const successfulJumpCount = stageRun?.successfulJumpCount ?? 0;
const durationLabel = formatJumpHopDurationLabel(
getJumpHopRunDurationMs(stageRun, nowMs),
@@ -1219,29 +1310,19 @@ export function JumpHopRuntimeShell({
<div className="jump-hop-runtime__sky" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_18%,rgba(255,255,255,0.82),transparent_30%),linear-gradient(180deg,rgba(255,255,255,0.18),rgba(234,204,179,0.24))]" />
<header className="relative z-20 flex items-center justify-between gap-2 px-3 pb-2 pt-[max(0.75rem,env(safe-area-inset-top))] sm:px-4">
<header className="relative z-20 grid grid-cols-[1fr_auto_1fr] items-center gap-2 px-3 pb-2 pt-[max(0.75rem,env(safe-area-inset-top))] sm:px-4">
<button
type="button"
onClick={exitHandler}
className="platform-button platform-button--ghost min-h-0 rounded-full bg-white/80 px-3 py-2 text-sm shadow-sm backdrop-blur"
className="platform-button platform-button--ghost min-h-0 justify-self-start rounded-full bg-white/80 px-3 py-2 text-sm shadow-sm backdrop-blur"
>
<ArrowLeft className="h-4 w-4" />
</button>
<div className="flex items-center gap-2 rounded-full border border-white/70 bg-white/82 px-3 py-2 text-sm font-black shadow-sm backdrop-blur">
<span>{successfulJumpCount}</span>
<span className="h-1 w-1 rounded-full bg-slate-300" />
<span>{durationLabel}</span>
</div>
<button
type="button"
onClick={onRestart}
disabled={isBusy}
className="platform-button platform-button--ghost min-h-0 rounded-full bg-white/80 px-3 py-2 text-sm shadow-sm backdrop-blur"
>
<RotateCcw className="h-4 w-4" />
</button>
<div aria-hidden="true" />
</header>
<main className="relative z-10 mx-auto flex w-full max-w-[30rem] flex-1 flex-col px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:px-4">
@@ -1251,7 +1332,7 @@ export function JumpHopRuntimeShell({
data-charging={isCharging ? 'true' : 'false'}
data-jump-animating={isJumpAnimating ? 'true' : 'false'}
data-platform-advancing={isPlatformAdvancing ? 'true' : 'false'}
className="jump-hop-runtime__stage relative min-h-0 flex-1 touch-none select-none overflow-hidden rounded-[1.5rem] border border-white/70 bg-white/40 shadow-[0_24px_70px_rgba(44,125,182,0.2)]"
className="jump-hop-runtime__stage relative min-h-0 flex-1 touch-none select-none overflow-hidden rounded-[1.5rem]"
onPointerDown={beginCharge}
onPointerMove={updateDragVector}
onPointerUp={(event) => void finishCharge(event)}
@@ -1260,7 +1341,18 @@ export function JumpHopRuntimeShell({
<div
aria-hidden="true"
className="jump-hop-runtime__scene-backdrop"
/>
data-has-background={stageBackgroundUrl ? 'true' : 'false'}
>
{stageBackgroundUrl ? (
<img
src={stageBackgroundUrl}
alt=""
draggable={false}
data-testid="jump-hop-stage-background-image"
className="jump-hop-runtime__scene-background-image"
/>
) : null}
</div>
<div
data-testid="jump-hop-camera-layer"
data-platform-advancing={isPlatformAdvancing ? 'true' : 'false'}
@@ -1318,6 +1410,17 @@ export function JumpHopRuntimeShell({
);
})}
{preloadTileAssets.length > 0 ? (
<div className="jump-hop-runtime__tile-preload" aria-hidden="true">
{preloadTileAssets.map((asset) => (
<JumpHopTilePreloadImage
key={getJumpHopTileAssetRefreshKey(asset) ?? asset.imageSrc}
asset={asset}
/>
))}
</div>
) : null}
{visualCharacterPosition && !isThreeCharacterLayerReady ? (
<div
className="jump-hop-runtime__character"
@@ -1426,14 +1529,27 @@ export function JumpHopRuntimeShell({
{isSettled ? (
<div className="absolute inset-0 z-[120] grid place-items-center bg-slate-950/28 px-6 backdrop-blur-[2px]">
<div className="w-full max-w-[18rem] rounded-[1.25rem] border border-white/70 bg-white/90 p-4 text-center shadow-[0_18px_50px_rgba(15,23,42,0.22)]">
<div
role="dialog"
aria-modal="true"
aria-labelledby="jump-hop-result-title"
className="w-full max-w-[24rem] rounded-[1.25rem] border border-white/70 bg-white/90 p-4 text-center shadow-[0_18px_50px_rgba(15,23,42,0.22)]"
>
<div className="text-2xl font-black">
{getJumpHopStatusLabel(stageRun?.status)}
<span id="jump-hop-result-title">
{getJumpHopStatusLabel(stageRun?.status)}
</span>
</div>
<div className="mt-2 flex justify-center gap-4 text-sm font-bold text-slate-600">
<span>{successfulJumpCount} </span>
<span>{durationLabel}</span>
</div>
{shouldShowFailureLeaderboard ? (
<JumpHopLeaderboardPanel
profileId={profile?.summary.profileId}
runtimeRequestOptions={runtimeRequestOptions}
/>
) : null}
<div className="mt-4 grid grid-cols-2 gap-2">
<button
type="button"
@@ -1456,21 +1572,15 @@ export function JumpHopRuntimeShell({
) : null}
</section>
<footer className="relative z-20 mt-3 flex items-center justify-between gap-3">
<div className="min-w-0 text-sm font-bold text-slate-700">
{error ? (
{error ? (
<footer className="relative z-20 mt-3 flex items-center justify-between gap-3">
<div className="min-w-0 text-sm font-bold text-slate-700">
<span className="text-[var(--platform-button-danger-text)]">
{error}
</span>
) : (
<span>{getJumpHopStatusLabel(stageRun?.status)}</span>
)}
</div>
</footer>
<JumpHopLeaderboardPanel
profileId={profile?.summary.profileId}
runtimeRequestOptions={runtimeRequestOptions}
/>
</div>
</footer>
) : null}
</main>
<style>{`
@@ -1498,12 +1608,38 @@ export function JumpHopRuntimeShell({
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
background:
radial-gradient(circle at 18% 18%, rgba(253, 230, 138, 0.36), transparent 24%),
radial-gradient(circle at 82% 22%, rgba(226, 171, 134, 0.34), transparent 28%),
linear-gradient(180deg, #fffdf9 0%, #f8efe7 52%, #f4e5d7 100%);
}
.jump-hop-runtime__scene-backdrop[data-has-background='true'] {
background: #f8efe7;
}
.jump-hop-runtime__scene-backdrop[data-has-background='true']::after {
content: '';
position: absolute;
inset: 0;
z-index: 2;
background:
radial-gradient(circle at 50% 20%, rgba(255, 255, 255, 0.16), transparent 34%),
linear-gradient(180deg, rgba(255, 253, 249, 0.08), rgba(50, 34, 24, 0.08));
}
.jump-hop-runtime__scene-background-image {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 50%;
user-select: none;
}
.jump-hop-runtime__camera-layer {
position: absolute;
inset: 0;
@@ -1550,6 +1686,12 @@ export function JumpHopRuntimeShell({
opacity 220ms ease;
}
.jump-hop-runtime__stage[data-platform-advancing='true'] .jump-hop-runtime__character {
transition:
transform 120ms ease,
opacity 160ms ease;
}
.jump-hop-runtime__platform-shadow {
position: absolute;
left: 8%;
@@ -1583,6 +1725,22 @@ export function JumpHopRuntimeShell({
overflow: hidden;
}
.jump-hop-runtime__tile-preload {
position: absolute;
left: -9999px;
top: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.jump-hop-runtime__tile-preload-image {
width: 1px;
height: 1px;
}
.jump-hop-runtime__landing-assist {
position: absolute;
z-index: 110;

View File

@@ -60,12 +60,12 @@ test('jump hop workspace submits theme payload after required field is filled',
templateId: 'jump-hop',
themeText: '云朵跳台',
workTitle: '云朵跳台跳一跳',
workDescription: '云朵跳台主题的俯视角平台跳跃作品',
workDescription: '云朵跳台主题的俯视角跳跃作品',
themeTags: ['云朵跳台', '跳一跳', '休闲'],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
characterPrompt: '内置默认 3D 角色',
tilePrompt: '云朵跳台主题的俯视角清爽游戏化立体感平台素材',
tilePrompt: '云朵跳台主题的正面30度视角主题物体图集物体本身作为跳跃落点',
endMoodPrompt: null,
});
});

View File

@@ -35,12 +35,12 @@ function buildJumpHopWorkspacePayload(
templateId: 'jump-hop',
themeText,
workTitle: `${themeText}跳一跳`,
workDescription: `${themeText}主题的俯视角平台跳跃作品`,
workDescription: `${themeText}主题的俯视角跳跃作品`,
themeTags: [themeText, '跳一跳', '休闲'],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
characterPrompt: '内置默认 3D 角色',
tilePrompt: `${themeText}主题的俯视角清爽游戏化立体感平台素材`,
tilePrompt: `${themeText}主题的正面30度视角主题物体图集物体本身作为跳跃落点`,
endMoodPrompt: null,
};
}

View File

@@ -512,7 +512,7 @@ describe('miniGameDraftGenerationProgress', () => {
const entries = buildJumpHopGenerationAnchorEntries(null, {
themeText: '云端糖果塔',
templateId: 'jump-hop',
tilePrompt: '云端糖果塔主题的俯视角清爽游戏化立体感平台素材',
tilePrompt: '云端糖果塔主题的正面30度视角主题物体图集物体本身作为跳跃落点',
});
expect(entries).toEqual([
@@ -524,7 +524,7 @@ describe('miniGameDraftGenerationProgress', () => {
{
id: 'jump-hop-tile-style',
label: '地块图集',
value: '云端糖果塔主题的俯视角清爽游戏化立体感平台素材',
value: '云端糖果塔主题的正面30度视角主题物体图集物体本身作为跳跃落点',
},
]);
});