Created centralized services for SBA player data fetching at lineup creation: Backend - New Services: - app/services/sba_api_client.py: REST client for SBA API (api.sba.manticorum.com) with batch player fetching and caching support - app/services/lineup_service.py: High-level service combining DB operations with API calls for complete lineup entries with player data Backend - Refactored Components: - app/core/game_engine.py: Replaced raw API calls with LineupService, reduced _prepare_next_play() from ~50 lines to ~15 lines - app/core/substitution_manager.py: Updated pinch_hit(), defensive_replace(), change_pitcher() to use lineup_service.get_sba_player_data() - app/models/game_models.py: Added player_name/player_image to LineupPlayerState - app/services/__init__.py: Exported new LineupService components - app/websocket/handlers.py: Enhanced lineup state handling Frontend - SBA League: - components/Game/CurrentSituation.vue: Restored player images with fallback badges (P/B letters) for both mobile and desktop layouts - components/Game/GameBoard.vue: Enhanced game board visualization - composables/useGameActions.ts: Updated game action handling - composables/useWebSocket.ts: Improved WebSocket state management - pages/games/[id].vue: Enhanced game page with better state handling - types/game.ts: Updated type definitions - types/websocket.ts: Added WebSocket type support Architecture Improvement: All SBA player data fetching now goes through LineupService: - Lineup creation: add_sba_player_to_lineup() - Lineup loading: load_team_lineup_with_player_data() - Substitutions: get_sba_player_data() All 739 unit tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
321 lines
8.1 KiB
TypeScript
321 lines
8.1 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,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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)
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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
|
|
requestPinchHitter,
|
|
requestDefensiveReplacement,
|
|
requestPitchingChange,
|
|
|
|
// Data requests
|
|
getLineup,
|
|
getBoxScore,
|
|
requestGameState,
|
|
}
|
|
}
|