This commit is contained in:
2026-05-14 01:11:58 +08:00
parent b13870f71b
commit 5a55180b78
61 changed files with 5050 additions and 1057 deletions

View File

@@ -22,7 +22,15 @@ vi.mock('../../hooks/useResolvedAssetReadUrl', () => ({
}));
vi.mock('../ResolvedAssetImage', () => ({
ResolvedAssetImage: () => null,
ResolvedAssetImage: ({
src,
alt,
className,
}: {
src?: string | null;
alt?: string;
className?: string;
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
}));
const mocapMock = vi.hoisted(() => ({
@@ -623,6 +631,36 @@ test('顶部不显示作者,关卡标题和倒计时更紧凑', () => {
expect(screen.queryByText('等待下一关候选')).toBeNull();
});
test('运行态优先把关卡 UI 背景渲染为舞台背景', () => {
const runWithUiBackground: PuzzleRunSnapshot = {
...clearedRun,
currentLevel: {
...clearedRun.currentLevel!,
status: 'playing',
coverImageSrc: '/generated-puzzle-assets/session/cover.png',
uiBackgroundImageSrc:
'/generated-puzzle-assets/session/ui/background.png',
remainingMs: 300_000,
timeLimitMs: 300_000,
},
};
const { container } = renderPuzzleRuntime(
<PuzzleRuntimeShell
run={runWithUiBackground}
onBack={vi.fn()}
onSwapPieces={vi.fn()}
onDragPiece={vi.fn()}
onAdvanceNextLevel={vi.fn()}
/>,
);
const backgroundImage = container.querySelector(
'img[src="/generated-puzzle-assets/session/ui/background.png"]',
);
expect(backgroundImage).toBeTruthy();
});
test('关闭通关弹窗后保留底部下一关入口', () => {
vi.useFakeTimers();
const onAdvanceNextLevel = vi.fn();

View File

@@ -11,7 +11,7 @@ import {
Sparkles,
Trophy,
} from 'lucide-react';
import { useEffect, useId, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import type {
DragPuzzlePieceRequest,
@@ -472,6 +472,17 @@ export function PuzzleRuntimeShell({
const { resolvedUrl: resolvedUiBackgroundImage } = useResolvedAssetReadUrl(
currentLevel?.uiBackgroundImageSrc ?? null,
);
const tryPlayBackgroundMusic = useCallback(() => {
const audio = backgroundAudioRef.current;
if (!audio || !resolvedBackgroundMusicSrc || runtimeStatus !== 'playing') {
if (audio) {
audio.pause();
}
return;
}
audio.volume = Math.max(0, Math.min(1, musicVolume));
void audio.play().catch(() => {});
}, [musicVolume, resolvedBackgroundMusicSrc, runtimeStatus]);
const mocapInput = useMocapInput({enabled: runtimeStatus === 'playing'});
const primaryMocapHand = mocapInput.latestCommand?.primaryHand;
const primaryMocapHandState = primaryMocapHand?.state;
@@ -498,16 +509,8 @@ export function PuzzleRuntimeShell({
}, [currentLevel]);
useEffect(() => {
const audio = backgroundAudioRef.current;
if (!audio || !resolvedBackgroundMusicSrc || runtimeStatus !== 'playing') {
if (audio) {
audio.pause();
}
return;
}
audio.volume = Math.max(0, Math.min(1, musicVolume));
void audio.play().catch(() => {});
}, [musicVolume, resolvedBackgroundMusicSrc, runtimeStatus]);
tryPlayBackgroundMusic();
}, [tryPlayBackgroundMusic]);
const commitSelectedPieceId = (pieceId: string | null) => {
selectedPieceIdRef.current = pieceId;
@@ -949,6 +952,7 @@ export function PuzzleRuntimeShell({
if (isInteractionLocked) {
return;
}
tryPlayBackgroundMusic();
if (!selectedPieceIdBeforeInput) {
commitSelectedPieceId(pieceId);
@@ -1257,6 +1261,7 @@ export function PuzzleRuntimeShell({
if (isInteractionLocked) {
return;
}
tryPlayBackgroundMusic();
event.preventDefault();
resetDragInteraction();
event.currentTarget.setPointerCapture?.(event.pointerId);