| | import { io, Socket } from 'socket.io-client'; |
| | import type { ServerToClientEvents, ClientToServerEvents, CardId } from '@shared/types'; |
| | import { useGameStore } from '../store/gameStore'; |
| |
|
| | const SERVER_URL = import.meta.env.PROD |
| | ? window.location.origin |
| | : 'http://localhost:3001'; |
| |
|
| | export const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(SERVER_URL, { |
| | autoConnect: false, |
| | transports: ['websocket'], |
| | upgrade: false, |
| | reconnectionAttempts: 5, |
| | reconnectionDelay: 1000, |
| | }); |
| |
|
| | let isInitialized = false; |
| | const processedEvents = new Set<string>(); |
| |
|
| | |
| | function shouldProcessEvent(eventKey: string): boolean { |
| | if (processedEvents.has(eventKey)) return false; |
| | processedEvents.add(eventKey); |
| | setTimeout(() => processedEvents.delete(eventKey), 5000); |
| | return true; |
| | } |
| |
|
| | export function initializeSocket(): void { |
| | |
| | if (isInitialized) { |
| | if (!socket.connected) { |
| | socket.connect(); |
| | } |
| | return; |
| | } |
| | isInitialized = true; |
| | |
| | const store = useGameStore.getState(); |
| | |
| | socket.on('connect', () => { |
| | console.log('Connected to server:', socket.id); |
| | store.setPlayerId(socket.id ?? null); |
| | store.setConnected(true); |
| | socket.emit('getRooms'); |
| | }); |
| | |
| | socket.on('disconnect', () => { |
| | console.log('Disconnected from server'); |
| | store.setConnected(false); |
| | }); |
| | |
| | socket.on('error', (message) => { |
| | store.addToast(message, 'danger'); |
| | }); |
| | |
| | |
| | socket.on('roomList', (rooms) => { |
| | store.setRoomList(rooms); |
| | }); |
| | |
| | socket.on('roomJoined', (room) => { |
| | store.setRoom(room); |
| | store.setScreen('room'); |
| | }); |
| | |
| | socket.on('roomUpdated', (room) => { |
| | store.setRoom(room); |
| | }); |
| | |
| | socket.on('roomLeft', () => { |
| | store.setRoom(null); |
| | store.setScreen('lobby'); |
| | socket.emit('getRooms'); |
| | }); |
| | |
| | |
| | socket.on('gameStarted', (state, hand) => { |
| | store.setGameState(state); |
| | store.setMyHand(hand); |
| | store.setScreen('game'); |
| | }); |
| | |
| | socket.on('gameStateUpdated', (state) => { |
| | store.setGameState(state); |
| | }); |
| | |
| | socket.on('handUpdated', (hand) => { |
| | store.setMyHand(hand); |
| | }); |
| | |
| | |
| | socket.on('cardPlayed', (_playerId, _cardId, _targetId) => { |
| | |
| | }); |
| | |
| | socket.on('peekCards', (cards) => { |
| | const eventKey = `peekCards-${cards.map(c => c.instanceId).join(',')}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('peek-cards', { peekCards: cards }); |
| | } |
| | }); |
| | |
| | socket.on('duelResult', (challenger, opponent, winnerId) => { |
| | const eventKey = `duelResult-${challenger.card.instanceId}-${opponent.card.instanceId}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('duel-result', { |
| | duelChallenger: challenger, |
| | duelOpponent: opponent, |
| | duelWinnerId: winnerId, |
| | }); |
| | } |
| | }); |
| | |
| | socket.on('swapResult', (youGave, youReceived, otherPlayerName) => { |
| | const eventKey = `swapResult-${youGave.instanceId}-${youReceived.instanceId}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('swap-result', { |
| | swapGaveCard: youGave, |
| | swapReceivedCard: youReceived, |
| | swapOtherPlayer: otherPlayerName, |
| | }); |
| | } |
| | }); |
| | |
| | socket.on('spellboundResult', (_success, _cardId) => { |
| | |
| | }); |
| | |
| | socket.on('blindStealStart', (_thiefId, _targetId, cardCount) => { |
| | const state = useGameStore.getState(); |
| | if (socket.id === state.playerId) { |
| | const eventKey = `blindSteal-${Date.now()}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('blind-steal', { blindStealCount: cardCount }); |
| | } |
| | } |
| | }); |
| | |
| | socket.on('arrangeHandPrompt', (_thiefId) => { |
| | const eventKey = `arrangeHand-${Date.now()}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('arrange-hand'); |
| | } |
| | }); |
| | |
| | socket.on('viewHand', (targetId, cards) => { |
| | const eventKey = `viewHand-${targetId}-${cards.map(c => c.instanceId).join(',')}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('view-hand', { viewHandCards: cards, viewHandTargetId: targetId }); |
| | } |
| | }); |
| | |
| | socket.on('flipTableCards', (cards) => { |
| | const eventKey = `flipTable-${cards.map(c => c.instanceId).join(',')}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('flip-table', { flipTableCards: cards }); |
| | } |
| | }); |
| | |
| | |
| | socket.on('kingRaPrompt', (playerId, cardPlayed, timeout) => { |
| | const state = useGameStore.getState(); |
| | |
| | |
| | if (playerId === state.playerId) { |
| | return; |
| | } |
| | |
| | |
| | if (Date.now() - state.lastActionResolvedAt < 2000) { |
| | return; |
| | } |
| | |
| | |
| | const currentModal = state.activeModal; |
| | if (currentModal && currentModal !== 'king-ra-prompt') { |
| | |
| | return; |
| | } |
| | |
| | |
| | store.openModal('king-ra-prompt', { |
| | kingRaPlayerId: playerId, |
| | kingRaCardPlayed: cardPlayed, |
| | kingRaTimeout: timeout, |
| | }); |
| | }); |
| | |
| | socket.on('kingRaResponse', (_responderId, _didCancel) => { |
| | |
| | }); |
| |
|
| | socket.on('actionPending', (_cardId: CardId, _timeout: number) => { |
| | store.setReactionWindowActive(true); |
| | }); |
| |
|
| | socket.on('actionResolved', (_cardId: CardId) => { |
| | |
| | const state = useGameStore.getState(); |
| | if (state.activeModal === 'king-ra-prompt') { |
| | store.closeModal(); |
| | } |
| | store.setReactionWindowActive(false); |
| | store.setLastActionResolvedAt(Date.now()); |
| | }); |
| |
|
| | socket.on('actionCancelled', (_cardId: CardId, _cancelCount) => { |
| | |
| | const state = useGameStore.getState(); |
| | if (state.activeModal === 'king-ra-prompt') { |
| | store.closeModal(); |
| | } |
| | store.setReactionWindowActive(false); |
| | store.setLastActionResolvedAt(Date.now()); |
| | }); |
| | |
| | |
| | socket.on('mummyDrawn', (playerId) => { |
| | const state = useGameStore.getState().gameState; |
| | const player = state?.players.find(p => p.id === playerId); |
| | store.setMummyEvent({ type: 'drawn', playerName: player?.name ?? 'A player' }); |
| | }); |
| | |
| | socket.on('mummyDefused', (playerId) => { |
| | const state = useGameStore.getState().gameState; |
| | const player = state?.players.find(p => p.id === playerId); |
| | store.setMummyEvent({ type: 'defused', playerName: player?.name ?? 'A player' }); |
| | }); |
| | |
| | socket.on('placeMummyPrompt', (deckSize) => { |
| | const eventKey = `placeMummy-${deckSize}-${Date.now()}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('place-mummy', { deckSize }); |
| | } |
| | }); |
| | |
| | socket.on('playerEliminated', (playerId) => { |
| | const state = useGameStore.getState().gameState; |
| | const player = state?.players.find(p => p.id === playerId); |
| | store.setMummyEvent({ type: 'eliminated', playerName: player?.name ?? 'A player' }); |
| | }); |
| | |
| | |
| | socket.on('turnStarted', (playerId, turnsRemaining) => { |
| | store.setTurnInfo(playerId, turnsRemaining); |
| | }); |
| | |
| | |
| | socket.on('notification', () => { |
| | |
| | }); |
| | |
| | |
| | socket.on('gameOver', (winnerId, winnerName) => { |
| | const eventKey = `gameOver-${winnerId}`; |
| | if (shouldProcessEvent(eventKey)) { |
| | store.openModal('game-over', { winnerId, winnerName }); |
| | } |
| | }); |
| | |
| | |
| | socket.connect(); |
| | } |
| |
|
| | export function disconnectSocket(): void { |
| | socket.disconnect(); |
| | } |
| |
|
| | |
| | export const emitCreateRoom = (playerName: string, roomName: string): void => { |
| | socket.emit('createRoom', playerName, roomName); |
| | }; |
| |
|
| | export const emitJoinRoom = (playerName: string, roomId: string): void => { |
| | socket.emit('joinRoom', playerName, roomId); |
| | }; |
| |
|
| | export const emitLeaveRoom = (): void => { |
| | socket.emit('leaveRoom'); |
| | }; |
| |
|
| | export const emitSetReady = (ready: boolean): void => { |
| | socket.emit('setReady', ready); |
| | }; |
| |
|
| | export const emitStartGame = (): void => { |
| | socket.emit('startGame'); |
| | }; |
| |
|
| | export const emitPlayCard = (cardInstanceId: string, targetPlayerId?: string, additionalData?: unknown): void => { |
| | socket.emit('playCard', cardInstanceId, targetPlayerId, additionalData); |
| | }; |
| |
|
| | export const emitDrawCard = (): void => { |
| | socket.emit('drawCard'); |
| | }; |
| |
|
| | export const emitSpellboundRequest = (targetId: string, requestedCardId: string): void => { |
| | socket.emit('spellboundRequest', targetId, requestedCardId as any); |
| | }; |
| |
|
| | export const emitBlindStealSelect = (position: number): void => { |
| | socket.emit('blindStealSelect', position); |
| | }; |
| |
|
| | export const emitArrangeHand = (newOrder: string[]): void => { |
| | socket.emit('arrangeHand', newOrder); |
| | }; |
| |
|
| | export const emitBurnCard = (cardInstanceId: string): void => { |
| | socket.emit('burnCard', cardInstanceId); |
| | }; |
| |
|
| | export const emitRearrangeTopCards = (newOrder: string[]): void => { |
| | socket.emit('rearrangeTopCards', newOrder); |
| | }; |
| |
|
| | export const emitPlaceMummy = (position: number): void => { |
| | socket.emit('placeMummy', position); |
| | }; |
| |
|
| | export const emitKingRaResponse = (useKingRa: boolean): void => { |
| | console.log('socket.ts emitKingRaResponse called with:', useKingRa); |
| | socket.emit('kingRaResponse', useKingRa); |
| | console.log('socket.ts kingRaResponse emitted'); |
| | }; |
| |
|
| | export const emitGetRooms = (): void => { |
| | socket.emit('getRooms'); |
| | }; |
| |
|