feat(pixel-office): add toggleable background music with gesture retry

This commit is contained in:
xmanrui
2026-03-07 02:15:37 +08:00
parent 53e5d53000
commit 17e1490c83
3 changed files with 95 additions and 1 deletions
+23 -1
View File
@@ -19,7 +19,14 @@ import { TileType, EditTool } from '@/lib/pixel-office/types'
import type { TileType as TileTypeVal, FloorColor, OfficeLayout } from '@/lib/pixel-office/types'
import { getCatalogEntry, isRotatable } from '@/lib/pixel-office/layout/furnitureCatalog'
import { createDefaultLayout, migrateLayoutColors, serializeLayout } from '@/lib/pixel-office/layout/layoutSerializer'
import { playDoneSound, unlockAudio, setSoundEnabled, isSoundEnabled } from '@/lib/pixel-office/notificationSound'
import {
playDoneSound,
playBackgroundMusic,
stopBackgroundMusic,
unlockAudio,
setSoundEnabled,
isSoundEnabled,
} from '@/lib/pixel-office/notificationSound'
import { loadCharacterPNGs, loadWallPNG } from '@/lib/pixel-office/sprites/pngLoader'
import { useI18n } from '@/lib/i18n'
import { EditorToolbar } from './components/EditorToolbar'
@@ -424,6 +431,7 @@ export default function PixelOfficePage() {
}
return () => {
stopBackgroundMusic()
cachedOfficeState = officeRef.current
cachedEditorState = editorRef.current
cachedSavedLayout = savedLayoutRef.current
@@ -435,6 +443,14 @@ export default function PixelOfficePage() {
}
}, [])
useEffect(() => {
if (soundOn) {
void playBackgroundMusic()
} else {
stopBackgroundMusic()
}
}, [soundOn])
useEffect(() => {
cachedAgents = agents
}, [agents])
@@ -1090,6 +1106,7 @@ export default function PixelOfficePage() {
const PHOTO_COUNT = 13
const handleMouseDown = (e: React.MouseEvent) => {
unlockAudio()
if (soundOn) void playBackgroundMusic()
if (!canvasRef.current || !officeRef.current) return
const office = officeRef.current
const editor = editorRef.current
@@ -1531,6 +1548,11 @@ export default function PixelOfficePage() {
setSoundEnabled(newVal)
setSoundOn(newVal)
localStorage.setItem('pixel-office-sound', String(newVal))
if (newVal) {
void playBackgroundMusic()
} else {
stopBackgroundMusic()
}
}, [])
const resetView = useCallback(() => {
+72
View File
@@ -9,9 +9,15 @@ import {
let soundEnabled = true
let audioCtx: AudioContext | null = null
let bgmAudio: HTMLAudioElement | null = null
let bgmGestureRetryBound = false
const BGM_SRC = '/assets/pixel-office/pixel-adventure.mp3'
const BGM_VOLUME = 0.28
export function setSoundEnabled(enabled: boolean): void {
soundEnabled = enabled
if (!enabled) stopBackgroundMusic()
}
export function isSoundEnabled(): boolean {
@@ -36,6 +42,51 @@ function playNote(ctx: AudioContext, freq: number, startOffset: number): void {
osc.stop(t + NOTIFICATION_NOTE_DURATION_SEC)
}
function getBgmAudio(): HTMLAudioElement | null {
if (typeof window === 'undefined') return null
if (!bgmAudio) {
bgmAudio = new Audio(BGM_SRC)
bgmAudio.loop = true
bgmAudio.preload = 'auto'
bgmAudio.volume = BGM_VOLUME
}
return bgmAudio
}
function bindBgmGestureRetry(): void {
if (typeof window === 'undefined' || bgmGestureRetryBound) return
bgmGestureRetryBound = true
const cleanup = () => {
if (typeof window === 'undefined' || !bgmGestureRetryBound) return
bgmGestureRetryBound = false
window.removeEventListener('pointerdown', resumeOnGesture)
window.removeEventListener('touchstart', resumeOnGesture)
window.removeEventListener('keydown', resumeOnGesture)
}
const resumeOnGesture = () => {
if (!soundEnabled) {
cleanup()
return
}
const audio = getBgmAudio()
if (!audio) {
cleanup()
return
}
audio.play().then(() => {
cleanup()
}).catch(() => {
// Keep listeners for next user gesture attempt.
})
}
window.addEventListener('pointerdown', resumeOnGesture, { passive: true })
window.addEventListener('touchstart', resumeOnGesture, { passive: true })
window.addEventListener('keydown', resumeOnGesture)
}
export async function playDoneSound(): Promise<void> {
if (!soundEnabled) return
try {
@@ -64,3 +115,24 @@ export function unlockAudio(): void {
// ignore
}
}
export async function playBackgroundMusic(): Promise<void> {
if (!soundEnabled) return
try {
const audio = getBgmAudio()
if (!audio) return
audio.muted = false
audio.loop = true
audio.volume = BGM_VOLUME
await audio.play()
} catch {
// Browser autoplay may block playback until a user gesture.
bindBgmGestureRetry()
}
}
export function stopBackgroundMusic(): void {
if (!bgmAudio) return
bgmAudio.pause()
bgmAudio.currentTime = 0
}
Binary file not shown.