- Añade sistema de reconexión tras refresh/cierre del navegador - Persistencia de sesión en localStorage (3h TTL) - Banner de reconexión en Home - Evento rejoin_room en backend - Nuevas categorías: Series TV, Marvel/DC, Disney, Memes, Pokémon, Mitología - Correcciones de bugs: - Fix: juego bloqueado al fallar robo (steal decision) - Fix: jugador duplicado al cambiar de equipo - Fix: rotación incorrecta de turno tras fallo - Config: soporte para Cloudflare tunnel (allowedHosts) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
315 lines
9.2 KiB
TypeScript
315 lines
9.2 KiB
TypeScript
import { useEffect, useCallback } from 'react'
|
|
import { useGameStore, saveSession, clearSession } 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'
|
|
|
|
// Team message type
|
|
export interface TeamMessage {
|
|
player_name: string
|
|
team: 'A' | 'B'
|
|
message: string
|
|
timestamp: string
|
|
}
|
|
|
|
export function useSocket() {
|
|
const { setRoom, addMessage, setShowStealPrompt, setCurrentQuestion, setGameResult, addReaction, addTeamMessage } =
|
|
useGameStore()
|
|
|
|
// Initialize sound player with current theme
|
|
const currentTheme = useThemeStore.getState().currentTheme
|
|
soundPlayer.loadTheme(currentTheme)
|
|
|
|
useEffect(() => {
|
|
// Get singleton socket connection
|
|
const socket = socketService.connect()
|
|
|
|
// 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)
|
|
})
|
|
|
|
// Room events
|
|
socket.on('room_created', (data: { room: GameRoom }) => {
|
|
setRoom(data.room)
|
|
})
|
|
|
|
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 }) => {
|
|
setRoom(data.room)
|
|
})
|
|
|
|
socket.on('team_changed', (data: { room: GameRoom }) => {
|
|
setRoom(data.room)
|
|
})
|
|
|
|
// Reconnection events
|
|
socket.on('rejoin_success', (data: { room: GameRoom; player_name: string; team: 'A' | 'B' }) => {
|
|
console.log('Rejoin successful:', data.player_name)
|
|
setRoom(data.room)
|
|
useGameStore.getState().setPlayerName(data.player_name)
|
|
// Update saved session with possibly new team
|
|
saveSession(data.room.code, data.player_name, data.team)
|
|
})
|
|
|
|
socket.on('rejoin_failed', (data: { message: string }) => {
|
|
console.log('Rejoin failed:', data.message)
|
|
clearSession()
|
|
})
|
|
|
|
socket.on('player_reconnected', (data: { player_name: string; team: string; room: GameRoom }) => {
|
|
console.log('Player reconnected:', data.player_name)
|
|
setRoom(data.room)
|
|
})
|
|
|
|
// 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)
|
|
// 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) => {
|
|
setRoom(data.room)
|
|
|
|
// Play appropriate sound based on answer result
|
|
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)
|
|
}
|
|
})
|
|
|
|
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 }) => {
|
|
setRoom(data.room)
|
|
setShowStealPrompt(false)
|
|
setCurrentQuestion(null)
|
|
})
|
|
|
|
socket.on('time_up', (data: { room: GameRoom; was_steal: boolean }) => {
|
|
setRoom(data.room)
|
|
if (!data.was_steal && data.room.can_steal) {
|
|
setShowStealPrompt(true)
|
|
} else {
|
|
setShowStealPrompt(false)
|
|
setCurrentQuestion(null)
|
|
}
|
|
})
|
|
|
|
// Chat events
|
|
socket.on('chat_message', (data: ChatMessage) => {
|
|
addMessage(data)
|
|
})
|
|
|
|
socket.on('emoji_reaction', (data: { player_name: string; team: string; emoji: string }) => {
|
|
// 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: {
|
|
room: GameRoom
|
|
winner: 'A' | 'B' | null
|
|
final_scores: { A: number; B: number }
|
|
replay_code: string | null
|
|
achievements_unlocked: Array<{
|
|
player_name: string
|
|
team: 'A' | 'B'
|
|
achievement: unknown
|
|
}>
|
|
}) => {
|
|
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,
|
|
replayCode: data.replay_code,
|
|
achievementsUnlocked: data.achievements_unlocked.map(a => ({
|
|
player_name: a.player_name,
|
|
team: a.team,
|
|
achievement: a.achievement as Achievement
|
|
}))
|
|
})
|
|
})
|
|
|
|
// No cleanup - socket connection persists across components
|
|
}, [])
|
|
|
|
// Socket methods - use singleton service
|
|
const createRoom = useCallback((playerName: string) => {
|
|
socketService.emit('create_room', { player_name: playerName })
|
|
}, [])
|
|
|
|
const joinRoom = useCallback((roomCode: string, playerName: string, team: 'A' | 'B') => {
|
|
socketService.emit('join_room', {
|
|
room_code: roomCode,
|
|
player_name: playerName,
|
|
team,
|
|
})
|
|
}, [])
|
|
|
|
const changeTeam = useCallback((team: 'A' | 'B') => {
|
|
socketService.emit('change_team', { team })
|
|
}, [])
|
|
|
|
const startGame = useCallback((board: Record<string, unknown>) => {
|
|
socketService.emit('start_game', { board })
|
|
}, [])
|
|
|
|
const selectQuestion = useCallback((questionId: number, categoryId: number) => {
|
|
socketService.emit('select_question', {
|
|
question_id: questionId,
|
|
category_id: categoryId,
|
|
})
|
|
}, [])
|
|
|
|
const submitAnswer = useCallback(
|
|
(answer: string, question: Record<string, unknown>, isSteal: boolean = false) => {
|
|
socketService.emit('submit_answer', {
|
|
answer,
|
|
question,
|
|
is_steal: isSteal,
|
|
})
|
|
},
|
|
[]
|
|
)
|
|
|
|
const stealDecision = useCallback((attempt: boolean, questionId: number, answer?: string) => {
|
|
socketService.emit('steal_decision', {
|
|
attempt,
|
|
question_id: questionId,
|
|
answer,
|
|
})
|
|
}, [])
|
|
|
|
const sendChatMessage = useCallback((message: string) => {
|
|
socketService.emit('chat_message', { message })
|
|
}, [])
|
|
|
|
const sendEmojiReaction = useCallback((emoji: string) => {
|
|
socketService.emit('emoji_reaction', { emoji })
|
|
}, [])
|
|
|
|
const sendReaction = useCallback((emoji: string, roomCode?: string, playerName?: string) => {
|
|
socketService.emit('send_reaction', {
|
|
emoji,
|
|
room_code: roomCode,
|
|
player_name: playerName,
|
|
})
|
|
}, [])
|
|
|
|
const sendTeamMessage = useCallback(
|
|
(message: string, roomCode: string, team: 'A' | 'B', playerName: string) => {
|
|
socketService.emit('team_message', {
|
|
room_code: roomCode,
|
|
team,
|
|
player_name: playerName,
|
|
message,
|
|
})
|
|
},
|
|
[]
|
|
)
|
|
|
|
const notifyTimerExpired = useCallback(() => {
|
|
socketService.emit('timer_expired', {})
|
|
}, [])
|
|
|
|
const rejoinRoom = useCallback((roomCode: string, playerName: string, team: 'A' | 'B') => {
|
|
socketService.emit('rejoin_room', {
|
|
room_code: roomCode,
|
|
player_name: playerName,
|
|
team,
|
|
})
|
|
}, [])
|
|
|
|
return {
|
|
socket: socketService.connect(),
|
|
createRoom,
|
|
joinRoom,
|
|
rejoinRoom,
|
|
changeTeam,
|
|
startGame,
|
|
selectQuestion,
|
|
submitAnswer,
|
|
stealDecision,
|
|
sendChatMessage,
|
|
sendEmojiReaction,
|
|
sendReaction,
|
|
sendTeamMessage,
|
|
notifyTimerExpired,
|
|
}
|
|
}
|