Sound System: - Add soundStore with volume/mute persistence - Add useSound hook with Web Audio API fallback - Add SoundControl component for in-game volume adjustment - Play sounds for correct/incorrect, steal, timer, victory/defeat Team Chat: - Add TeamChat component with collapsible panel - Add team_message WebSocket event (team-only visibility) - Store up to 50 messages per session Emoji Reactions: - Add EmojiReactions bar with 8 emojis - Add ReactionOverlay with floating animations (Framer Motion) - Add rate limiting (1 reaction per 3 seconds) - Broadcast reactions to all players in room Admin Monitor: - Add Monitor page showing active rooms from Redis - Display player counts, team composition, status - Add ability to close problematic rooms Admin Settings: - Add Settings page for game configuration - Configure points/times by difficulty, steal penalty, max players - Store config in JSON file with service helpers CSV Import/Export: - Add export endpoint with optional filters - Add import endpoint with validation and error reporting - Add UI buttons and import result modal in Questions page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
import { create } from 'zustand'
|
|
import { persist } from 'zustand/middleware'
|
|
import type { ThemeName } from '../types'
|
|
|
|
export type SoundEffect =
|
|
| 'correct'
|
|
| 'incorrect'
|
|
| 'steal'
|
|
| 'timer_tick'
|
|
| 'timer_urgent'
|
|
| 'victory'
|
|
| 'defeat'
|
|
| 'select'
|
|
|
|
interface SoundState {
|
|
volume: number
|
|
muted: boolean
|
|
soundsLoaded: boolean
|
|
currentLoadedTheme: ThemeName | null
|
|
setVolume: (volume: number) => void
|
|
setMuted: (muted: boolean) => void
|
|
toggleMute: () => void
|
|
setSoundsLoaded: (loaded: boolean) => void
|
|
setCurrentLoadedTheme: (theme: ThemeName | null) => void
|
|
}
|
|
|
|
export const useSoundStore = create<SoundState>()(
|
|
persist(
|
|
(set) => ({
|
|
volume: 0.7,
|
|
muted: false,
|
|
soundsLoaded: false,
|
|
currentLoadedTheme: null,
|
|
setVolume: (volume) => set({ volume: Math.max(0, Math.min(1, volume)) }),
|
|
setMuted: (muted) => set({ muted }),
|
|
toggleMute: () => set((state) => ({ muted: !state.muted })),
|
|
setSoundsLoaded: (soundsLoaded) => set({ soundsLoaded }),
|
|
setCurrentLoadedTheme: (currentLoadedTheme) => set({ currentLoadedTheme }),
|
|
}),
|
|
{
|
|
name: 'trivia-sound',
|
|
partialize: (state) => ({ volume: state.volume, muted: state.muted }),
|
|
}
|
|
)
|
|
)
|
|
|
|
// Sound file paths per theme
|
|
// All themes share the same base sounds but can be customized per theme
|
|
export const soundPaths: Record<ThemeName, Record<SoundEffect, string>> = {
|
|
drrr: {
|
|
correct: '/sounds/drrr/correct.mp3',
|
|
incorrect: '/sounds/drrr/incorrect.mp3',
|
|
steal: '/sounds/drrr/steal.mp3',
|
|
timer_tick: '/sounds/drrr/tick.mp3',
|
|
timer_urgent: '/sounds/drrr/urgent.mp3',
|
|
victory: '/sounds/drrr/victory.mp3',
|
|
defeat: '/sounds/drrr/defeat.mp3',
|
|
select: '/sounds/drrr/select.mp3',
|
|
},
|
|
retro: {
|
|
correct: '/sounds/retro/correct.mp3',
|
|
incorrect: '/sounds/retro/incorrect.mp3',
|
|
steal: '/sounds/retro/steal.mp3',
|
|
timer_tick: '/sounds/retro/tick.mp3',
|
|
timer_urgent: '/sounds/retro/urgent.mp3',
|
|
victory: '/sounds/retro/victory.mp3',
|
|
defeat: '/sounds/retro/defeat.mp3',
|
|
select: '/sounds/retro/select.mp3',
|
|
},
|
|
minimal: {
|
|
correct: '/sounds/minimal/correct.mp3',
|
|
incorrect: '/sounds/minimal/incorrect.mp3',
|
|
steal: '/sounds/minimal/steal.mp3',
|
|
timer_tick: '/sounds/minimal/tick.mp3',
|
|
timer_urgent: '/sounds/minimal/urgent.mp3',
|
|
victory: '/sounds/minimal/victory.mp3',
|
|
defeat: '/sounds/minimal/defeat.mp3',
|
|
select: '/sounds/minimal/select.mp3',
|
|
},
|
|
rgb: {
|
|
correct: '/sounds/rgb/correct.mp3',
|
|
incorrect: '/sounds/rgb/incorrect.mp3',
|
|
steal: '/sounds/rgb/steal.mp3',
|
|
timer_tick: '/sounds/rgb/tick.mp3',
|
|
timer_urgent: '/sounds/rgb/urgent.mp3',
|
|
victory: '/sounds/rgb/victory.mp3',
|
|
defeat: '/sounds/rgb/defeat.mp3',
|
|
select: '/sounds/rgb/select.mp3',
|
|
},
|
|
anime: {
|
|
correct: '/sounds/anime/correct.mp3',
|
|
incorrect: '/sounds/anime/incorrect.mp3',
|
|
steal: '/sounds/anime/steal.mp3',
|
|
timer_tick: '/sounds/anime/tick.mp3',
|
|
timer_urgent: '/sounds/anime/urgent.mp3',
|
|
victory: '/sounds/anime/victory.mp3',
|
|
defeat: '/sounds/anime/defeat.mp3',
|
|
select: '/sounds/anime/select.mp3',
|
|
},
|
|
}
|
|
|
|
// Fallback sounds using Web Audio API generated tones
|
|
// These are used when actual sound files are not available
|
|
export const fallbackSoundConfigs: Record<SoundEffect, { frequency: number; duration: number; type: OscillatorType }> = {
|
|
correct: { frequency: 880, duration: 0.15, type: 'sine' },
|
|
incorrect: { frequency: 220, duration: 0.3, type: 'square' },
|
|
steal: { frequency: 660, duration: 0.2, type: 'sawtooth' },
|
|
timer_tick: { frequency: 440, duration: 0.05, type: 'sine' },
|
|
timer_urgent: { frequency: 880, duration: 0.1, type: 'square' },
|
|
victory: { frequency: 523, duration: 0.5, type: 'sine' },
|
|
defeat: { frequency: 196, duration: 0.5, type: 'sine' },
|
|
select: { frequency: 600, duration: 0.08, type: 'sine' },
|
|
}
|