This commit is contained in:
2026-05-14 13:40:50 +08:00
parent 5a55180b78
commit 2dc9d752e4
24 changed files with 1873 additions and 98 deletions

View File

@@ -78,6 +78,7 @@ afterEach(() => {
__MATCH3D_KEEP_3D_TEST_RENDER__?: boolean;
}
).__MATCH3D_KEEP_3D_TEST_RENDER__;
vi.restoreAllMocks();
});
function renderRuntime(
@@ -475,6 +476,74 @@ test('运行态会换签并渲染抓大鹅中心容器 UI 图', async () => {
});
});
test('运行态从任意素材读取作品级背景音乐并换签播放', async () => {
const run = startLocalMatch3DRun(3);
const playSpy = vi
.spyOn(HTMLMediaElement.prototype, 'play')
.mockResolvedValue(undefined);
vi.spyOn(globalThis, 'fetch').mockImplementation((input) => {
const url = String(input);
const signedUrl = url.includes('legacyPublicPath')
? 'https://oss.example.com/match3d-music.mp3'
: 'https://oss.example.com/match3d-view.png';
return Promise.resolve(
new Response(
JSON.stringify({
read: {
signedUrl,
expiresAt: new Date(Date.now() + 60_000).toISOString(),
},
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
},
),
);
});
const generatedItemAssets: Match3DGeneratedItemAsset[] = [
{
itemId: 'match3d-item-1',
itemName: '草莓',
imageSrc: '/match3d/strawberry.png',
imageObjectKey: null,
imageViews: [],
status: 'image_ready',
modelSrc: null,
modelObjectKey: null,
},
{
itemId: 'match3d-item-2',
itemName: '苹果',
imageSrc: '/match3d/apple.png',
imageObjectKey: null,
imageViews: [],
status: 'image_ready',
modelSrc: null,
modelObjectKey: null,
backgroundMusic: {
taskId: 'music-task-1',
provider: 'vector-engine-suno',
assetObjectId: 'asset-music-1',
assetKind: 'match3d_background_music',
audioSrc: '/generated-match3d-assets/audio/music.mp3',
prompt: '',
title: '果园轻舞',
updatedAt: '2026-05-14T00:00:00.000Z',
},
},
];
renderRuntime(run, generatedItemAssets);
await waitFor(() => {
expect(screen.getByLabelText('抓大鹅背景音乐').getAttribute('src')).toBe(
'https://oss.example.com/match3d-music.mp3',
);
});
await waitFor(() => expect(playSpy).toHaveBeenCalled());
});
test('本地试玩按难度档位生成类型并兼容历史硬核消除数', () => {
const smallRun = startLocalMatch3DRun(12);
const hardRun = startLocalMatch3DRun(20);

View File

@@ -29,6 +29,7 @@ import {
} from '../../services/assetReadUrlService';
import {
getMatch3DGeneratedImageViewSources,
normalizeMatch3DGeneratedItemAssetsForRuntime,
} from '../../services/match3dGeneratedModelCache';
import {
DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG,
@@ -481,6 +482,10 @@ export function Match3DRuntimeShell({
useState('');
const musicVolume = authUi?.musicVolume ?? DEFAULT_MATCH3D_MUSIC_VOLUME;
const levelAudioConfig = DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG;
const runtimeGeneratedItemAssets = useMemo(
() => normalizeMatch3DGeneratedItemAssetsForRuntime(generatedItemAssets),
[generatedItemAssets],
);
useEffect(() => {
setTimeLeftMs(run?.remainingMs ?? 0);
@@ -559,7 +564,7 @@ export function Match3DRuntimeShell({
const backgroundAssetSrc =
backgroundImageSrc?.trim() ||
generatedItemAssets
runtimeGeneratedItemAssets
.map(
(asset) =>
asset.backgroundAsset?.imageSrc?.trim() ||
@@ -569,7 +574,7 @@ export function Match3DRuntimeShell({
.find(Boolean) ||
'';
const containerAssetSrc =
generatedItemAssets
runtimeGeneratedItemAssets
.map(
(asset) =>
asset.backgroundAsset?.containerImageSrc?.trim() ||
@@ -578,8 +583,8 @@ export function Match3DRuntimeShell({
)
.find(Boolean) || '';
const imageSourcesByType = useMemo(
() => buildMatch3DImageSourcesByType(run, generatedItemAssets),
[generatedItemAssets, run],
() => buildMatch3DImageSourcesByType(run, runtimeGeneratedItemAssets),
[runtimeGeneratedItemAssets, run],
);
const imageSourceSignature = useMemo(
() => buildMatch3DImageSourceSignature(imageSourcesByType),
@@ -597,7 +602,7 @@ export function Match3DRuntimeShell({
[imageSourcesByType, resolvedImageSources],
);
const backgroundMusicSrc =
generatedItemAssets.find((asset) => asset.backgroundMusic?.audioSrc)
runtimeGeneratedItemAssets.find((asset) => asset.backgroundMusic?.audioSrc)
?.backgroundMusic?.audioSrc ?? null;
const [resolvedBackgroundMusicSrc, setResolvedBackgroundMusicSrc] = useState('');
const [resolvedContainerImageSrc, setResolvedContainerImageSrc] = useState('');
@@ -605,7 +610,7 @@ export function Match3DRuntimeShell({
if (!run) {
return new Map<string, string>();
}
const readyAssets = generatedItemAssets.filter(
const readyAssets = runtimeGeneratedItemAssets.filter(
(asset) => asset.clickSound?.audioSrc,
);
const sortedTypes = [
@@ -617,7 +622,7 @@ export function Match3DRuntimeShell({
return src ? [[typeId, src] as const] : [];
}),
);
}, [generatedItemAssets, run]);
}, [runtimeGeneratedItemAssets, run]);
const tryPlayBackgroundMusic = useCallback(() => {
const audio = backgroundAudioRef.current;
@@ -879,6 +884,7 @@ export function Match3DRuntimeShell({
src={resolvedBackgroundMusicSrc}
loop
preload="auto"
aria-label="抓大鹅背景音乐"
/>
) : null}
<div