1
This commit is contained in:
@@ -35,6 +35,13 @@ import {
|
||||
type RuntimeDragInputSession,
|
||||
type RuntimeInputPoint,
|
||||
} from '../../services/input-devices';
|
||||
import {
|
||||
DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG,
|
||||
playRuntimeClickSound,
|
||||
playRuntimeCountdownSound,
|
||||
playRuntimeLevelClearSound,
|
||||
resolveRuntimeCountdownSecondBucket,
|
||||
} from '../../services/runtimeAudioFeedback';
|
||||
import { useMocapInput } from '../../services/useMocapInput';
|
||||
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../../uiAssets';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
@@ -326,6 +333,11 @@ function triggerPuzzlePiecePressHapticFeedback() {
|
||||
vibrate.call(navigator, [PUZZLE_PIECE_PRESS_HAPTIC_PATTERN_MS]);
|
||||
}
|
||||
|
||||
function triggerPuzzlePiecePressFeedback(volume: number) {
|
||||
triggerPuzzlePiecePressHapticFeedback();
|
||||
playRuntimeClickSound(undefined, volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼图运行时壳层。
|
||||
* 前端维护运行时即时交互:交换、拖动、合并、拆分与本关通关在前端裁决。
|
||||
@@ -378,6 +390,8 @@ export function PuzzleRuntimeShell({
|
||||
const previousUiPauseActiveRef = useRef(false);
|
||||
const pauseChangePromiseRef = useRef<Promise<void>>(Promise.resolve());
|
||||
const timeExpiredSyncKeyRef = useRef<string | null>(null);
|
||||
const clearSoundKeyRef = useRef<string | null>(null);
|
||||
const countdownSoundKeyRef = useRef<string | null>(null);
|
||||
const dragSessionRef = useRef<{
|
||||
pieceId: string;
|
||||
inputId: string;
|
||||
@@ -425,6 +439,7 @@ export function PuzzleRuntimeShell({
|
||||
const mergeGroupSignatureRef = useRef<string | null>(null);
|
||||
const hintDemoTimeoutRef = useRef<number | null>(null);
|
||||
const mergeFlashTimeoutRef = useRef<number | null>(null);
|
||||
const backgroundAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const boardRef = useRef<HTMLDivElement | null>(null);
|
||||
const currentLevel = run?.currentLevel ?? null;
|
||||
const currentLevelRef = useRef(currentLevel);
|
||||
@@ -442,11 +457,21 @@ export function PuzzleRuntimeShell({
|
||||
const clearResultKey = currentLevel
|
||||
? `${run?.runId ?? 'run'}:${currentLevel.profileId}:${currentLevel.levelIndex}`
|
||||
: null;
|
||||
const runtimeRunId = run?.runId ?? null;
|
||||
const currentLevelIndex = currentLevel?.levelIndex ?? null;
|
||||
const currentLevelStartedAtMs = currentLevel?.startedAtMs ?? null;
|
||||
const currentLevelStatus = currentLevel?.status ?? null;
|
||||
const musicVolume = authUi?.musicVolume ?? DEFAULT_PUZZLE_MUSIC_VOLUME;
|
||||
const backgroundMusicSrc = currentLevel?.backgroundMusic?.audioSrc?.trim() || null;
|
||||
const levelAudioConfig = DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG;
|
||||
const onMusicVolumeChange = authUi?.setMusicVolume ?? (() => {});
|
||||
const { resolvedUrl: resolvedBackgroundMusicSrc } = useResolvedAssetReadUrl(backgroundMusicSrc);
|
||||
const { resolvedUrl: resolvedCoverImage } = useResolvedAssetReadUrl(
|
||||
currentLevel?.coverImageSrc ?? null,
|
||||
);
|
||||
const { resolvedUrl: resolvedUiBackgroundImage } = useResolvedAssetReadUrl(
|
||||
currentLevel?.uiBackgroundImageSrc ?? null,
|
||||
);
|
||||
const mocapInput = useMocapInput({enabled: runtimeStatus === 'playing'});
|
||||
const primaryMocapHand = mocapInput.latestCommand?.primaryHand;
|
||||
const primaryMocapHandState = primaryMocapHand?.state;
|
||||
@@ -472,6 +497,18 @@ export function PuzzleRuntimeShell({
|
||||
currentLevelRef.current = currentLevel;
|
||||
}, [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]);
|
||||
|
||||
const commitSelectedPieceId = (pieceId: string | null) => {
|
||||
selectedPieceIdRef.current = pieceId;
|
||||
setSelectedPieceId(pieceId);
|
||||
@@ -815,6 +852,41 @@ export function PuzzleRuntimeShell({
|
||||
run?.runId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!runtimeRunId ||
|
||||
currentLevelStatus !== 'playing' ||
|
||||
currentLevelIndex === null ||
|
||||
currentLevelStartedAtMs === null
|
||||
) {
|
||||
countdownSoundKeyRef.current = null;
|
||||
return;
|
||||
}
|
||||
const secondBucket =
|
||||
displayRemainingMs <= levelAudioConfig.countdownWarningThresholdMs
|
||||
? resolveRuntimeCountdownSecondBucket(displayRemainingMs)
|
||||
: null;
|
||||
if (secondBucket === null) {
|
||||
countdownSoundKeyRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const soundKey = `${runtimeRunId}:${currentLevelIndex}:${currentLevelStartedAtMs}:${secondBucket}`;
|
||||
if (countdownSoundKeyRef.current === soundKey) {
|
||||
return;
|
||||
}
|
||||
countdownSoundKeyRef.current = soundKey;
|
||||
playRuntimeCountdownSound(musicVolume);
|
||||
}, [
|
||||
currentLevelIndex,
|
||||
currentLevelStartedAtMs,
|
||||
currentLevelStatus,
|
||||
displayRemainingMs,
|
||||
levelAudioConfig.countdownWarningThresholdMs,
|
||||
musicVolume,
|
||||
runtimeRunId,
|
||||
]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (hintDemoTimeoutRef.current !== null) {
|
||||
@@ -853,6 +925,10 @@ export function PuzzleRuntimeShell({
|
||||
|
||||
// 通关后先保留完整画面,再播放对角线闪光,最后延迟弹出结算弹窗。
|
||||
clearPresentationKeyRef.current = clearResultKey;
|
||||
if (clearSoundKeyRef.current !== clearResultKey) {
|
||||
clearSoundKeyRef.current = clearResultKey;
|
||||
playRuntimeLevelClearSound(musicVolume);
|
||||
}
|
||||
clearPresentationTimeouts();
|
||||
setIsClearFlashVisible(true);
|
||||
setIsClearResultReady(false);
|
||||
@@ -864,7 +940,7 @@ export function PuzzleRuntimeShell({
|
||||
setIsClearResultReady(true);
|
||||
}, PUZZLE_CLEAR_FLASH_DURATION_MS + PUZZLE_CLEAR_DIALOG_DELAY_MS),
|
||||
];
|
||||
}, [clearResultKey, currentLevel, dismissedClearKey]);
|
||||
}, [clearResultKey, currentLevel, dismissedClearKey, musicVolume]);
|
||||
|
||||
const handlePieceTap = (
|
||||
pieceId: string,
|
||||
@@ -994,7 +1070,7 @@ export function PuzzleRuntimeShell({
|
||||
syncRuntimeDragFromController(session);
|
||||
selectedPieceBeforeInputRef.current = selectedPieceIdRef.current;
|
||||
commitSelectedPieceId(session.targetId);
|
||||
triggerPuzzlePiecePressHapticFeedback();
|
||||
triggerPuzzlePiecePressFeedback(musicVolume);
|
||||
},
|
||||
onDragStart: (session) => {
|
||||
draggingTargetRef.current = resolvePuzzleRuntimeDragTarget(session.targetId);
|
||||
@@ -1352,16 +1428,38 @@ export function PuzzleRuntimeShell({
|
||||
<div
|
||||
className={`puzzle-runtime-shell ${embedded ? 'relative h-full min-h-0 w-full' : 'fixed inset-0 z-[100]'} flex justify-center`}
|
||||
>
|
||||
{resolvedBackgroundMusicSrc ? (
|
||||
<audio
|
||||
ref={backgroundAudioRef}
|
||||
src={resolvedBackgroundMusicSrc}
|
||||
loop
|
||||
preload="auto"
|
||||
/>
|
||||
) : null}
|
||||
<div className="puzzle-runtime-stage relative h-full w-full overflow-hidden">
|
||||
{resolvedUiBackgroundImage ? (
|
||||
<ResolvedAssetImage
|
||||
src={resolvedUiBackgroundImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
{currentLevel.coverImageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={currentLevel.coverImageSrc}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-[0.16] blur-2xl"
|
||||
className={`absolute inset-0 h-full w-full object-cover blur-2xl ${
|
||||
resolvedUiBackgroundImage ? 'opacity-[0.06]' : 'opacity-[0.16]'
|
||||
}`}
|
||||
/>
|
||||
) : null}
|
||||
<div className="puzzle-runtime-stage__grid" />
|
||||
<div
|
||||
className={`puzzle-runtime-stage__grid ${
|
||||
resolvedUiBackgroundImage ? 'opacity-20' : ''
|
||||
}`}
|
||||
/>
|
||||
|
||||
<div className="absolute left-0 top-0 z-20 w-full px-3 py-3 sm:px-4">
|
||||
<div className="grid grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-start gap-2 sm:grid-cols-[2.75rem_minmax(0,1fr)_2.75rem] sm:gap-3">
|
||||
|
||||
Reference in New Issue
Block a user