Add WebSocket client and game store infrastructure
- Extend socket client with heartbeat handling - Add game store computed properties and state management - Add ConnectionStatus and game-related types - Support turn phase, game over, and connection tracking Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a20e97a0a0
commit
c0194b77eb
@ -231,7 +231,11 @@ class SocketClient {
|
||||
* Subscribe to game state updates.
|
||||
*/
|
||||
onGameState(handler: ServerToClientEvents['game:state']): () => void {
|
||||
this._socket?.on('game:state', handler)
|
||||
if (!this._socket) {
|
||||
console.warn('[SocketClient] Cannot subscribe to game:state - socket not connected')
|
||||
return () => {} // Return no-op unsubscribe
|
||||
}
|
||||
this._socket.on('game:state', handler)
|
||||
return () => this._socket?.off('game:state', handler)
|
||||
}
|
||||
|
||||
@ -239,7 +243,11 @@ class SocketClient {
|
||||
* Subscribe to action results.
|
||||
*/
|
||||
onActionResult(handler: ServerToClientEvents['game:action_result']): () => void {
|
||||
this._socket?.on('game:action_result', handler)
|
||||
if (!this._socket) {
|
||||
console.warn('[SocketClient] Cannot subscribe to game:action_result - socket not connected')
|
||||
return () => {}
|
||||
}
|
||||
this._socket.on('game:action_result', handler)
|
||||
return () => this._socket?.off('game:action_result', handler)
|
||||
}
|
||||
|
||||
@ -247,7 +255,11 @@ class SocketClient {
|
||||
* Subscribe to game errors.
|
||||
*/
|
||||
onGameError(handler: ServerToClientEvents['game:error']): () => void {
|
||||
this._socket?.on('game:error', handler)
|
||||
if (!this._socket) {
|
||||
console.warn('[SocketClient] Cannot subscribe to game:error - socket not connected')
|
||||
return () => {}
|
||||
}
|
||||
this._socket.on('game:error', handler)
|
||||
return () => this._socket?.off('game:error', handler)
|
||||
}
|
||||
|
||||
@ -255,7 +267,11 @@ class SocketClient {
|
||||
* Subscribe to game over events.
|
||||
*/
|
||||
onGameOver(handler: ServerToClientEvents['game:game_over']): () => void {
|
||||
this._socket?.on('game:game_over', handler)
|
||||
if (!this._socket) {
|
||||
console.warn('[SocketClient] Cannot subscribe to game:game_over - socket not connected')
|
||||
return () => {}
|
||||
}
|
||||
this._socket.on('game:game_over', handler)
|
||||
return () => this._socket?.off('game:game_over', handler)
|
||||
}
|
||||
|
||||
@ -263,7 +279,11 @@ class SocketClient {
|
||||
* Subscribe to opponent connection status.
|
||||
*/
|
||||
onOpponentConnected(handler: ServerToClientEvents['game:opponent_connected']): () => void {
|
||||
this._socket?.on('game:opponent_connected', handler)
|
||||
if (!this._socket) {
|
||||
console.warn('[SocketClient] Cannot subscribe to game:opponent_connected - socket not connected')
|
||||
return () => {}
|
||||
}
|
||||
this._socket.on('game:opponent_connected', handler)
|
||||
return () => this._socket?.off('game:opponent_connected', handler)
|
||||
}
|
||||
|
||||
@ -271,7 +291,11 @@ class SocketClient {
|
||||
* Subscribe to turn timeout warnings.
|
||||
*/
|
||||
onTurnTimeoutWarning(handler: ServerToClientEvents['game:turn_timeout_warning']): () => void {
|
||||
this._socket?.on('game:turn_timeout_warning', handler)
|
||||
if (!this._socket) {
|
||||
console.warn('[SocketClient] Cannot subscribe to game:turn_timeout_warning - socket not connected')
|
||||
return () => {}
|
||||
}
|
||||
this._socket.on('game:turn_timeout_warning', handler)
|
||||
return () => this._socket?.off('game:turn_timeout_warning', handler)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,8 +13,9 @@ import type {
|
||||
CardInstance,
|
||||
CardDefinition,
|
||||
TurnPhase,
|
||||
Action,
|
||||
} from '@/types'
|
||||
import { getMyPlayerState, getOpponentState, getCardDefinition } from '@/types'
|
||||
import { getMyPlayerState, getOpponentState, getCardDefinition, ConnectionStatus } from '@/types'
|
||||
|
||||
export const useGameStore = defineStore('game', () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -24,11 +25,20 @@ export const useGameStore = defineStore('game', () => {
|
||||
/** Current game state from server */
|
||||
const gameState = ref<VisibleGameState | null>(null)
|
||||
|
||||
/** Current game ID */
|
||||
const currentGameId = ref<string | null>(null)
|
||||
|
||||
/** Whether we're currently in a game */
|
||||
const isInGame = ref(false)
|
||||
|
||||
/** Connection status */
|
||||
const isConnected = ref(false)
|
||||
/** WebSocket connection status */
|
||||
const connectionStatus = ref<ConnectionStatus>(ConnectionStatus.DISCONNECTED)
|
||||
|
||||
/** Last event ID received from server (for reconnection replay) */
|
||||
const lastEventId = ref<string | null>(null)
|
||||
|
||||
/** Actions queued while offline (for retry on reconnect) */
|
||||
const pendingActions = ref<Action[]>([])
|
||||
|
||||
/** Loading state */
|
||||
const isLoading = ref(false)
|
||||
@ -134,11 +144,33 @@ export const useGameStore = defineStore('game', () => {
|
||||
// Computed - Game State
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Current game ID */
|
||||
/** Current game ID from state (prefer currentGameId ref for setting) */
|
||||
const gameId = computed<string | null>(() =>
|
||||
gameState.value?.game_id ?? null
|
||||
gameState.value?.game_id ?? currentGameId.value
|
||||
)
|
||||
|
||||
/** Current turn phase */
|
||||
const currentPhase = computed<TurnPhase | null>(() =>
|
||||
gameState.value?.phase ?? null
|
||||
)
|
||||
|
||||
/** Computed connection state helpers */
|
||||
const isConnected = computed<boolean>(() =>
|
||||
connectionStatus.value === ConnectionStatus.CONNECTED
|
||||
)
|
||||
|
||||
/** My player state */
|
||||
const myPlayerState = computed<VisiblePlayerState | null>(() => {
|
||||
if (!gameState.value) return null
|
||||
return getMyPlayerState(gameState.value)
|
||||
})
|
||||
|
||||
/** Opponent's player state */
|
||||
const opponentPlayerState = computed<VisiblePlayerState | null>(() => {
|
||||
if (!gameState.value) return null
|
||||
return getOpponentState(gameState.value)
|
||||
})
|
||||
|
||||
/** Whether it's my turn */
|
||||
const isMyTurn = computed<boolean>(() =>
|
||||
gameState.value?.is_my_turn ?? false
|
||||
@ -199,21 +231,48 @@ export const useGameStore = defineStore('game', () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Set the game state (called from WebSocket handler) */
|
||||
function setGameState(state: VisibleGameState): void {
|
||||
function setGameState(state: VisibleGameState, eventId?: string): void {
|
||||
gameState.value = state
|
||||
currentGameId.value = state.game_id
|
||||
isInGame.value = true
|
||||
error.value = null
|
||||
|
||||
// Track last event ID for reconnection support
|
||||
if (eventId) {
|
||||
lastEventId.value = eventId
|
||||
}
|
||||
}
|
||||
|
||||
/** Set WebSocket connection status */
|
||||
function setConnectionStatus(status: ConnectionStatus): void {
|
||||
connectionStatus.value = status
|
||||
}
|
||||
|
||||
/** Queue an action for retry when connection is restored */
|
||||
function queueAction(action: Action): void {
|
||||
pendingActions.value.push(action)
|
||||
}
|
||||
|
||||
/** Clear pending actions (after successful retry or on game exit) */
|
||||
function clearPendingActions(): void {
|
||||
pendingActions.value = []
|
||||
}
|
||||
|
||||
/** Get and clear pending actions (for retry logic) */
|
||||
function takePendingActions(): Action[] {
|
||||
const actions = [...pendingActions.value]
|
||||
pendingActions.value = []
|
||||
return actions
|
||||
}
|
||||
|
||||
/** Clear the game state (when leaving game) */
|
||||
function clearGameState(): void {
|
||||
function clearGame(): void {
|
||||
gameState.value = null
|
||||
currentGameId.value = null
|
||||
isInGame.value = false
|
||||
}
|
||||
|
||||
/** Set connection status */
|
||||
function setConnected(connected: boolean): void {
|
||||
isConnected.value = connected
|
||||
pendingActions.value = []
|
||||
error.value = null
|
||||
// Keep lastEventId for potential reconnection
|
||||
}
|
||||
|
||||
/** Set loading state */
|
||||
@ -233,14 +292,19 @@ export const useGameStore = defineStore('game', () => {
|
||||
return {
|
||||
// State
|
||||
gameState,
|
||||
currentGameId,
|
||||
isInGame,
|
||||
isConnected,
|
||||
connectionStatus,
|
||||
lastEventId,
|
||||
pendingActions,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Player states
|
||||
myPlayer,
|
||||
opponent,
|
||||
myPlayerState,
|
||||
opponentPlayerState,
|
||||
|
||||
// My zones
|
||||
myHand,
|
||||
@ -264,21 +328,31 @@ export const useGameStore = defineStore('game', () => {
|
||||
gameId,
|
||||
isMyTurn,
|
||||
phase,
|
||||
currentPhase,
|
||||
turnNumber,
|
||||
isGameOver,
|
||||
winnerId,
|
||||
didIWin,
|
||||
forcedAction,
|
||||
hasForcedAction,
|
||||
isConnected,
|
||||
|
||||
// Card lookup
|
||||
lookupCard,
|
||||
|
||||
// Actions
|
||||
setGameState,
|
||||
clearGameState,
|
||||
setConnected,
|
||||
setConnectionStatus,
|
||||
queueAction,
|
||||
clearPendingActions,
|
||||
takePendingActions,
|
||||
clearGame,
|
||||
setLoading,
|
||||
setError,
|
||||
}
|
||||
}, {
|
||||
persist: {
|
||||
// Persist only lastEventId for reconnection support
|
||||
paths: ['lastEventId'],
|
||||
},
|
||||
})
|
||||
|
||||
@ -91,6 +91,58 @@ export type {
|
||||
|
||||
export { CARD_SIZES } from './phaser'
|
||||
|
||||
// =============================================================================
|
||||
// WebSocket Types (from ws.ts)
|
||||
// =============================================================================
|
||||
|
||||
export type {
|
||||
// Client messages
|
||||
JoinGameMessage,
|
||||
ActionMessage,
|
||||
ResignMessage,
|
||||
HeartbeatMessage,
|
||||
ClientMessage,
|
||||
// Server messages
|
||||
GameStateMessage,
|
||||
ActionResultMessage,
|
||||
ErrorMessage,
|
||||
TurnStartMessage,
|
||||
TurnTimeoutMessage,
|
||||
GameOverMessage,
|
||||
OpponentStatusMessage,
|
||||
HeartbeatAckMessage,
|
||||
ServerMessage,
|
||||
// Action types
|
||||
Action,
|
||||
PlayCardAction,
|
||||
AttachEnergyAction,
|
||||
EvolveAction,
|
||||
AttackAction,
|
||||
RetreatAction,
|
||||
UseAbilityAction,
|
||||
EndTurnAction,
|
||||
SelectPrizeAction,
|
||||
SelectNewActiveAction,
|
||||
DiscardFromHandAction,
|
||||
} from './ws'
|
||||
|
||||
export {
|
||||
WSErrorCode,
|
||||
ConnectionStatus,
|
||||
// Type guards
|
||||
isGameStateMessage,
|
||||
isActionResultMessage,
|
||||
isErrorMessage,
|
||||
isTurnStartMessage,
|
||||
isTurnTimeoutMessage,
|
||||
isGameOverMessage,
|
||||
isOpponentStatusMessage,
|
||||
isHeartbeatAckMessage,
|
||||
// Utility functions
|
||||
generateMessageId,
|
||||
createClientMessage,
|
||||
} from './ws'
|
||||
|
||||
// =============================================================================
|
||||
// API Types (from api.ts)
|
||||
// =============================================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user