/** * Game store for managing active game state. * * Receives VisibleGameState from WebSocket and provides computed * helpers for accessing player data, cards, and game status. */ import { ref, computed } from 'vue' import { defineStore } from 'pinia' import type { VisibleGameState, VisiblePlayerState, CardInstance, CardDefinition, TurnPhase, Action, RulesConfig, } from '@/types' import { getMyPlayerState, getOpponentState, getCardDefinition, ConnectionStatus } from '@/types' export const useGameStore = defineStore('game', () => { // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- /** Current game state from server */ const gameState = ref(null) /** Current game ID */ const currentGameId = ref(null) /** Whether we're currently in a game */ const isInGame = ref(false) /** WebSocket connection status */ const connectionStatus = ref(ConnectionStatus.DISCONNECTED) /** Last event ID received from server (for reconnection replay) */ const lastEventId = ref(null) /** Actions queued while offline (for retry on reconnect) */ const pendingActions = ref([]) /** Loading state */ const isLoading = ref(false) /** Error message */ const error = ref(null) // --------------------------------------------------------------------------- // Computed - Player States // --------------------------------------------------------------------------- /** My player state */ const myPlayer = computed(() => { if (!gameState.value) return null return getMyPlayerState(gameState.value) }) /** Opponent's player state */ const opponent = computed(() => { if (!gameState.value) return null return getOpponentState(gameState.value) }) // --------------------------------------------------------------------------- // Computed - My Zones // --------------------------------------------------------------------------- /** Cards in my hand */ const myHand = computed(() => myPlayer.value?.hand.cards ?? [] ) /** My active Pokemon */ const myActive = computed(() => myPlayer.value?.active.cards[0] ?? null ) /** My benched Pokemon */ const myBench = computed(() => myPlayer.value?.bench.cards ?? [] ) /** My discard pile */ const myDiscard = computed(() => myPlayer.value?.discard.cards ?? [] ) /** My available energy */ const myEnergy = computed(() => myPlayer.value?.energy_zone.cards ?? [] ) /** My deck count */ const myDeckCount = computed(() => myPlayer.value?.deck_count ?? 0 ) /** My prize count */ const myPrizeCount = computed(() => myPlayer.value?.prizes_count ?? 0 ) /** My score */ const myScore = computed(() => myPlayer.value?.score ?? 0 ) // --------------------------------------------------------------------------- // Computed - Opponent Zones // --------------------------------------------------------------------------- /** Opponent's active Pokemon */ const oppActive = computed(() => opponent.value?.active.cards[0] ?? null ) /** Opponent's benched Pokemon */ const oppBench = computed(() => opponent.value?.bench.cards ?? [] ) /** Opponent's hand count (not contents) */ const oppHandCount = computed(() => opponent.value?.hand.count ?? 0 ) /** Opponent's deck count */ const oppDeckCount = computed(() => opponent.value?.deck_count ?? 0 ) /** Opponent's prize count */ const oppPrizeCount = computed(() => opponent.value?.prizes_count ?? 0 ) /** Opponent's score */ const oppScore = computed(() => opponent.value?.score ?? 0 ) // --------------------------------------------------------------------------- // Computed - Game State // --------------------------------------------------------------------------- /** Current game ID from state (prefer currentGameId ref for setting) */ const gameId = computed(() => gameState.value?.game_id ?? currentGameId.value ) /** Current turn phase */ const currentPhase = computed(() => gameState.value?.phase ?? null ) /** Computed connection state helpers */ const isConnected = computed(() => connectionStatus.value === ConnectionStatus.CONNECTED ) /** My player state */ const myPlayerState = computed(() => { if (!gameState.value) return null return getMyPlayerState(gameState.value) }) /** Opponent's player state */ const opponentPlayerState = computed(() => { if (!gameState.value) return null return getOpponentState(gameState.value) }) /** Whether it's my turn */ const isMyTurn = computed(() => gameState.value?.is_my_turn ?? false ) /** Current turn phase */ const phase = computed(() => gameState.value?.phase ?? null ) /** Current turn number */ const turnNumber = computed(() => gameState.value?.turn_number ?? 0 ) /** Whether game is over */ const isGameOver = computed(() => gameState.value?.winner_id !== null ) /** Winner ID if game is over */ const winnerId = computed(() => gameState.value?.winner_id ?? null ) /** Whether I won */ const didIWin = computed(() => winnerId.value === gameState.value?.viewer_id ) /** Forced action info */ const forcedAction = computed(() => { if (!gameState.value?.forced_action_type) return null return { player: gameState.value.forced_action_player, type: gameState.value.forced_action_type, reason: gameState.value.forced_action_reason, } }) /** Whether I need to respond to a forced action */ const hasForcedAction = computed(() => forcedAction.value?.player === gameState.value?.viewer_id ) // --------------------------------------------------------------------------- // Computed - Rules Config // --------------------------------------------------------------------------- /** Current game rules configuration */ const rulesConfig = computed(() => gameState.value?.rules_config ?? null ) /** Whether this game uses classic prize cards (vs points system) */ const usePrizeCards = computed(() => rulesConfig.value?.prizes.use_prize_cards ?? false ) /** Number of prizes/points needed to win */ const prizeCount = computed(() => rulesConfig.value?.prizes.count ?? 4 ) /** Maximum number of Pokemon on the bench */ const benchSize = computed(() => rulesConfig.value?.bench.max_size ?? 5 ) /** Whether this game uses an energy deck zone (Pokemon Pocket style) */ const energyDeckEnabled = computed(() => rulesConfig.value?.deck.energy_deck_enabled ?? true ) // --------------------------------------------------------------------------- // Computed - Card Lookup // --------------------------------------------------------------------------- /** Look up a card definition by ID */ function lookupCard(definitionId: string): CardDefinition | null { if (!gameState.value) return null return getCardDefinition(gameState.value, definitionId) } // --------------------------------------------------------------------------- // Actions // --------------------------------------------------------------------------- /** Set the game state (called from WebSocket handler) */ 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 clearGame(): void { gameState.value = null currentGameId.value = null isInGame.value = false pendingActions.value = [] error.value = null // Keep lastEventId for potential reconnection } /** Set loading state */ function setLoading(loading: boolean): void { isLoading.value = loading } /** Set error message */ function setError(message: string | null): void { error.value = message } // --------------------------------------------------------------------------- // Return // --------------------------------------------------------------------------- return { // State gameState, currentGameId, isInGame, connectionStatus, lastEventId, pendingActions, isLoading, error, // Player states myPlayer, opponent, myPlayerState, opponentPlayerState, // My zones myHand, myActive, myBench, myDiscard, myEnergy, myDeckCount, myPrizeCount, myScore, // Opponent zones oppActive, oppBench, oppHandCount, oppDeckCount, oppPrizeCount, oppScore, // Game state gameId, isMyTurn, phase, currentPhase, turnNumber, isGameOver, winnerId, didIWin, forcedAction, hasForcedAction, isConnected, // Rules config rulesConfig, usePrizeCards, prizeCount, benchSize, energyDeckEnabled, // Card lookup lookupCard, // Actions setGameState, setConnectionStatus, queueAction, clearPendingActions, takePendingActions, clearGame, setLoading, setError, } }, { persist: { // Persist only lastEventId for reconnection support paths: ['lastEventId'], }, })