/** * Game Store * * Manages active game state, synchronized with backend via WebSocket. * This is the central state container for real-time gameplay. */ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import type { GameState, PlayResult, DecisionPrompt, DefensiveDecision, OffensiveDecision, RollData, Lineup, BenchPlayer, XCheckData, DecideAdvanceData, DecideThrowData, DecideSpeedCheckData, } from '~/types' export const useGameStore = defineStore('game', () => { // ============================================================================ // State // ============================================================================ const gameState = ref(null) const homeLineup = ref([]) const awayLineup = ref([]) const homeBench = ref([]) const awayBench = ref([]) const playHistory = ref([]) const currentDecisionPrompt = ref(null) const pendingRoll = ref(null) const lastPlayResult = ref(null) const isConnected = ref(false) const isLoading = ref(false) const error = ref(null) // X-Check workflow state const xCheckData = ref(null) const decideData = ref(null) // Decision state (local pending decisions before submission) const pendingDefensiveSetup = ref(null) const pendingOffensiveDecision = ref | null>(null) const pendingStealAttempts = ref([]) const decisionHistory = ref>([]) // ============================================================================ // Getters // ============================================================================ const gameId = computed(() => gameState.value?.game_id ?? null) const leagueId = computed(() => gameState.value?.league_id ?? null) const currentInning = computed(() => gameState.value?.inning ?? 1) const currentHalf = computed(() => gameState.value?.half ?? 'top') const outs = computed(() => gameState.value?.outs ?? 0) const balls = computed(() => gameState.value?.balls ?? 0) const strikes = computed(() => gameState.value?.strikes ?? 0) const homeScore = computed(() => gameState.value?.home_score ?? 0) const awayScore = computed(() => gameState.value?.away_score ?? 0) const gameStatus = computed(() => gameState.value?.status ?? 'pending') const isGameActive = computed(() => gameStatus.value === 'active') const isGameComplete = computed(() => gameStatus.value === 'completed') const currentBatter = computed(() => gameState.value?.current_batter ?? null) const currentPitcher = computed(() => gameState.value?.current_pitcher ?? null) const currentCatcher = computed(() => gameState.value?.current_catcher ?? null) const runnersOnBase = computed(() => { const runners: number[] = [] if (gameState.value?.on_first) runners.push(1) if (gameState.value?.on_second) runners.push(2) if (gameState.value?.on_third) runners.push(3) return runners }) const basesLoaded = computed(() => runnersOnBase.value.length === 3) const basesEmpty = computed(() => runnersOnBase.value.length === 0) const runnerInScoringPosition = computed(() => runnersOnBase.value.includes(2) || runnersOnBase.value.includes(3) ) const battingTeamId = computed(() => { if (!gameState.value) return null return gameState.value.half === 'top' ? gameState.value.away_team_id : gameState.value.home_team_id }) const fieldingTeamId = computed(() => { if (!gameState.value) return null return gameState.value.half === 'top' ? gameState.value.home_team_id : gameState.value.away_team_id }) const isBattingTeamAI = computed(() => { if (!gameState.value) return false return gameState.value.half === 'top' ? gameState.value.away_team_is_ai : gameState.value.home_team_is_ai }) const isFieldingTeamAI = computed(() => { if (!gameState.value) return false return gameState.value.half === 'top' ? gameState.value.home_team_is_ai : gameState.value.away_team_is_ai }) const needsDefensiveDecision = computed(() => { // Standardized naming (2025-01-21): Both backend and frontend now use 'awaiting_defensive' return currentDecisionPrompt.value?.phase === 'awaiting_defensive' || gameState.value?.decision_phase === 'awaiting_defensive' }) const needsOffensiveDecision = computed(() => { // Standardized naming (2025-01-21): Both backend and frontend now use 'awaiting_offensive' return currentDecisionPrompt.value?.phase === 'awaiting_offensive' || gameState.value?.decision_phase === 'awaiting_offensive' }) const needsStolenBaseDecision = computed(() => { // Standardized naming (2025-01-21): Both backend and frontend now use 'awaiting_stolen_base' return currentDecisionPrompt.value?.phase === 'awaiting_stolen_base' || gameState.value?.decision_phase === 'awaiting_stolen_base' }) const needsXCheckResult = computed(() => { return currentDecisionPrompt.value?.phase === 'awaiting_x_check_result' || gameState.value?.decision_phase === 'awaiting_x_check_result' }) const needsDecideAdvance = computed(() => { return currentDecisionPrompt.value?.phase === 'awaiting_decide_advance' || gameState.value?.decision_phase === 'awaiting_decide_advance' }) const needsDecideThrow = computed(() => { return currentDecisionPrompt.value?.phase === 'awaiting_decide_throw' || gameState.value?.decision_phase === 'awaiting_decide_throw' }) const needsDecideResult = computed(() => { return currentDecisionPrompt.value?.phase === 'awaiting_decide_result' || gameState.value?.decision_phase === 'awaiting_decide_result' }) const canRollDice = computed(() => { return gameState.value?.decision_phase === 'resolution' && !pendingRoll.value }) const canSubmitOutcome = computed(() => { return pendingRoll.value !== null }) const recentPlays = computed(() => { return playHistory.value.slice(-10).reverse() }) // ============================================================================ // Actions // ============================================================================ /** * Set complete game state (from server) */ function setGameState(state: GameState) { const oldBatter = gameState.value?.current_batter const newBatter = state?.current_batter const oldBatterInfo = oldBatter ? `lineup_id=${oldBatter.lineup_id}, batting_order=${oldBatter.batting_order}` : 'None' const newBatterInfo = newBatter ? `lineup_id=${newBatter.lineup_id}, batting_order=${newBatter.batting_order}` : 'None' console.log('[GameStore] setGameState - current_batter:', oldBatterInfo, '->', newBatterInfo) gameState.value = state error.value = null } /** * Update partial game state */ function updateGameState(updates: Partial) { if (gameState.value) { gameState.value = { ...gameState.value, ...updates } } } /** * Set lineups for both teams */ function setLineups(home: Lineup[], away: Lineup[]) { homeLineup.value = home awayLineup.value = away } /** * Update lineup for a specific team */ function updateLineup(teamId: number, lineup: Lineup[]) { if (teamId === gameState.value?.home_team_id) { homeLineup.value = lineup } else if (teamId === gameState.value?.away_team_id) { awayLineup.value = lineup } } /** * Set bench players for a specific team * Bench players come from RosterLink (players not in active lineup) * with is_pitcher/is_batter computed properties for UI filtering */ function setBench(teamId: number, bench: BenchPlayer[]) { if (teamId === gameState.value?.home_team_id) { homeBench.value = bench } else if (teamId === gameState.value?.away_team_id) { awayBench.value = bench } } /** * Set play history (replaces entire array - used for initial sync) * O(1) operation - no deduplication needed */ function setPlayHistory(plays: PlayResult[]) { playHistory.value = plays } /** * Add play to history (used for live play_resolved events) * O(1) operation - assumes no duplicates from live events */ function addPlayToHistory(play: PlayResult) { playHistory.value.push(play) // Update game state from play result if provided if (play.new_state) { updateGameState(play.new_state) } } /** * Set current decision prompt */ function setDecisionPrompt(prompt: DecisionPrompt | null) { currentDecisionPrompt.value = prompt } /** * Clear decision prompt after submission */ function clearDecisionPrompt() { currentDecisionPrompt.value = null } /** * Set pending dice roll */ function setPendingRoll(roll: RollData | null) { pendingRoll.value = roll } /** * Clear pending roll after outcome submission */ function clearPendingRoll() { pendingRoll.value = null } /** * Set last play result */ function setLastPlayResult(result: PlayResult | null) { lastPlayResult.value = result } /** * Clear last play result */ function clearLastPlayResult() { lastPlayResult.value = null } /** * Set connection status */ function setConnected(connected: boolean) { isConnected.value = connected } /** * Set loading state */ function setLoading(loading: boolean) { isLoading.value = loading } /** * Set error */ function setError(message: string | null) { error.value = message } /** * Set pending defensive setup (before submission) */ function setPendingDefensiveSetup(setup: DefensiveDecision | null) { pendingDefensiveSetup.value = setup } /** * Set pending offensive decision (before submission) */ function setPendingOffensiveDecision(decision: Omit | null) { pendingOffensiveDecision.value = decision } /** * Set pending steal attempts (before submission) */ function setPendingStealAttempts(attempts: number[]) { pendingStealAttempts.value = attempts } /** * Add decision to history */ function addDecisionToHistory(type: 'Defensive' | 'Offensive', summary: string) { decisionHistory.value.unshift({ type, summary, timestamp: new Date().toLocaleTimeString(), }) // Keep only last 10 decisions if (decisionHistory.value.length > 10) { decisionHistory.value = decisionHistory.value.slice(0, 10) } } /** * Clear all pending decisions */ function clearPendingDecisions() { pendingDefensiveSetup.value = null pendingOffensiveDecision.value = null pendingStealAttempts.value = [] } /** * Set x-check data (from decision_required event) */ function setXCheckData(data: XCheckData | null) { xCheckData.value = data } /** * Clear x-check data after resolution */ function clearXCheckData() { xCheckData.value = null } /** * Set DECIDE data (from decision_required event) */ function setDecideData(data: DecideAdvanceData | DecideThrowData | DecideSpeedCheckData | null) { decideData.value = data } /** * Clear DECIDE data after resolution */ function clearDecideData() { decideData.value = null } /** * Reset game store (when leaving game) */ function resetGame() { gameState.value = null homeLineup.value = [] awayLineup.value = [] homeBench.value = [] awayBench.value = [] playHistory.value = [] currentDecisionPrompt.value = null pendingRoll.value = null isConnected.value = false isLoading.value = false error.value = null pendingDefensiveSetup.value = null pendingOffensiveDecision.value = null pendingStealAttempts.value = [] decisionHistory.value = [] xCheckData.value = null decideData.value = null } /** * Get active lineup for a team */ function getActiveLineup(teamId: number): Lineup[] { const lineup = teamId === gameState.value?.home_team_id ? homeLineup.value : awayLineup.value return lineup.filter(p => p.is_active) } /** * Get bench players for a team */ function getBenchPlayers(teamId: number): Lineup[] { const lineup = teamId === gameState.value?.home_team_id ? homeLineup.value : awayLineup.value return lineup.filter(p => !p.is_active) } /** * Find player in lineup by lineup_id */ function findPlayerInLineup(lineupId: number): Lineup | undefined { return [...homeLineup.value, ...awayLineup.value].find( p => p.lineup_id === lineupId ) } // ============================================================================ // Return Store API // ============================================================================ return { // State gameState: readonly(gameState), homeLineup: readonly(homeLineup), awayLineup: readonly(awayLineup), homeBench: readonly(homeBench), awayBench: readonly(awayBench), playHistory: readonly(playHistory), currentDecisionPrompt: readonly(currentDecisionPrompt), pendingRoll: readonly(pendingRoll), lastPlayResult: readonly(lastPlayResult), isConnected: readonly(isConnected), isLoading: readonly(isLoading), error: readonly(error), pendingDefensiveSetup: readonly(pendingDefensiveSetup), pendingOffensiveDecision: readonly(pendingOffensiveDecision), pendingStealAttempts: readonly(pendingStealAttempts), decisionHistory: readonly(decisionHistory), xCheckData: readonly(xCheckData), decideData: readonly(decideData), // Getters gameId, leagueId, currentInning, currentHalf, outs, balls, strikes, homeScore, awayScore, gameStatus, isGameActive, isGameComplete, currentBatter, currentPitcher, currentCatcher, runnersOnBase, basesLoaded, basesEmpty, runnerInScoringPosition, battingTeamId, fieldingTeamId, isBattingTeamAI, isFieldingTeamAI, needsDefensiveDecision, needsOffensiveDecision, needsStolenBaseDecision, needsXCheckResult, needsDecideAdvance, needsDecideThrow, needsDecideResult, canRollDice, canSubmitOutcome, recentPlays, // Actions setGameState, updateGameState, setLineups, updateLineup, setBench, setPlayHistory, addPlayToHistory, setDecisionPrompt, clearDecisionPrompt, setPendingRoll, clearPendingRoll, setLastPlayResult, clearLastPlayResult, setConnected, setLoading, setError, setPendingDefensiveSetup, setPendingOffensiveDecision, setPendingStealAttempts, addDecisionToHistory, clearPendingDecisions, setXCheckData, clearXCheckData, setDecideData, clearDecideData, resetGame, getActiveLineup, getBenchPlayers, findPlayerInLineup, } })