CLAUDE: Fix hasRunners detection and hide outfield for groundballs

Bug fix:
- Fixed hasRunners prop using wrong property path (gameState.runners.first
  instead of gameState.on_first) - hit location selector was never showing

Optimization:
- Hide outfield positions (LF, CF, RF) for groundball outcomes since
  groundballs by definition stay in the infield
- Auto-clear outfield selection when switching to groundball outcome

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-27 21:17:21 -06:00
parent 9f88317b79
commit c27a652e54
3 changed files with 37 additions and 59 deletions

View File

@ -58,7 +58,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="location-group"> <div v-if="showOutfieldLocations" class="location-group">
<div class="location-group-label">Outfield</div> <div class="location-group-label">Outfield</div>
<div class="location-buttons"> <div class="location-buttons">
<button <button
@ -105,7 +105,7 @@ import { ref, computed } from 'vue'
import type { PlayOutcome, RollData } from '~/types' import type { PlayOutcome, RollData } from '~/types'
// Import centralized outcome constants // Import centralized outcome constants
import { OUTCOME_CATEGORIES, OUTCOMES_REQUIRING_HIT_LOCATION, HIT_LOCATIONS } from '~/constants/outcomes' import { OUTCOME_CATEGORIES, OUTCOMES_REQUIRING_HIT_LOCATION, INFIELD_ONLY_OUTCOMES, HIT_LOCATIONS } from '~/constants/outcomes'
interface Props { interface Props {
rollData: RollData | null rollData: RollData | null
@ -144,6 +144,12 @@ const needsHitLocation = computed(() => {
return props.hasRunners return props.hasRunners
}) })
// Hide outfield positions for groundball outcomes (they stay in the infield)
const showOutfieldLocations = computed(() => {
if (!selectedOutcome.value) return true
return !(INFIELD_ONLY_OUTCOMES as readonly string[]).includes(selectedOutcome.value)
})
const canSubmitForm = computed(() => { const canSubmitForm = computed(() => {
if (!selectedOutcome.value) return false if (!selectedOutcome.value) return false
if (needsHitLocation.value && !selectedHitLocation.value) return false if (needsHitLocation.value && !selectedHitLocation.value) return false
@ -157,6 +163,13 @@ const selectOutcome = (outcome: PlayOutcome) => {
if (!(outcomesNeedingHitLocation as readonly string[]).includes(outcome)) { if (!(outcomesNeedingHitLocation as readonly string[]).includes(outcome)) {
selectedHitLocation.value = null selectedHitLocation.value = null
} }
// Clear outfield hit location if switching to groundball (infield only)
if ((INFIELD_ONLY_OUTCOMES as readonly string[]).includes(outcome)) {
const outfield = HIT_LOCATIONS.outfield as readonly string[]
if (selectedHitLocation.value && outfield.includes(selectedHitLocation.value)) {
selectedHitLocation.value = null
}
}
} }
const selectHitLocation = (location: string) => { const selectHitLocation = (location: string) => {

View File

@ -84,6 +84,16 @@ export const OUTCOMES_REQUIRING_HIT_LOCATION = [
'error', 'error',
] as const satisfies readonly PlayOutcome[] ] as const satisfies readonly PlayOutcome[]
/**
* Outcomes that only allow infield hit locations (no outfield)
* Groundballs by definition stay in the infield
*/
export const INFIELD_ONLY_OUTCOMES = [
'groundball_a',
'groundball_b',
'groundball_c',
] as const satisfies readonly PlayOutcome[]
/** /**
* Hit location options * Hit location options
*/ */

View File

@ -87,7 +87,7 @@
:last-play-result="lastPlayResult" :last-play-result="lastPlayResult"
:can-submit-outcome="canSubmitOutcome" :can-submit-outcome="canSubmitOutcome"
:outs="gameState?.outs ?? 0" :outs="gameState?.outs ?? 0"
:has-runners="!!(gameState?.runners?.first || gameState?.runners?.second || gameState?.runners?.third)" :has-runners="!!(gameState?.on_first || gameState?.on_second || gameState?.on_third)"
@roll-dice="handleRollDice" @roll-dice="handleRollDice"
@submit-outcome="handleSubmitOutcome" @submit-outcome="handleSubmitOutcome"
@dismiss-result="handleDismissResult" @dismiss-result="handleDismissResult"
@ -140,7 +140,7 @@
:last-play-result="lastPlayResult" :last-play-result="lastPlayResult"
:can-submit-outcome="canSubmitOutcome" :can-submit-outcome="canSubmitOutcome"
:outs="gameState?.outs ?? 0" :outs="gameState?.outs ?? 0"
:has-runners="!!(gameState?.runners?.first || gameState?.runners?.second || gameState?.runners?.third)" :has-runners="!!(gameState?.on_first || gameState?.on_second || gameState?.on_third)"
@roll-dice="handleRollDice" @roll-dice="handleRollDice"
@submit-outcome="handleSubmitOutcome" @submit-outcome="handleSubmitOutcome"
@dismiss-result="handleDismissResult" @dismiss-result="handleDismissResult"
@ -555,65 +555,20 @@ const updateScoreBoardHeight = () => {
onMounted(async () => { onMounted(async () => {
console.log('[Game Page] Mounted for game:', gameId.value) console.log('[Game Page] Mounted for game:', gameId.value)
// Check if we have valid auth // Verify authentication via cookies
console.log('[Game Page] Auth check - isAuthenticated:', authStore.isAuthenticated, 'isTokenValid:', authStore.isTokenValid) const isAuthed = await authStore.checkAuth()
console.log('[Game Page] Auth check - isAuthenticated:', authStore.isAuthenticated)
if (!authStore.isAuthenticated || !authStore.isTokenValid) { if (!isAuthed) {
console.warn('[Game Page] No valid authentication - fetching test token from backend') console.warn('[Game Page] Not authenticated - redirecting to login')
// Clear any old auth first navigateTo('/auth/login?redirect=' + encodeURIComponent(route.fullPath))
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 return
} }
} else {
console.log('[Game Page] Using existing valid token')
}
// Connect to WebSocket console.log('[Game Page] Authenticated as:', authStore.currentUser?.username)
if (!isConnected.value) {
console.log('[Game Page] Attempting WebSocket connection...')
connect()
}
// Wait for connection, then join game // WebSocket auto-connects via watch on isAuthenticated in useWebSocket composable
// Just wait for connection, then join game
watch(isConnected, async (connected) => { watch(isConnected, async (connected) => {
if (connected) { if (connected) {
connectionStatus.value = 'connected' connectionStatus.value = 'connected'