270 lines
8.2 KiB
Vue
270 lines
8.2 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>
|
||
Offensive Action
|
||
</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">
|
||
<!-- Action Selection -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Select Action
|
||
</label>
|
||
<div class="grid grid-cols-1 gap-3">
|
||
<button
|
||
v-for="option in availableActions"
|
||
:key="option.value"
|
||
type="button"
|
||
:disabled="!isActive || option.disabled"
|
||
:class="getActionButtonClasses(option.value, option.disabled)"
|
||
:title="option.disabledReason"
|
||
@click="selectAction(option.value)"
|
||
>
|
||
<div class="flex items-start gap-3">
|
||
<span class="text-2xl flex-shrink-0">{{ option.icon }}</span>
|
||
<div class="flex-1 text-left">
|
||
<div class="font-semibold text-base">{{ option.label }}</div>
|
||
<div class="text-sm opacity-90 mt-0.5">
|
||
{{ option.disabled ? option.disabledReason : option.description }}
|
||
</div>
|
||
</div>
|
||
<div v-if="localDecision.action === option.value" class="flex-shrink-0">
|
||
<span class="text-xl">✓</span>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Current Strategy Summary -->
|
||
<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 Strategy
|
||
</h4>
|
||
<div class="space-y-1 text-xs">
|
||
<div>
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Action:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ currentActionLabel }}</span>
|
||
</div>
|
||
<div v-if="actionRequiresSpecialHandling" class="mt-2 p-2 bg-yellow-100 dark:bg-yellow-900/30 rounded">
|
||
<span class="font-medium text-yellow-800 dark:text-yellow-200">{{ specialHandlingNote }}</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 { OffensiveDecision } from '~/types/game'
|
||
import ActionButton from '~/components/UI/ActionButton.vue'
|
||
|
||
interface ActionOption {
|
||
value: OffensiveDecision['action']
|
||
label: string
|
||
icon: string
|
||
description: string
|
||
disabled: boolean
|
||
disabledReason?: string
|
||
}
|
||
|
||
interface Props {
|
||
gameId: string
|
||
isActive: boolean
|
||
currentDecision?: Omit<OffensiveDecision, 'steal_attempts'>
|
||
hasRunnersOnBase?: boolean
|
||
runnerOnFirst?: boolean
|
||
runnerOnSecond?: boolean
|
||
runnerOnThird?: boolean
|
||
basesLoaded?: boolean
|
||
outs?: number
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
isActive: false,
|
||
hasRunnersOnBase: false,
|
||
runnerOnFirst: false,
|
||
runnerOnSecond: false,
|
||
runnerOnThird: false,
|
||
basesLoaded: false,
|
||
outs: 0,
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
submit: [decision: Omit<OffensiveDecision, 'steal_attempts'>]
|
||
}>()
|
||
|
||
// Local state
|
||
const submitting = ref(false)
|
||
const localDecision = ref<Omit<OffensiveDecision, 'steal_attempts'>>({
|
||
action: props.currentDecision?.action || 'swing_away',
|
||
})
|
||
|
||
// Action options with smart filtering
|
||
const availableActions = computed<ActionOption[]>(() => {
|
||
const twoOuts = props.outs >= 2
|
||
|
||
return [
|
||
{
|
||
value: 'swing_away',
|
||
label: 'Swing Away',
|
||
icon: '⚾',
|
||
description: 'Normal swing, no special tactics',
|
||
disabled: false,
|
||
},
|
||
{
|
||
value: 'steal',
|
||
label: 'Steal',
|
||
icon: '🏃',
|
||
description: 'Attempt to steal base(s) - configure on steal inputs tab',
|
||
disabled: false,
|
||
},
|
||
{
|
||
value: 'check_jump',
|
||
label: 'Check Jump',
|
||
icon: '👀',
|
||
description: 'Lead runner checks jump at start of delivery',
|
||
disabled: !props.hasRunnersOnBase,
|
||
disabledReason: 'Requires runner on base',
|
||
},
|
||
{
|
||
value: 'hit_and_run',
|
||
label: 'Hit and Run',
|
||
icon: '💨',
|
||
description: 'Runner(s) take off as pitcher delivers; batter must make contact',
|
||
disabled: !props.hasRunnersOnBase,
|
||
disabledReason: 'Requires runner on base',
|
||
},
|
||
{
|
||
value: 'sac_bunt',
|
||
label: 'Sacrifice Bunt',
|
||
icon: '🎯',
|
||
description: 'Bunt to advance runners, batter likely out',
|
||
disabled: twoOuts,
|
||
disabledReason: twoOuts ? 'Cannot bunt with 2 outs' : undefined,
|
||
},
|
||
{
|
||
value: 'squeeze_bunt',
|
||
label: 'Squeeze Bunt',
|
||
icon: '🔥',
|
||
description: 'Runner on 3rd breaks for home as pitcher delivers',
|
||
disabled: !props.runnerOnThird || twoOuts,
|
||
disabledReason: twoOuts
|
||
? 'Cannot squeeze with 2 outs'
|
||
: !props.runnerOnThird
|
||
? 'Requires runner on third'
|
||
: undefined,
|
||
},
|
||
]
|
||
})
|
||
|
||
// Computed
|
||
const currentActionLabel = computed(() => {
|
||
const option = availableActions.value.find(opt => opt.value === localDecision.value.action)
|
||
return option?.label || 'Swing Away'
|
||
})
|
||
|
||
const actionRequiresSpecialHandling = computed(() => {
|
||
return localDecision.value.action === 'steal' || localDecision.value.action === 'squeeze_bunt'
|
||
})
|
||
|
||
const specialHandlingNote = computed(() => {
|
||
if (localDecision.value.action === 'steal') {
|
||
return 'Configure which bases to steal on the Stolen Base Inputs tab'
|
||
}
|
||
if (localDecision.value.action === 'squeeze_bunt') {
|
||
return 'R3 will break for home as pitcher delivers - high risk, high reward!'
|
||
}
|
||
return ''
|
||
})
|
||
|
||
const hasChanges = computed(() => {
|
||
if (!props.currentDecision) return true
|
||
return localDecision.value.action !== props.currentDecision.action
|
||
})
|
||
|
||
const submitButtonText = computed(() => {
|
||
if (!props.isActive) return 'Wait for Your Turn'
|
||
if (!hasChanges.value) return 'Submit (Keep Action)'
|
||
return 'Submit Offensive Strategy'
|
||
})
|
||
|
||
// Methods
|
||
const selectAction = (action: OffensiveDecision['action']) => {
|
||
if (!props.isActive) return
|
||
|
||
// Check if action is disabled
|
||
const option = availableActions.value.find(opt => opt.value === action)
|
||
if (option?.disabled) return
|
||
|
||
localDecision.value.action = action
|
||
}
|
||
|
||
const getActionButtonClasses = (action: OffensiveDecision['action'], disabled: boolean) => {
|
||
const isSelected = localDecision.value.action === action
|
||
const base = 'w-full p-4 rounded-lg border-2 transition-all duration-200 disabled:cursor-not-allowed'
|
||
|
||
if (disabled) {
|
||
return `${base} bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500 border-gray-200 dark:border-gray-700 opacity-60`
|
||
}
|
||
|
||
if (isSelected) {
|
||
return `${base} bg-gradient-to-r from-blue-600 to-blue-700 text-white border-blue-700 shadow-lg`
|
||
} else {
|
||
return `${base} bg-white dark:bg-gray-700 text-gray-900 dark:text-white border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400`
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
if (!props.isActive) return
|
||
|
||
submitting.value = true
|
||
try {
|
||
emit('submit', { ...localDecision.value })
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// Watch for prop changes
|
||
watch(() => props.currentDecision, (newDecision) => {
|
||
if (newDecision) {
|
||
localDecision.value = { ...newDecision }
|
||
}
|
||
}, { deep: true })
|
||
|
||
// Auto-reset to swing_away if current action becomes invalid
|
||
watch(() => availableActions.value, (actions) => {
|
||
const currentAction = localDecision.value.action
|
||
const currentOption = actions.find(opt => opt.value === currentAction)
|
||
|
||
if (currentOption?.disabled) {
|
||
localDecision.value.action = 'swing_away'
|
||
}
|
||
}, { deep: true })
|
||
</script>
|