strat-gameplay-webapp/frontend-sba/pages/games/[id].vue
Cal Corum 2381456189 test: Skip unstable test suites
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 20:18:33 -06:00

724 lines
26 KiB
Vue
Executable File

<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- Sticky ScoreBoard Header -->
<div ref="scoreBoardRef" class="sticky top-0 z-20">
<ScoreBoard
:home-score="gameState?.home_score"
:away-score="gameState?.away_score"
:inning="gameState?.inning"
:half="gameState?.half"
:balls="gameState?.balls"
:strikes="gameState?.strikes"
:outs="gameState?.outs"
:runners="runnersState"
/>
</div>
<!-- Main Game Container -->
<div class="container mx-auto px-4 py-6 lg:py-8">
<!-- Connection Status Banner -->
<div
v-if="!isConnected"
class="mb-4 bg-yellow-50 border-l-4 border-yellow-400 p-4 rounded-lg"
>
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-700">
{{ connectionStatus === 'connecting' ? 'Connecting to game server...' : 'Disconnected from server. Attempting to reconnect...' }}
</p>
</div>
</div>
</div>
<!-- Mobile Layout (Stacked) -->
<div class="lg:hidden space-y-6">
<!-- Current Situation -->
<CurrentSituation
:current-batter="gameState?.current_batter"
:current-pitcher="gameState?.current_pitcher"
/>
<!-- Game Board -->
<GameBoard
:runners="runnersState"
:current-batter="gameState?.current_batter"
:current-pitcher="gameState?.current_pitcher"
/>
<!-- Play-by-Play Feed -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-md">
<PlayByPlay
:plays="playHistory"
:limit="5"
:compact="true"
/>
</div>
<!-- Decision Panel (Phase F3) -->
<DecisionPanel
v-if="showDecisions"
:game-id="gameId"
:current-team="currentTeam"
:is-my-turn="isMyTurn"
:phase="decisionPhase"
:runners="runnersData"
:current-defensive-setup="pendingDefensiveSetup ?? undefined"
:current-offensive-decision="pendingOffensiveDecision ?? undefined"
:current-steal-attempts="pendingStealAttempts"
:decision-history="decisionHistory"
@defensive-submit="handleDefensiveSubmit"
@offensive-submit="handleOffensiveSubmit"
@steal-attempts-submit="handleStealAttemptsSubmit"
/>
<!-- Gameplay Panel (Phase F4) -->
<GameplayPanel
v-if="showGameplay"
:game-id="gameId"
:is-my-turn="isMyTurn"
:can-roll-dice="canRollDice"
:pending-roll="pendingRoll"
:last-play-result="lastPlayResult"
:can-submit-outcome="canSubmitOutcome"
:outs="gameState?.outs ?? 0"
:has-runners="!!(gameState?.runners?.first || gameState?.runners?.second || gameState?.runners?.third)"
@roll-dice="handleRollDice"
@submit-outcome="handleSubmitOutcome"
@dismiss-result="handleDismissResult"
/>
</div>
<!-- Desktop Layout (Grid) -->
<div class="hidden lg:grid lg:grid-cols-3 gap-6">
<!-- Left Column: Game State -->
<div class="lg:col-span-2 space-y-6">
<!-- Current Situation -->
<CurrentSituation
:current-batter="gameState?.current_batter"
:current-pitcher="gameState?.current_pitcher"
/>
<!-- Game Board -->
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg">
<GameBoard
:runners="runnersState"
:current-batter="gameState?.current_batter"
:current-pitcher="gameState?.current_pitcher"
/>
</div>
<!-- Decision Panel (Phase F3) -->
<DecisionPanel
v-if="showDecisions"
:game-id="gameId"
:current-team="currentTeam"
:is-my-turn="isMyTurn"
:phase="decisionPhase"
:runners="runnersData"
:current-defensive-setup="pendingDefensiveSetup ?? undefined"
:current-offensive-decision="pendingOffensiveDecision ?? undefined"
:current-steal-attempts="pendingStealAttempts"
:decision-history="decisionHistory"
@defensive-submit="handleDefensiveSubmit"
@offensive-submit="handleOffensiveSubmit"
@steal-attempts-submit="handleStealAttemptsSubmit"
/>
<!-- Gameplay Panel (Phase F4) -->
<GameplayPanel
v-if="showGameplay"
:game-id="gameId"
:is-my-turn="isMyTurn"
:can-roll-dice="canRollDice"
:pending-roll="pendingRoll"
:last-play-result="lastPlayResult"
:can-submit-outcome="canSubmitOutcome"
:outs="gameState?.outs ?? 0"
:has-runners="!!(gameState?.runners?.first || gameState?.runners?.second || gameState?.runners?.third)"
@roll-dice="handleRollDice"
@submit-outcome="handleSubmitOutcome"
@dismiss-result="handleDismissResult"
/>
</div>
<!-- Right Column: Play-by-Play -->
<div class="lg:col-span-1">
<div
class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg sticky"
:style="{ top: `${scoreBoardHeight + 16}px` }"
>
<PlayByPlay
:plays="playHistory"
:scrollable="true"
:max-height="600"
:show-filters="true"
/>
</div>
</div>
</div>
<!-- Loading State -->
<div
v-if="isLoading"
class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
>
<div class="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-2xl text-center">
<div class="w-16 h-16 mx-auto mb-4 border-4 border-primary border-t-transparent rounded-full animate-spin"/>
<p class="text-gray-900 dark:text-white font-semibold">Loading game...</p>
</div>
</div>
<!-- Game Not Started State -->
<div
v-if="gameState && gameState.status === 'pending'"
class="mt-6 bg-blue-50 dark:bg-blue-900/20 border-2 border-blue-200 dark:border-blue-700 rounded-xl p-8 text-center"
>
<div class="w-20 h-20 mx-auto mb-4 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">Game Starting Soon</h3>
<p class="text-gray-600 dark:text-gray-400">
Waiting for all players to join. The game will begin once everyone is ready.
</p>
</div>
<!-- Game Ended State -->
<div
v-if="gameState && gameState.status === 'completed'"
class="mt-6 bg-green-50 dark:bg-green-900/20 border-2 border-green-200 dark:border-green-700 rounded-xl p-8 text-center"
>
<div class="w-20 h-20 mx-auto mb-4 bg-green-100 dark:bg-green-800 rounded-full flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Game Complete!</h3>
<p class="text-xl text-gray-700 dark:text-gray-300 mb-4">
Final Score: {{ gameState.away_score }} - {{ gameState.home_score }}
</p>
<button
class="px-6 py-3 bg-primary hover:bg-blue-700 text-white rounded-lg font-semibold transition shadow-md"
@click="navigateTo('/games')"
>
Back to Games
</button>
</div>
</div>
<!-- Substitution Panel Modal (Phase F5) -->
<Teleport to="body">
<div
v-if="showSubstitutions"
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
@click.self="handleSubstitutionCancel"
>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<SubstitutionPanel
v-if="myTeamId"
:game-id="gameId"
:team-id="myTeamId"
:current-lineup="currentLineup"
:bench-players="benchPlayers"
:current-pitcher="currentPitcher"
:current-batter="currentBatter"
@pinch-hitter="handlePinchHitter"
@defensive-replacement="handleDefensiveReplacement"
@pitching-change="handlePitchingChange"
@cancel="handleSubstitutionCancel"
/>
</div>
</div>
</Teleport>
<!-- Floating Action Button for Substitutions -->
<button
v-if="canMakeSubstitutions"
class="fixed bottom-6 right-6 w-16 h-16 bg-primary hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center z-40 transition-all hover:scale-110"
aria-label="Open Substitutions"
@click="showSubstitutions = true"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</button>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '~/store/game'
import { useAuthStore } from '~/store/auth'
import { useUiStore } from '~/store/ui'
import { useWebSocket } from '~/composables/useWebSocket'
import { useGameActions } from '~/composables/useGameActions'
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
import GameBoard from '~/components/Game/GameBoard.vue'
import CurrentSituation from '~/components/Game/CurrentSituation.vue'
import PlayByPlay from '~/components/Game/PlayByPlay.vue'
import DecisionPanel from '~/components/Decisions/DecisionPanel.vue'
import GameplayPanel from '~/components/Gameplay/GameplayPanel.vue'
import SubstitutionPanel from '~/components/Substitutions/SubstitutionPanel.vue'
import type { DefensiveDecision, OffensiveDecision, PlayOutcome, RollData, PlayResult } from '~/types/game'
import type { Lineup } from '~/types/player'
definePageMeta({
layout: 'game',
// middleware: ['auth'], // Temporarily disabled for WebSocket testing
})
const route = useRoute()
const gameStore = useGameStore()
const authStore = useAuthStore()
const uiStore = useUiStore()
// Initialize auth from localStorage (for testing without OAuth)
// TEMPORARY: Clear old test tokens to force refresh
if (import.meta.client && localStorage.getItem('auth_token')?.startsWith('test-token-')) {
console.log('[Game Page] Clearing old test token')
localStorage.clear()
}
authStore.initializeAuth()
// Get game ID from route
const gameId = computed(() => route.params.id as string)
// WebSocket connection
const { socket, isConnected, connectionError, connect } = useWebSocket()
// Pass the raw string value from route params, not computed value
// useGameActions will create its own computed internally if needed
const actions = useGameActions(route.params.id as string)
// Game state from store
const gameState = computed(() => gameStore.gameState)
const playHistory = computed(() => gameStore.playHistory)
const canRollDice = computed(() => gameStore.canRollDice)
const canSubmitOutcome = computed(() => gameStore.canSubmitOutcome)
const pendingDefensiveSetup = computed(() => gameStore.pendingDefensiveSetup)
const pendingOffensiveDecision = computed(() => gameStore.pendingOffensiveDecision)
const pendingStealAttempts = computed(() => gameStore.pendingStealAttempts)
const decisionHistory = computed(() => gameStore.decisionHistory)
const needsDefensiveDecision = computed(() => gameStore.needsDefensiveDecision)
const needsOffensiveDecision = computed(() => gameStore.needsOffensiveDecision)
const basesEmpty = computed(() => gameStore.basesEmpty)
const pendingRoll = computed(() => gameStore.pendingRoll)
const lastPlayResult = computed(() => gameStore.lastPlayResult)
const currentDecisionPrompt = computed(() => gameStore.currentDecisionPrompt)
// Local UI state
const isLoading = ref(true)
const connectionStatus = ref<'connecting' | 'connected' | 'disconnected'>('connecting')
const showSubstitutions = ref(false)
const myTeamId = ref<number | null>(null) // TODO: Get from auth/game state
// Dynamic ScoreBoard height tracking
const scoreBoardRef = ref<HTMLElement | null>(null)
const scoreBoardHeight = ref(0)
// Computed helpers
const runnersState = computed(() => {
if (!gameState.value) {
return { first: false, second: false, third: false }
}
return {
first: gameState.value.on_first !== null,
second: gameState.value.on_second !== null,
third: gameState.value.on_third !== null
}
})
const runnersData = computed(() => {
return {
first: gameState.value?.on_first ?? null,
second: gameState.value?.on_second ?? null,
third: gameState.value?.on_third ?? null,
}
})
const currentTeam = computed(() => {
return gameState.value?.half === 'top' ? 'away' : 'home'
})
const isMyTurn = computed(() => {
// TODO: Implement actual team ownership logic
// For now, assume it's always the player's turn for testing
return true
})
const decisionPhase = computed(() => {
if (needsDefensiveDecision.value) return 'defensive'
if (needsOffensiveDecision.value) return 'offensive'
return 'idle'
})
// Phase F6: Conditional panel rendering
const showDecisions = computed(() => {
// Don't show decision panels if there's a result pending dismissal
if (lastPlayResult.value) {
return false
}
const result = gameState.value?.status === 'active' &&
isMyTurn.value &&
(needsDefensiveDecision.value || needsOffensiveDecision.value)
// Debug logging
console.log('[Game Page] Panel visibility check:', {
gameStatus: gameState.value?.status,
isMyTurn: isMyTurn.value,
needsDefensiveDecision: needsDefensiveDecision.value,
needsOffensiveDecision: needsOffensiveDecision.value,
decision_phase: gameState.value?.decision_phase,
showDecisions: result,
currentDecisionPrompt: currentDecisionPrompt.value,
hasLastPlayResult: !!lastPlayResult.value
})
return result
})
const showGameplay = computed(() => {
// Show gameplay panel if there's a result to display OR if we're in the resolution phase
if (lastPlayResult.value) {
return true
}
return gameState.value?.status === 'active' &&
isMyTurn.value &&
!needsDefensiveDecision.value &&
!needsOffensiveDecision.value
})
const canMakeSubstitutions = computed(() => {
return gameState.value?.status === 'active' && isMyTurn.value
})
// Lineup helpers for substitutions
const currentLineup = computed(() => {
if (!myTeamId.value) return []
return myTeamId.value === gameState.value?.home_team_id
? gameStore.homeLineup.filter(l => l.is_active)
: gameStore.awayLineup.filter(l => l.is_active)
})
const benchPlayers = computed(() => {
if (!myTeamId.value) return []
return myTeamId.value === gameState.value?.home_team_id
? gameStore.homeLineup.filter(l => !l.is_active)
: gameStore.awayLineup.filter(l => !l.is_active)
})
const currentBatter = computed(() => {
const batterState = gameState.value?.current_batter
if (!batterState) return null
return gameStore.findPlayerInLineup(batterState.lineup_id)
})
const currentPitcher = computed(() => {
const pitcherState = gameState.value?.current_pitcher
if (!pitcherState) return null
return gameStore.findPlayerInLineup(pitcherState.lineup_id)
})
// Methods - Gameplay (Phase F4)
const handleRollDice = async () => {
console.log('[Game Page] Rolling dice')
try {
await actions.rollDice()
// The dice_rolled event will update pendingRoll via WebSocket
} catch (error) {
console.error('[Game Page] Failed to roll dice:', error)
}
}
const handleSubmitOutcome = async (data: { outcome: PlayOutcome; hitLocation?: string }) => {
console.log('[Game Page] Submitting outcome:', data)
try {
await actions.submitManualOutcome(
data.outcome,
data.hitLocation
)
// Pending roll will be cleared by backend after successful submission
} catch (error) {
console.error('[Game Page] Failed to submit outcome:', error)
}
}
const handleDismissResult = () => {
console.log('[Game Page] Dismissing result')
gameStore.clearLastPlayResult()
}
const handleDefensiveSubmit = async (decision: DefensiveDecision) => {
console.log('[Game Page] Submitting defensive decision:', decision)
try {
await actions.submitDefensiveDecision(decision)
gameStore.setPendingDefensiveSetup(decision)
gameStore.addDecisionToHistory('Defensive', `${decision.infield_depth} infield, ${decision.outfield_depth} outfield`)
} catch (error) {
console.error('[Game Page] Failed to submit defensive decision:', error)
}
}
const handleOffensiveSubmit = async (decision: Omit<OffensiveDecision, 'steal_attempts'>) => {
console.log('[Game Page] Submitting offensive decision:', decision)
try {
// Combine with steal attempts
const fullDecision: OffensiveDecision = {
...decision,
steal_attempts: pendingStealAttempts.value,
}
await actions.submitOffensiveDecision(fullDecision)
gameStore.setPendingOffensiveDecision(decision)
const actionLabels: Record<string, string> = {
swing_away: 'Swing Away',
steal: 'Steal',
check_jump: 'Check Jump',
hit_and_run: 'Hit & Run',
sac_bunt: 'Sac Bunt',
squeeze_bunt: 'Squeeze Bunt',
}
gameStore.addDecisionToHistory('Offensive', actionLabels[decision.action] || decision.action)
} catch (error) {
console.error('[Game Page] Failed to submit offensive decision:', error)
}
}
const handleStealAttemptsSubmit = (attempts: number[]) => {
console.log('[Game Page] Updating steal attempts:', attempts)
gameStore.setPendingStealAttempts(attempts)
}
// Methods - Substitutions (Phase F5)
const handlePinchHitter = async (data: { playerOutLineupId: number; playerInCardId: number; teamId: number }) => {
console.log('[Game Page] Submitting pinch hitter:', data)
try {
await actions.submitSubstitution(
'pinch_hitter',
data.playerOutLineupId,
data.playerInCardId,
data.teamId
)
showSubstitutions.value = false
} catch (error) {
console.error('[Game Page] Failed to submit pinch hitter:', error)
}
}
const handleDefensiveReplacement = async (data: { playerOutLineupId: number; playerInCardId: number; newPosition: string; teamId: number }) => {
console.log('[Game Page] Submitting defensive replacement:', data)
try {
await actions.submitSubstitution(
'defensive_replacement',
data.playerOutLineupId,
data.playerInCardId,
data.teamId,
data.newPosition
)
showSubstitutions.value = false
} catch (error) {
console.error('[Game Page] Failed to submit defensive replacement:', error)
}
}
const handlePitchingChange = async (data: { playerOutLineupId: number; playerInCardId: number; teamId: number }) => {
console.log('[Game Page] Submitting pitching change:', data)
try {
await actions.submitSubstitution(
'pitching_change',
data.playerOutLineupId,
data.playerInCardId,
data.teamId
)
showSubstitutions.value = false
} catch (error) {
console.error('[Game Page] Failed to submit pitching change:', error)
}
}
const handleSubstitutionCancel = () => {
console.log('[Game Page] Cancelling substitution')
showSubstitutions.value = false
}
// Measure ScoreBoard height dynamically
const updateScoreBoardHeight = () => {
if (scoreBoardRef.value) {
scoreBoardHeight.value = scoreBoardRef.value.offsetHeight
console.log('[Game Page] ScoreBoard height:', scoreBoardHeight.value)
}
}
// Lifecycle
onMounted(async () => {
console.log('[Game Page] Mounted for game:', gameId.value)
// Check if we have valid auth
console.log('[Game Page] Auth check - isAuthenticated:', authStore.isAuthenticated, 'isTokenValid:', authStore.isTokenValid)
if (!authStore.isAuthenticated || !authStore.isTokenValid) {
console.warn('[Game Page] No valid authentication - fetching test token from backend')
// Clear any old auth first
authStore.clearAuth()
try {
// Fetch a valid JWT token from backend
const response = await fetch('http://localhost:8000/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: 'test-user-1',
username: 'TestPlayer',
discord_id: 'test-discord-id'
})
})
if (!response.ok) {
throw new Error(`Failed to get token: ${response.statusText}`)
}
const data = await response.json()
console.log('[Game Page] Received token from backend')
// Set auth with real JWT token
authStore.setAuth({
access_token: data.access_token,
refresh_token: 'test-refresh-token',
expires_in: 604800, // 7 days in seconds
user: {
id: 'test-user-1',
discord_id: 'test-discord-id',
username: 'TestPlayer',
discriminator: '0001',
avatar: null,
email: 'test@example.com',
}
})
console.log('[Game Page] Test token set - isAuthenticated:', authStore.isAuthenticated, 'isTokenValid:', authStore.isTokenValid)
} catch (error) {
console.error('[Game Page] Failed to fetch test token:', error)
connectionStatus.value = 'disconnected'
isLoading.value = false
return
}
} else {
console.log('[Game Page] Using existing valid token')
}
// Connect to WebSocket
if (!isConnected.value) {
console.log('[Game Page] Attempting WebSocket connection...')
connect()
}
// Wait for connection, then join game
watch(isConnected, async (connected) => {
if (connected) {
connectionStatus.value = 'connected'
console.log('[Game Page] Connected - Joining game as player')
// Join game room
await actions.joinGame('player')
// Request current game state
await actions.requestGameState()
isLoading.value = false
} else {
connectionStatus.value = 'disconnected'
}
}, { immediate: true })
// Timeout fallback - if not connected after 5 seconds, stop loading
setTimeout(() => {
if (isLoading.value) {
console.error('[Game Page] Connection timeout - stopping loading state')
isLoading.value = false
connectionStatus.value = 'disconnected'
}
}, 5000)
// Measure ScoreBoard height after initial render
setTimeout(() => {
updateScoreBoardHeight()
}, 100)
// Update on window resize
if (import.meta.client) {
window.addEventListener('resize', updateScoreBoardHeight)
}
})
// Watch for game state to load lineups
watch(gameState, (state) => {
if (state && state.home_team_id && state.away_team_id) {
// Request lineup data for both teams to populate player names
console.log('[Game Page] Game state received - requesting lineups for teams:', state.home_team_id, state.away_team_id)
actions.getLineup(state.home_team_id)
actions.getLineup(state.away_team_id)
}
}, { immediate: true })
// Quality of Life: Auto-submit default decisions when bases are empty
watch([needsDefensiveDecision, needsOffensiveDecision, basesEmpty], ([defensive, offensive, empty]) => {
// Only auto-submit if it's the player's turn and bases are empty
if (!isMyTurn.value || !empty) return
// Auto-submit defensive decision with defaults
if (defensive && !pendingDefensiveSetup.value) {
const defaultDefense: DefensiveDecision = {
infield_depth: 'normal',
outfield_depth: 'normal',
hold_runners: []
}
console.log('[Game Page] Bases empty - auto-submitting default defensive decision')
uiStore.showInfo('Bases empty - auto-submitting default defensive setup', 2000)
handleDefensiveSubmit(defaultDefense)
}
// Auto-submit offensive decision with swing away
if (offensive && !pendingOffensiveDecision.value) {
const defaultOffense = {
action: 'swing_away' as const
}
console.log('[Game Page] Bases empty - auto-submitting default offensive decision')
uiStore.showInfo('Bases empty - auto-submitting swing away', 2000)
handleOffensiveSubmit(defaultOffense)
}
})
onUnmounted(() => {
console.log('[Game Page] Unmounted - Leaving game')
// Leave game room
actions.leaveGame()
// Reset game store
gameStore.resetGame()
// Cleanup resize listener
if (import.meta.client) {
window.removeEventListener('resize', updateScoreBoardHeight)
}
})
// Watch for connection errors
watch(connectionError, (error) => {
if (error) {
console.error('[Game Page] Connection error:', error)
}
})
</script>
<style scoped>
/* Additional styling if needed */
</style>