import { io } from 'socket.io-client'; import { useGameStore } from '../store/gameStore'; import type { Account, GameDetail, GameStatusPayload, Hand, HistoryGame, MyPlayer, OnlineCountPayload, Registration, } from '../types'; export const socket = io({ autoConnect: false }); /** Attach (or clear) the session token sent in the Socket.IO `auth` handshake * so the server can auto-login this connection (now and on every reconnect). */ export function setAuthToken(token: string | null) { socket.auth = token ? { token } : {}; } // Module-level state for the create→register chain and gid resolution on register_player let _pendingGid: string | null = null; let _createName: string | null = null; export const emit = { // auth registerAccount: (username: string) => socket.emit('register_account', username), confirmAccount: (username: string, code: string) => socket.emit('confirm_account', username, code), login: (username: string, code: string) => socket.emit('login', username, code), // De-authenticates this connection server-side (no disconnect/reconnect -- // see the "logout" handler comment in api/__init__.py for why). logout: () => socket.emit('logout'), // history getPlayerHistory: () => socket.emit('get_player_history'), getGameDetail: (gid: string) => socket.emit('get_game_detail', gid), // lobby / game createGame: (name: string) => { _createName = name; socket.emit('create_game', name); }, registerPlayer: (gid: string) => { _pendingGid = gid; socket.emit('register_player', gid); }, // Re-seat into an already-started game via the logged-in account (e.g. after // a server restart). The server replies with register_player + game_status. rejoinGame: (gid: string) => { _pendingGid = gid; socket.emit('rejoin_game', gid); }, // Reopen a prematurely-ended game from history back into the lobby. restoreGame: (gid: string) => socket.emit('restore_game', gid), leaveGame: () => socket.emit('leave_game'), // Bots: host-only, before the game starts (seat picked by the server). // kind: 'heuristic' (default) | 'neural' (trained net) | 'random' addBot: (gid: string, kind: string = 'heuristic') => socket.emit('add_bot', gid, kind), removeBot: (gid: string, order: number) => socket.emit('remove_bot', gid, order), endGame: (gid: string) => socket.emit('end_game', gid), startGame: (gid: string) => socket.emit('start_game', gid), reconnectToGame: (gid: string, token: string) => socket.emit('reconnect_to_game', gid, token), gameStatus: () => socket.emit('game_status'), playerCards: () => socket.emit('player_cards'), addGuess: (guess: number) => socket.emit('add_guess', guess), playCard: (cardKey: string) => socket.emit('play_card', cardKey), }; export function setupSocketListeners() { socket.on('get_games', ({ games, online_count, available_count }: OnlineCountPayload) => { useGameStore.getState().setGames(games); if (typeof online_count === 'number') useGameStore.getState().setOnlineCount(online_count); if (typeof available_count === 'number') useGameStore.getState().setAvailableCount(available_count); }); // Registration step 1: server returns the otpauth URI to render as a QR code. socket.on('register_account', (data: Registration) => { useGameStore.getState().setRegistration(data); }); // Login / confirm / auto-login. A token is present on explicit login (persist it); // auto-login on connect omits it (we keep the stored one). socket.on('login', ({ player, token }: { player: Account; token?: string }) => { if (token) { localStorage.setItem('bridzik_token', token); setAuthToken(token); } useGameStore.getState().setAccount(player); useGameStore.getState().setRegistration(null); }); socket.on('get_player_history', ({ games }: { games: HistoryGame[] }) => { useGameStore.getState().setHistory(games); }); socket.on('get_game_detail', (detail: GameDetail) => { useGameStore.getState().setGameDetail(detail); }); socket.on('create_game', ({ gid }: { gid: string }) => { // Auto-chain: register into the just-created game (identity comes from the session). if (_createName !== null) { emit.registerPlayer(gid); _createName = null; } }); socket.on( 'register_player', ({ player, token }: { player: { order: number; name: string }; token: string }) => { const saved = localStorage.getItem('bridzik_player'); const gid = _pendingGid ?? (saved ? (JSON.parse(saved) as MyPlayer).gid : ''); _pendingGid = null; const myPlayer: MyPlayer = { ...player, token, gid }; useGameStore.getState().setMyPlayer(myPlayer); localStorage.setItem('bridzik_player', JSON.stringify(myPlayer)); } ); socket.on('game_status', (payload: GameStatusPayload) => { useGameStore.getState().setGameStatus(payload); }); // Host ended the game permanently -> drop local game state, back to lobby. socket.on('game_ended', () => { useGameStore.getState().reset(); localStorage.removeItem('bridzik_player'); }); socket.on('player_cards', ({ cards }: { cards: Hand }) => { useGameStore.getState().setHand(cards); }); socket.on('player_connection', ({ order, connected }: { order: number; connected: boolean }) => { useGameStore.getState().updatePlayerConnection(order, connected); }); socket.on('error', ({ error }: { error: string }) => { useGameStore.getState().setError(error); }); // Surface connection failures instead of silently buffering emits. socket.on('connect_error', (err: Error) => { // `active` = klient sa automaticky pokusi znova (typicky prebudeny mobil, // kym sa siet zobudi) -- prechodne, netreba strasit toastom. if (socket.active) return; useGameStore.getState().setError(`Spojenie so serverom zlyhalo: ${err.message}`); }); }