- Add submitSubstitution() method that routes to appropriate internal method - Remove individual substitution methods from exports (now internal only) - Add validation for defensive_replacement requiring position parameter - Update tests to use unified wrapper, add test for validation error Fixes CRIT-001: Game page called non-existent submitSubstitution method Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
375 lines
9.6 KiB
TypeScript
375 lines
9.6 KiB
TypeScript
/**
|
|
* 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)
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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 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,
|
|
|
|
// Substitutions
|
|
submitSubstitution,
|
|
|
|
// Undo/Rollback
|
|
undoLastPlay,
|
|
|
|
// Data requests
|
|
getLineup,
|
|
getBoxScore,
|
|
requestGameState,
|
|
}
|
|
}
|