strat-gameplay-webapp/frontend-sba/components/Decisions/DefensiveSetup.vue

197 lines
5.8 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>