- 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>
215 lines
5.1 KiB
TypeScript
215 lines
5.1 KiB
TypeScript
import { create } from 'zustand'
|
|
import type { GameRoom, Question, ChatMessage, Achievement } from '../types'
|
|
|
|
// Session persistence helpers
|
|
const SESSION_KEY = 'trivy_session'
|
|
|
|
interface SavedSession {
|
|
roomCode: string
|
|
playerName: string
|
|
team: 'A' | 'B'
|
|
timestamp: number
|
|
}
|
|
|
|
export function saveSession(roomCode: string, playerName: string, team: 'A' | 'B') {
|
|
const session: SavedSession = {
|
|
roomCode,
|
|
playerName,
|
|
team,
|
|
timestamp: Date.now()
|
|
}
|
|
localStorage.setItem(SESSION_KEY, JSON.stringify(session))
|
|
}
|
|
|
|
export function getSavedSession(): SavedSession | null {
|
|
try {
|
|
const data = localStorage.getItem(SESSION_KEY)
|
|
if (!data) return null
|
|
|
|
const session: SavedSession = JSON.parse(data)
|
|
// Session expires after 3 hours (same as room TTL)
|
|
const threeHours = 3 * 60 * 60 * 1000
|
|
if (Date.now() - session.timestamp > threeHours) {
|
|
clearSession()
|
|
return null
|
|
}
|
|
return session
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function clearSession() {
|
|
localStorage.removeItem(SESSION_KEY)
|
|
}
|
|
|
|
export interface Reaction {
|
|
id: string
|
|
player_name: string
|
|
team: 'A' | 'B'
|
|
emoji: string
|
|
timestamp: string
|
|
}
|
|
|
|
export interface TeamMessage {
|
|
player_name: string
|
|
team: 'A' | 'B'
|
|
message: string
|
|
timestamp: string
|
|
}
|
|
|
|
const MAX_TEAM_MESSAGES = 50
|
|
|
|
interface GameState {
|
|
// Room state
|
|
room: GameRoom | null
|
|
setRoom: (room: GameRoom | null) => void
|
|
|
|
// Player info
|
|
playerName: string
|
|
setPlayerName: (name: string) => void
|
|
|
|
// Current question
|
|
currentQuestion: Question | null
|
|
setCurrentQuestion: (question: Question | null) => void
|
|
|
|
// Timer
|
|
timerEnd: Date | null
|
|
setTimerEnd: (end: Date | null) => void
|
|
|
|
// Chat messages
|
|
messages: ChatMessage[]
|
|
addMessage: (message: ChatMessage) => void
|
|
clearMessages: () => void
|
|
|
|
// Team chat messages
|
|
teamMessages: TeamMessage[]
|
|
addTeamMessage: (message: TeamMessage) => void
|
|
clearTeamMessages: () => void
|
|
|
|
// Achievements
|
|
achievements: Achievement[]
|
|
setAchievements: (achievements: Achievement[]) => void
|
|
unlockAchievement: (id: number) => void
|
|
|
|
// Game stats (for achievements tracking)
|
|
stats: {
|
|
correctStreak: number
|
|
stealsAttempted: number
|
|
stealsSuccessful: number
|
|
categoryCorrect: Record<number, number>
|
|
fastAnswers: number
|
|
maxDeficit: number
|
|
}
|
|
updateStats: (updates: Partial<GameState['stats']>) => void
|
|
resetStats: () => void
|
|
|
|
// UI state
|
|
showStealPrompt: boolean
|
|
setShowStealPrompt: (show: boolean) => void
|
|
|
|
// Reactions
|
|
reactions: Reaction[]
|
|
addReaction: (reaction: Omit<Reaction, 'id'>) => void
|
|
removeReaction: (id: string) => void
|
|
clearReactions: () => void
|
|
|
|
// Game result
|
|
gameResult: {
|
|
winner: 'A' | 'B' | null
|
|
finalScores: { A: number; B: number }
|
|
replayCode: string | null
|
|
achievementsUnlocked: Array<{
|
|
player_name: string
|
|
team: 'A' | 'B'
|
|
achievement: Achievement
|
|
}>
|
|
} | null
|
|
setGameResult: (result: GameState['gameResult']) => void
|
|
|
|
// Reset
|
|
resetGame: () => void
|
|
}
|
|
|
|
const initialStats = {
|
|
correctStreak: 0,
|
|
stealsAttempted: 0,
|
|
stealsSuccessful: 0,
|
|
categoryCorrect: {},
|
|
fastAnswers: 0,
|
|
maxDeficit: 0,
|
|
}
|
|
|
|
export const useGameStore = create<GameState>((set) => ({
|
|
room: null,
|
|
setRoom: (room) => set({ room }),
|
|
|
|
playerName: '',
|
|
setPlayerName: (playerName) => set({ playerName }),
|
|
|
|
currentQuestion: null,
|
|
setCurrentQuestion: (currentQuestion) => set({ currentQuestion }),
|
|
|
|
timerEnd: null,
|
|
setTimerEnd: (timerEnd) => set({ timerEnd }),
|
|
|
|
messages: [],
|
|
addMessage: (message) =>
|
|
set((state) => ({ messages: [...state.messages, message].slice(-100) })),
|
|
clearMessages: () => set({ messages: [] }),
|
|
|
|
teamMessages: [],
|
|
addTeamMessage: (message) =>
|
|
set((state) => ({
|
|
teamMessages: [...state.teamMessages, message].slice(-MAX_TEAM_MESSAGES),
|
|
})),
|
|
clearTeamMessages: () => set({ teamMessages: [] }),
|
|
|
|
achievements: [],
|
|
setAchievements: (achievements) => set({ achievements }),
|
|
unlockAchievement: (id) =>
|
|
set((state) => ({
|
|
achievements: state.achievements.map((a) =>
|
|
a.id === id ? { ...a, unlocked: true, unlockedAt: new Date().toISOString() } : a
|
|
),
|
|
})),
|
|
|
|
stats: initialStats,
|
|
updateStats: (updates) =>
|
|
set((state) => ({ stats: { ...state.stats, ...updates } })),
|
|
resetStats: () => set({ stats: initialStats }),
|
|
|
|
showStealPrompt: false,
|
|
setShowStealPrompt: (showStealPrompt) => set({ showStealPrompt }),
|
|
|
|
reactions: [],
|
|
addReaction: (reaction) =>
|
|
set((state) => ({
|
|
reactions: [
|
|
...state.reactions,
|
|
{ ...reaction, id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` },
|
|
],
|
|
})),
|
|
removeReaction: (id) =>
|
|
set((state) => ({
|
|
reactions: state.reactions.filter((r) => r.id !== id),
|
|
})),
|
|
clearReactions: () => set({ reactions: [] }),
|
|
|
|
gameResult: null,
|
|
setGameResult: (gameResult) => set({ gameResult }),
|
|
|
|
resetGame: () =>
|
|
set({
|
|
room: null,
|
|
currentQuestion: null,
|
|
timerEnd: null,
|
|
messages: [],
|
|
teamMessages: [],
|
|
reactions: [],
|
|
stats: initialStats,
|
|
showStealPrompt: false,
|
|
gameResult: null,
|
|
}),
|
|
}))
|