feat: 5 categorías rotativas por partida + pool de 200 preguntas + mejoras UI
Cambios principales: - Tablero ahora muestra 5 categorías aleatorias (de 8 disponibles) - Pool de 200 preguntas (8 cats × 5 diffs × 5 opciones) - Preguntas rotan aleatoriamente entre partidas - Diseño mejorado estilo Jeopardy con efectos visuales - Socket singleton para conexión persistente - Nuevos sonidos: game_start, player_join, question_reveal, hover, countdown - Control de volumen vertical - Barra de progreso del timer en modal de preguntas - Animaciones mejoradas con Framer Motion Backend: - question_service: selección aleatoria de 5 categorías - room_manager: fix retorno de create_room - game_events: carga board desde DB, await en enter_room Frontend: - Game.tsx: tablero dinámico, efectos hover, mejor scoreboard - useSocket: singleton service, eventos con sonidos - SoundControl: slider vertical - soundStore: 5 nuevos efectos de sonido Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { useGameStore } from '../stores/gameStore'
|
||||
import { soundPlayer } from './useSound'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import { useSoundStore } from '../stores/soundStore'
|
||||
import { socketService } from '../services/socket'
|
||||
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
|
||||
@@ -18,8 +16,7 @@ export interface TeamMessage {
|
||||
}
|
||||
|
||||
export function useSocket() {
|
||||
const socketRef = useRef<Socket | null>(null)
|
||||
const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd, setGameResult, addReaction, addTeamMessage } =
|
||||
const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setGameResult, addReaction, addTeamMessage } =
|
||||
useGameStore()
|
||||
|
||||
// Initialize sound player with current theme
|
||||
@@ -27,23 +24,16 @@ export function useSocket() {
|
||||
soundPlayer.loadTheme(currentTheme)
|
||||
|
||||
useEffect(() => {
|
||||
// Create socket connection
|
||||
socketRef.current = io(SOCKET_URL, {
|
||||
transports: ['websocket', 'polling'],
|
||||
autoConnect: true,
|
||||
})
|
||||
// Get singleton socket connection
|
||||
const socket = socketService.connect()
|
||||
|
||||
const socket = socketRef.current
|
||||
|
||||
// Connection events
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to server')
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected from server')
|
||||
})
|
||||
// Only set up listeners once globally
|
||||
if (socketService.isInitialized) {
|
||||
return // No cleanup - socket persists
|
||||
}
|
||||
socketService.setInitialized()
|
||||
|
||||
// Error handler
|
||||
socket.on('error', (data: { message: string }) => {
|
||||
console.error('Socket error:', data.message)
|
||||
})
|
||||
@@ -55,6 +45,9 @@ export function useSocket() {
|
||||
|
||||
socket.on('player_joined', (data: { room: GameRoom }) => {
|
||||
setRoom(data.room)
|
||||
// Play sound when a player joins
|
||||
const volume = useSoundStore.getState().volume
|
||||
soundPlayer.play('player_join', volume)
|
||||
})
|
||||
|
||||
socket.on('player_left', (data: { room: GameRoom }) => {
|
||||
@@ -68,11 +61,22 @@ export function useSocket() {
|
||||
// Game events
|
||||
socket.on('game_started', (data: { room: GameRoom }) => {
|
||||
setRoom(data.room)
|
||||
// Play game start sound
|
||||
const volume = useSoundStore.getState().volume
|
||||
soundPlayer.play('game_start', volume)
|
||||
})
|
||||
|
||||
socket.on('question_selected', (data: { room: GameRoom; question_id: number }) => {
|
||||
setRoom(data.room)
|
||||
// Fetch full question details
|
||||
// Find the question in the board and set it as current
|
||||
const questionId = data.question_id
|
||||
for (const categoryQuestions of Object.values(data.room.board || {})) {
|
||||
const question = (categoryQuestions as Array<{ id: number }>).find(q => q.id === questionId)
|
||||
if (question) {
|
||||
setCurrentQuestion(question as unknown as import('../types').Question)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('answer_result', (data: AnswerResult) => {
|
||||
@@ -82,12 +86,17 @@ export function useSocket() {
|
||||
const volume = useSoundStore.getState().volume
|
||||
if (data.valid) {
|
||||
soundPlayer.play('correct', volume)
|
||||
// Clear current question after correct answer
|
||||
setCurrentQuestion(null)
|
||||
} else {
|
||||
soundPlayer.play('incorrect', volume)
|
||||
}
|
||||
|
||||
if (!data.valid && !data.was_steal && data.room.can_steal) {
|
||||
setShowStealPrompt(true)
|
||||
} else if (data.was_steal) {
|
||||
// Clear question after steal attempt (success or fail)
|
||||
setCurrentQuestion(null)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -103,6 +112,7 @@ export function useSocket() {
|
||||
socket.on('steal_passed', (data: { room: GameRoom }) => {
|
||||
setRoom(data.room)
|
||||
setShowStealPrompt(false)
|
||||
setCurrentQuestion(null)
|
||||
})
|
||||
|
||||
socket.on('time_up', (data: { room: GameRoom; was_steal: boolean }) => {
|
||||
@@ -111,6 +121,7 @@ export function useSocket() {
|
||||
setShowStealPrompt(true)
|
||||
} else {
|
||||
setShowStealPrompt(false)
|
||||
setCurrentQuestion(null)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -175,18 +186,16 @@ export function useSocket() {
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setTimerEnd, setGameResult, addReaction, addTeamMessage])
|
||||
// No cleanup - socket connection persists across components
|
||||
}, [])
|
||||
|
||||
// Socket methods
|
||||
// Socket methods - use singleton service
|
||||
const createRoom = useCallback((playerName: string) => {
|
||||
socketRef.current?.emit('create_room', { player_name: playerName })
|
||||
socketService.emit('create_room', { player_name: playerName })
|
||||
}, [])
|
||||
|
||||
const joinRoom = useCallback((roomCode: string, playerName: string, team: 'A' | 'B') => {
|
||||
socketRef.current?.emit('join_room', {
|
||||
socketService.emit('join_room', {
|
||||
room_code: roomCode,
|
||||
player_name: playerName,
|
||||
team,
|
||||
@@ -194,15 +203,15 @@ export function useSocket() {
|
||||
}, [])
|
||||
|
||||
const changeTeam = useCallback((team: 'A' | 'B') => {
|
||||
socketRef.current?.emit('change_team', { team })
|
||||
socketService.emit('change_team', { team })
|
||||
}, [])
|
||||
|
||||
const startGame = useCallback((board: Record<string, unknown>) => {
|
||||
socketRef.current?.emit('start_game', { board })
|
||||
socketService.emit('start_game', { board })
|
||||
}, [])
|
||||
|
||||
const selectQuestion = useCallback((questionId: number, categoryId: number) => {
|
||||
socketRef.current?.emit('select_question', {
|
||||
socketService.emit('select_question', {
|
||||
question_id: questionId,
|
||||
category_id: categoryId,
|
||||
})
|
||||
@@ -210,7 +219,7 @@ export function useSocket() {
|
||||
|
||||
const submitAnswer = useCallback(
|
||||
(answer: string, question: Record<string, unknown>, isSteal: boolean = false) => {
|
||||
socketRef.current?.emit('submit_answer', {
|
||||
socketService.emit('submit_answer', {
|
||||
answer,
|
||||
question,
|
||||
is_steal: isSteal,
|
||||
@@ -220,7 +229,7 @@ export function useSocket() {
|
||||
)
|
||||
|
||||
const stealDecision = useCallback((attempt: boolean, questionId: number, answer?: string) => {
|
||||
socketRef.current?.emit('steal_decision', {
|
||||
socketService.emit('steal_decision', {
|
||||
attempt,
|
||||
question_id: questionId,
|
||||
answer,
|
||||
@@ -228,15 +237,15 @@ export function useSocket() {
|
||||
}, [])
|
||||
|
||||
const sendChatMessage = useCallback((message: string) => {
|
||||
socketRef.current?.emit('chat_message', { message })
|
||||
socketService.emit('chat_message', { message })
|
||||
}, [])
|
||||
|
||||
const sendEmojiReaction = useCallback((emoji: string) => {
|
||||
socketRef.current?.emit('emoji_reaction', { emoji })
|
||||
socketService.emit('emoji_reaction', { emoji })
|
||||
}, [])
|
||||
|
||||
const sendReaction = useCallback((emoji: string, roomCode?: string, playerName?: string) => {
|
||||
socketRef.current?.emit('send_reaction', {
|
||||
socketService.emit('send_reaction', {
|
||||
emoji,
|
||||
room_code: roomCode,
|
||||
player_name: playerName,
|
||||
@@ -245,7 +254,7 @@ export function useSocket() {
|
||||
|
||||
const sendTeamMessage = useCallback(
|
||||
(message: string, roomCode: string, team: 'A' | 'B', playerName: string) => {
|
||||
socketRef.current?.emit('team_message', {
|
||||
socketService.emit('team_message', {
|
||||
room_code: roomCode,
|
||||
team,
|
||||
player_name: playerName,
|
||||
@@ -256,11 +265,11 @@ export function useSocket() {
|
||||
)
|
||||
|
||||
const notifyTimerExpired = useCallback(() => {
|
||||
socketRef.current?.emit('timer_expired', {})
|
||||
socketService.emit('timer_expired', {})
|
||||
}, [])
|
||||
|
||||
return {
|
||||
socket: socketRef.current,
|
||||
socket: socketService.connect(),
|
||||
createRoom,
|
||||
joinRoom,
|
||||
changeTeam,
|
||||
|
||||
Reference in New Issue
Block a user