/** * Game Actions Composable * * Provides type-safe methods for emitting game actions to the server. * Wraps Socket.io emit calls with proper error handling and validation. * * This composable is league-agnostic and will be shared between SBA and PD. */ import { computed } from 'vue' import type { DefensiveDecision, OffensiveDecision, ManualOutcomeSubmission, PlayOutcome, } from '~/types' import { useWebSocket } from './useWebSocket' import { useGameStore } from '~/store/game' import { useUiStore } from '~/store/ui' export function useGameActions(gameId?: string) { const { socket, isConnected } = useWebSocket() const gameStore = useGameStore() const uiStore = useUiStore() // Use provided gameId or get from store const currentGameId = computed(() => gameId || gameStore.gameId) /** * Validate socket connection before emitting */ function validateConnection(): boolean { console.log('[GameActions] validateConnection check:', { isConnected: isConnected.value, hasSocket: !!socket.value, currentGameId: currentGameId.value, passedGameId: gameId, storeGameId: gameStore.gameId }) if (!isConnected.value) { console.error('[GameActions] Validation failed: Not connected') uiStore.showError('Not connected to game server') return false } if (!socket.value) { console.error('[GameActions] Validation failed: No socket') uiStore.showError('WebSocket not initialized') return false } if (!currentGameId.value) { console.error('[GameActions] Validation failed: No game ID') uiStore.showError('No active game') return false } return true } // ============================================================================ // Connection Actions // ============================================================================ /** * Join a game room */ function joinGame(role: 'player' | 'spectator' = 'player') { if (!validateConnection()) return console.log(`[GameActions] Joining game ${currentGameId.value} as ${role}`) socket.value!.emit('join_game', { game_id: currentGameId.value!, role, }) } /** * Leave current game room */ function leaveGame() { if (!socket.value || !currentGameId.value) return console.log(`[GameActions] Leaving game ${currentGameId.value}`) socket.value.emit('leave_game', { game_id: currentGameId.value, }) // Clear game state gameStore.resetGame() } // ============================================================================ // Strategic Decision Actions // ============================================================================ /** * Submit defensive decision */ function submitDefensiveDecision(decision: DefensiveDecision) { if (!validateConnection()) return console.log('[GameActions] Submitting defensive decision:', decision) socket.value!.emit('submit_defensive_decision', { game_id: currentGameId.value!, infield_depth: decision.infield_depth, outfield_depth: decision.outfield_depth, hold_runners: decision.hold_runners, }) } /** * Submit offensive decision */ function submitOffensiveDecision(decision: OffensiveDecision) { if (!validateConnection()) return console.log('[GameActions] Submitting offensive decision:', decision) socket.value!.emit('submit_offensive_decision', { game_id: currentGameId.value!, action: decision.action, steal_attempts: decision.steal_attempts, }) } // ============================================================================ // Manual Outcome Workflow // ============================================================================ /** * Roll dice for manual outcome selection */ function rollDice() { if (!validateConnection()) return if (!gameStore.canRollDice) { uiStore.showWarning('Cannot roll dice at this time') return } console.log('[GameActions] Rolling dice') socket.value!.emit('roll_dice', { game_id: currentGameId.value!, }) uiStore.showInfo('Rolling dice...', 2000) } /** * Submit manual outcome after reading card */ function submitManualOutcome(outcome: PlayOutcome, hitLocation?: string) { if (!validateConnection()) return if (!gameStore.canSubmitOutcome) { uiStore.showWarning('Must roll dice first') return } console.log('[GameActions] Submitting outcome:', outcome, hitLocation) socket.value!.emit('submit_manual_outcome', { game_id: currentGameId.value!, outcome: outcome, hit_location: hitLocation, }) uiStore.showInfo('Submitting outcome...', 2000) } // ============================================================================ // X-Check Interactive Workflow // ============================================================================ /** * Submit x-check result selection (result code + error) */ function submitXCheckResult(resultCode: string, errorResult: string) { if (!validateConnection()) return console.log('[GameActions] Submitting x-check result:', resultCode, errorResult) socket.value!.emit('submit_x_check_result', { game_id: currentGameId.value!, result_code: resultCode, error_result: errorResult, }) uiStore.showInfo('Submitting x-check result...', 2000) } /** * Submit DECIDE advance decision (offensive player) */ function submitDecideAdvance(advance: boolean) { if (!validateConnection()) return console.log('[GameActions] Submitting DECIDE advance:', advance) socket.value!.emit('submit_decide_advance', { game_id: currentGameId.value!, advance, }) uiStore.showInfo('Submitting advance decision...', 2000) } /** * Submit DECIDE throw target (defensive player) */ function submitDecideThrow(target: 'runner' | 'first') { if (!validateConnection()) return console.log('[GameActions] Submitting DECIDE throw:', target) socket.value!.emit('submit_decide_throw', { game_id: currentGameId.value!, target, }) uiStore.showInfo('Submitting throw decision...', 2000) } /** * Submit DECIDE speed check result (offensive player) */ function submitDecideResult(outcome: 'safe' | 'out') { if (!validateConnection()) return console.log('[GameActions] Submitting DECIDE result:', outcome) socket.value!.emit('submit_decide_result', { game_id: currentGameId.value!, outcome, }) uiStore.showInfo('Submitting speed check result...', 2000) } // ============================================================================ // Uncapped Hit Decision Tree // ============================================================================ /** * Submit lead runner advance decision (offensive player) */ function submitUncappedLeadAdvance(advance: boolean) { if (!validateConnection()) return console.log('[GameActions] Submitting uncapped lead advance:', advance) socket.value!.emit('submit_uncapped_lead_advance', { game_id: currentGameId.value!, advance, }) uiStore.showInfo('Submitting lead runner decision...', 2000) } /** * Submit defensive throw decision (defensive player) */ function submitUncappedDefensiveThrow(willThrow: boolean) { if (!validateConnection()) return console.log('[GameActions] Submitting uncapped defensive throw:', willThrow) socket.value!.emit('submit_uncapped_defensive_throw', { game_id: currentGameId.value!, will_throw: willThrow, }) uiStore.showInfo('Submitting throw decision...', 2000) } /** * Submit trail runner advance decision (offensive player) */ function submitUncappedTrailAdvance(advance: boolean) { if (!validateConnection()) return console.log('[GameActions] Submitting uncapped trail advance:', advance) socket.value!.emit('submit_uncapped_trail_advance', { game_id: currentGameId.value!, advance, }) uiStore.showInfo('Submitting trail runner decision...', 2000) } /** * Submit throw target selection (defensive player) */ function submitUncappedThrowTarget(target: 'lead' | 'trail') { if (!validateConnection()) return console.log('[GameActions] Submitting uncapped throw target:', target) socket.value!.emit('submit_uncapped_throw_target', { game_id: currentGameId.value!, target, }) uiStore.showInfo('Submitting throw target...', 2000) } /** * Submit safe/out result (offensive player) */ function submitUncappedSafeOut(result: 'safe' | 'out') { if (!validateConnection()) return console.log('[GameActions] Submitting uncapped safe/out:', result) socket.value!.emit('submit_uncapped_safe_out', { game_id: currentGameId.value!, result, }) uiStore.showInfo('Submitting speed check result...', 2000) } // ============================================================================ // Substitution Actions // ============================================================================ /** * Request pinch hitter substitution */ function requestPinchHitter( playerOutLineupId: number, playerInCardId: number, teamId: number ) { if (!validateConnection()) return console.log('[GameActions] Requesting pinch hitter') socket.value!.emit('request_pinch_hitter', { game_id: currentGameId.value!, player_out_lineup_id: playerOutLineupId, player_in_card_id: playerInCardId, team_id: teamId, }) uiStore.showInfo('Requesting pinch hitter...', 3000) } /** * Request defensive replacement */ function requestDefensiveReplacement( playerOutLineupId: number, playerInCardId: number, newPosition: string, teamId: number ) { if (!validateConnection()) return console.log('[GameActions] Requesting defensive replacement') socket.value!.emit('request_defensive_replacement', { game_id: currentGameId.value!, player_out_lineup_id: playerOutLineupId, player_in_card_id: playerInCardId, new_position: newPosition, team_id: teamId, }) uiStore.showInfo('Requesting defensive replacement...', 3000) } /** * Request pitching change */ function requestPitchingChange( playerOutLineupId: number, playerInCardId: number, teamId: number ) { if (!validateConnection()) return console.log('[GameActions] Requesting pitching change') socket.value!.emit('request_pitching_change', { game_id: currentGameId.value!, player_out_lineup_id: playerOutLineupId, player_in_card_id: playerInCardId, team_id: teamId, }) uiStore.showInfo('Requesting pitching change...', 3000) } /** * Submit substitution - unified wrapper for all substitution types * Routes to the appropriate specific method based on type */ function submitSubstitution( type: 'pinch_hitter' | 'defensive_replacement' | 'pitching_change', playerOutLineupId: number, playerInCardId: number, teamId: number, newPosition?: string ) { switch (type) { case 'pinch_hitter': requestPinchHitter(playerOutLineupId, playerInCardId, teamId) break case 'defensive_replacement': if (!newPosition) { uiStore.showError('Position required for defensive replacement') return } requestDefensiveReplacement(playerOutLineupId, playerInCardId, newPosition, teamId) break case 'pitching_change': requestPitchingChange(playerOutLineupId, playerInCardId, teamId) break default: uiStore.showError(`Unknown substitution type: ${type}`) } } // ============================================================================ // Undo/Rollback Actions // ============================================================================ /** * Undo the last N plays * Rolls back plays from the database and reconstructs game state */ function undoLastPlay(numPlays: number = 1) { if (!validateConnection()) return console.log('[GameActions] Undoing last', numPlays, 'play(s)') socket.value!.emit('rollback_play', { game_id: currentGameId.value!, num_plays: numPlays, }) uiStore.showInfo(`Undoing ${numPlays} play(s)...`, 3000) } // ============================================================================ // Data Request Actions // ============================================================================ /** * Get lineup for a team */ function getLineup(teamId: number) { if (!validateConnection()) return console.log('[GameActions] Requesting lineup for team:', teamId) socket.value!.emit('get_lineup', { game_id: currentGameId.value!, team_id: teamId, }) } /** * Get bench players for a team (for substitutions) */ function getBench(teamId: number) { if (!validateConnection()) return console.log('[GameActions] Requesting bench for team:', teamId) socket.value!.emit('get_bench', { game_id: currentGameId.value!, team_id: teamId, }) } /** * Get box score */ function getBoxScore() { if (!validateConnection()) return console.log('[GameActions] Requesting box score') socket.value!.emit('get_box_score', { game_id: currentGameId.value!, }) } /** * Request full game state (for reconnection) */ function requestGameState() { if (!validateConnection()) return console.log('[GameActions] Requesting full game state') socket.value!.emit('request_game_state', { game_id: currentGameId.value!, }) uiStore.showInfo('Syncing game state...', 3000) } // ============================================================================ // Return API // ============================================================================ return { // Connection joinGame, leaveGame, // Strategic decisions submitDefensiveDecision, submitOffensiveDecision, // Manual workflow rollDice, submitManualOutcome, // X-Check interactive workflow submitXCheckResult, submitDecideAdvance, submitDecideThrow, submitDecideResult, // Uncapped hit decision tree submitUncappedLeadAdvance, submitUncappedDefensiveThrow, submitUncappedTrailAdvance, submitUncappedThrowTarget, submitUncappedSafeOut, // Substitutions submitSubstitution, // Undo/Rollback undoLastPlay, // Data requests getLineup, getBench, getBoxScore, requestGameState, } }