197 lines
5.8 KiB
Vue
197 lines
5.8 KiB
Vue
<template>
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between mb-6">
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||
<span class="text-2xl">🛡️</span>
|
||
Defensive Setup
|
||
</h3>
|
||
<span
|
||
v-if="!isActive"
|
||
class="px-3 py-1 text-xs font-semibold rounded-full bg-gray-200 text-gray-600"
|
||
>
|
||
Opponent's Turn
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Form -->
|
||
<form class="space-y-6" @submit.prevent="handleSubmit">
|
||
<!-- Infield Depth -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Infield Depth
|
||
</label>
|
||
<ButtonGroup
|
||
v-model="infieldDepth"
|
||
:options="infieldDepthOptions"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
variant="primary"
|
||
vertical
|
||
/>
|
||
</div>
|
||
|
||
<!-- Outfield Depth -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Outfield Depth
|
||
</label>
|
||
<ButtonGroup
|
||
v-model="outfieldDepth"
|
||
:options="outfieldDepthOptions"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
variant="primary"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Visual Preview -->
|
||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-gray-700 dark:to-gray-600 rounded-lg p-4">
|
||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||
Current Setup
|
||
</h4>
|
||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||
<div>
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Infield:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ infieldDisplay }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Outfield:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ outfieldDisplay }}</span>
|
||
</div>
|
||
<div class="col-span-2">
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Holding:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ holdingDisplay }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Submit Button -->
|
||
<ActionButton
|
||
type="submit"
|
||
variant="success"
|
||
size="lg"
|
||
:disabled="!isActive"
|
||
:loading="submitting"
|
||
full-width
|
||
>
|
||
{{ submitButtonText }}
|
||
</ActionButton>
|
||
</form>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue'
|
||
import type { DefensiveDecision, GameState } from '~/types/game'
|
||
import ButtonGroup from '~/components/UI/ButtonGroup.vue'
|
||
import type { ButtonGroupOption } from '~/components/UI/ButtonGroup.vue'
|
||
import ActionButton from '~/components/UI/ActionButton.vue'
|
||
import { useDefensiveSetup } from '~/composables/useDefensiveSetup'
|
||
|
||
interface Props {
|
||
gameId: string
|
||
isActive: boolean
|
||
currentSetup?: DefensiveDecision
|
||
gameState?: GameState
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
isActive: false,
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
submit: [setup: DefensiveDecision]
|
||
}>()
|
||
|
||
const { infieldDepth, outfieldDepth, holdRunnersArray, getDecision, syncFromDecision } = useDefensiveSetup()
|
||
|
||
// Local state
|
||
const submitting = ref(false)
|
||
|
||
// Dynamic options based on game state
|
||
const infieldDepthOptions = computed<ButtonGroupOption[]>(() => {
|
||
const options: ButtonGroupOption[] = [
|
||
{ value: 'normal', label: 'Normal', icon: '•' },
|
||
]
|
||
|
||
// Only show infield_in and corners_in if runner on third
|
||
if (props.gameState?.on_third) {
|
||
options.push({ value: 'infield_in', label: 'Infield In', icon: '⬆️' })
|
||
options.push({ value: 'corners_in', label: 'Corners In', icon: '◀️▶️' })
|
||
}
|
||
|
||
return options
|
||
})
|
||
|
||
const outfieldDepthOptions = computed<ButtonGroupOption[]>(() => {
|
||
const options: ButtonGroupOption[] = [
|
||
{ value: 'normal', label: 'Normal', icon: '•' },
|
||
]
|
||
|
||
// Check for walk-off scenario
|
||
if (props.gameState) {
|
||
const { inning, half, home_score, away_score, on_first, on_second, on_third } = props.gameState
|
||
const isHomeBatting = half === 'bottom'
|
||
const isLateInning = inning >= 9
|
||
const isCloseGame = isHomeBatting
|
||
? home_score <= away_score
|
||
: away_score <= home_score
|
||
const hasRunners = on_first || on_second || on_third
|
||
|
||
// Only show shallow in walk-off situations
|
||
if (isHomeBatting && isLateInning && isCloseGame && hasRunners) {
|
||
options.push({ value: 'shallow', label: 'Shallow', icon: '⬇️' })
|
||
}
|
||
}
|
||
|
||
return options
|
||
})
|
||
|
||
// Display helpers
|
||
const infieldDisplay = computed(() => {
|
||
const option = infieldDepthOptions.value.find(opt => opt.value === infieldDepth.value)
|
||
return option?.label || 'Normal'
|
||
})
|
||
|
||
const outfieldDisplay = computed(() => {
|
||
const option = outfieldDepthOptions.value.find(opt => opt.value === outfieldDepth.value)
|
||
return option?.label || 'Normal'
|
||
})
|
||
|
||
const holdingDisplay = computed(() => {
|
||
const arr = holdRunnersArray.value
|
||
if (arr.length === 0) return 'None'
|
||
return arr.map(base => {
|
||
if (base === 1) return '1st'
|
||
if (base === 2) return '2nd'
|
||
if (base === 3) return '3rd'
|
||
return base
|
||
}).join(', ')
|
||
})
|
||
|
||
const submitButtonText = computed(() => {
|
||
if (!props.isActive) return 'Wait for Your Turn'
|
||
return 'Submit Defensive Setup'
|
||
})
|
||
|
||
// Handle form submission
|
||
const handleSubmit = async () => {
|
||
if (!props.isActive) return
|
||
|
||
submitting.value = true
|
||
try {
|
||
emit('submit', getDecision())
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// Sync composable state from prop when it changes (e.g. server-confirmed state)
|
||
watch(() => props.currentSetup, (newSetup) => {
|
||
if (newSetup) {
|
||
syncFromDecision(newSetup)
|
||
}
|
||
}, { deep: true })
|
||
</script>
|