strat-gameplay-webapp/frontend-sba/components/Decisions/OffensiveApproach.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

270 lines
8.2 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>
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>