feat(phase6): Add sounds, team chat, reactions, monitor, settings, and CSV import/export
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>
This commit is contained in:
@@ -1,15 +1,31 @@
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { useGameStore } from '../stores/gameStore'
|
||||
import { soundPlayer } from './useSound'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import { useSoundStore } from '../stores/soundStore'
|
||||
import type { GameRoom, ChatMessage, AnswerResult, Achievement } from '../types'
|
||||
import type { Reaction } from '../stores/gameStore'
|
||||
|
||||
const SOCKET_URL = import.meta.env.VITE_WS_URL || 'http://localhost:8000'
|
||||
|
||||
// Team message type
|
||||
export interface TeamMessage {
|
||||
player_name: string
|
||||
team: 'A' | 'B'
|
||||
message: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export function useSocket() {
|
||||
const socketRef = useRef<Socket | null>(null)
|
||||
const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd, setGameResult } =
|
||||
const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd, setGameResult, addReaction, addTeamMessage } =
|
||||
useGameStore()
|
||||
|
||||
// Initialize sound player with current theme
|
||||
const currentTheme = useThemeStore.getState().currentTheme
|
||||
soundPlayer.loadTheme(currentTheme)
|
||||
|
||||
useEffect(() => {
|
||||
// Create socket connection
|
||||
socketRef.current = io(SOCKET_URL, {
|
||||
@@ -61,14 +77,27 @@ export function useSocket() {
|
||||
|
||||
socket.on('answer_result', (data: AnswerResult) => {
|
||||
setRoom(data.room)
|
||||
|
||||
// Play appropriate sound based on answer result
|
||||
const volume = useSoundStore.getState().volume
|
||||
if (data.valid) {
|
||||
soundPlayer.play('correct', volume)
|
||||
} else {
|
||||
soundPlayer.play('incorrect', volume)
|
||||
}
|
||||
|
||||
if (!data.valid && !data.was_steal && data.room.can_steal) {
|
||||
setShowStealPrompt(true)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('steal_attempted', (data: { room: GameRoom }) => {
|
||||
socket.on('steal_attempted', (data: { room: GameRoom; success?: boolean }) => {
|
||||
setRoom(data.room)
|
||||
setShowStealPrompt(false)
|
||||
|
||||
// Play steal sound when a steal is attempted
|
||||
const volume = useSoundStore.getState().volume
|
||||
soundPlayer.play('steal', volume)
|
||||
})
|
||||
|
||||
socket.on('steal_passed', (data: { room: GameRoom }) => {
|
||||
@@ -91,8 +120,23 @@ export function useSocket() {
|
||||
})
|
||||
|
||||
socket.on('emoji_reaction', (data: { player_name: string; team: string; emoji: string }) => {
|
||||
// Handle emoji reaction display
|
||||
console.log(`${data.player_name} reacted with ${data.emoji}`)
|
||||
// Legacy handler - redirect to new reaction system
|
||||
addReaction({
|
||||
player_name: data.player_name,
|
||||
team: data.team as 'A' | 'B',
|
||||
emoji: data.emoji,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('receive_reaction', (data: Omit<Reaction, 'id'>) => {
|
||||
// Add reaction to the store for display in overlay
|
||||
addReaction(data)
|
||||
})
|
||||
|
||||
// Team chat events
|
||||
socket.on('receive_team_message', (data: TeamMessage) => {
|
||||
addTeamMessage(data)
|
||||
})
|
||||
|
||||
socket.on('game_finished', (data: {
|
||||
@@ -107,6 +151,18 @@ export function useSocket() {
|
||||
}>
|
||||
}) => {
|
||||
setRoom(data.room)
|
||||
|
||||
// Determine if current player is on the winning team
|
||||
const currentPlayerName = useGameStore.getState().playerName
|
||||
const myTeam = data.room.teams.A.find(p => p.name === currentPlayerName) ? 'A' : 'B'
|
||||
const volume = useSoundStore.getState().volume
|
||||
|
||||
if (data.winner === myTeam) {
|
||||
soundPlayer.play('victory', volume)
|
||||
} else if (data.winner !== null) {
|
||||
soundPlayer.play('defeat', volume)
|
||||
}
|
||||
|
||||
setGameResult({
|
||||
winner: data.winner,
|
||||
finalScores: data.final_scores,
|
||||
@@ -122,7 +178,7 @@ export function useSocket() {
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd, setGameResult])
|
||||
}, [setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd, setGameResult, addReaction, addTeamMessage])
|
||||
|
||||
// Socket methods
|
||||
const createRoom = useCallback((playerName: string) => {
|
||||
@@ -179,6 +235,26 @@ export function useSocket() {
|
||||
socketRef.current?.emit('emoji_reaction', { emoji })
|
||||
}, [])
|
||||
|
||||
const sendReaction = useCallback((emoji: string, roomCode?: string, playerName?: string) => {
|
||||
socketRef.current?.emit('send_reaction', {
|
||||
emoji,
|
||||
room_code: roomCode,
|
||||
player_name: playerName,
|
||||
})
|
||||
}, [])
|
||||
|
||||
const sendTeamMessage = useCallback(
|
||||
(message: string, roomCode: string, team: 'A' | 'B', playerName: string) => {
|
||||
socketRef.current?.emit('team_message', {
|
||||
room_code: roomCode,
|
||||
team,
|
||||
player_name: playerName,
|
||||
message,
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const notifyTimerExpired = useCallback(() => {
|
||||
socketRef.current?.emit('timer_expired', {})
|
||||
}, [])
|
||||
@@ -194,6 +270,8 @@ export function useSocket() {
|
||||
stealDecision,
|
||||
sendChatMessage,
|
||||
sendEmojiReaction,
|
||||
sendReaction,
|
||||
sendTeamMessage,
|
||||
notifyTimerExpired,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user