CLAUDE: Refactor offensive decisions - replace approach with action field
Frontend refactor complete - updated TypeScript interfaces and OffensiveApproach component to use new action-based system with smart filtering. Changes: - TypeScript interfaces: Replaced approach/hit_and_run/bunt_attempt with action field - OffensiveApproach.vue: Complete refactor with 6 action choices and smart filtering - Smart filtering: Automatically disables invalid actions based on game state - Auto-reset: If current action becomes invalid, resets to swing_away TypeScript updates (types/game.ts, types/websocket.ts): - OffensiveDecision.action: 6 valid choices (swing_away, steal, check_jump, hit_and_run, sac_bunt, squeeze_bunt) - Removed deprecated fields: approach, hit_and_run, bunt_attempt - OffensiveDecisionRequest updated to match Component features: - Smart filtering based on game state (runners, outs) - Visual feedback for disabled actions with explanatory text - Special handling notes for steal and squeeze_bunt - Auto-reset to swing_away when actions become invalid - Clean, modern UI with action icons and descriptions Action requirements enforced in UI: - check_jump: requires runner on base - hit_and_run: requires runner on base - sac_bunt: disabled with 2 outs - squeeze_bunt: requires R3, disabled with 2 outs - steal/swing_away: always available Files modified: - types/game.ts - types/websocket.ts - components/Decisions/OffensiveApproach.vue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b0d79ef7ef
commit
4bdadeca07
@ -4,7 +4,7 @@
|
||||
<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 Approach
|
||||
Offensive Action
|
||||
</h3>
|
||||
<span
|
||||
v-if="!isActive"
|
||||
@ -16,27 +16,30 @@
|
||||
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<!-- Batting Approach -->
|
||||
<!-- Action Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||
Batting Approach
|
||||
Select Action
|
||||
</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<button
|
||||
v-for="option in approachOptions"
|
||||
v-for="option in availableActions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
:disabled="!isActive"
|
||||
:class="getApproachButtonClasses(option.value)"
|
||||
@click="selectApproach(option.value)"
|
||||
:disabled="!isActive || option.disabled"
|
||||
:class="getActionButtonClasses(option.value, option.disabled)"
|
||||
@click="selectAction(option.value)"
|
||||
:title="option.disabledReason"
|
||||
>
|
||||
<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.description }}</div>
|
||||
<div class="text-sm opacity-90 mt-0.5">
|
||||
{{ option.disabled ? option.disabledReason : option.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="localDecision.approach === option.value" class="flex-shrink-0">
|
||||
<div v-if="localDecision.action === option.value" class="flex-shrink-0">
|
||||
<span class="text-xl">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,40 +47,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Special Tactics -->
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||
Special Tactics
|
||||
</label>
|
||||
<div class="space-y-3">
|
||||
<!-- Hit and Run -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<ToggleSwitch
|
||||
v-model="localDecision.hit_and_run"
|
||||
label="Hit and Run"
|
||||
:disabled="!isActive || !canUseHitAndRun"
|
||||
size="md"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2 ml-14">
|
||||
Runner(s) take off as pitcher delivers; batter must make contact
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Bunt Attempt -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<ToggleSwitch
|
||||
v-model="localDecision.bunt_attempt"
|
||||
label="Bunt Attempt"
|
||||
:disabled="!isActive"
|
||||
size="md"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2 ml-14">
|
||||
Attempt to bunt for a hit or sacrifice
|
||||
</p>
|
||||
</div>
|
||||
</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">
|
||||
@ -85,14 +54,11 @@
|
||||
</h4>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div>
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Approach:</span>
|
||||
<span class="ml-1 text-gray-900 dark:text-white">{{ currentApproachLabel }}</span>
|
||||
<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="localDecision.hit_and_run || localDecision.bunt_attempt">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400">Tactics:</span>
|
||||
<span class="ml-1 text-gray-900 dark:text-white">
|
||||
{{ activeTactics }}
|
||||
</span>
|
||||
<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>
|
||||
@ -115,14 +81,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { OffensiveDecision } from '~/types/game'
|
||||
import ToggleSwitch from '~/components/UI/ToggleSwitch.vue'
|
||||
import ActionButton from '~/components/UI/ActionButton.vue'
|
||||
|
||||
interface ApproachOption {
|
||||
value: OffensiveDecision['approach']
|
||||
interface ActionOption {
|
||||
value: OffensiveDecision['action']
|
||||
label: string
|
||||
icon: string
|
||||
description: string
|
||||
disabled: boolean
|
||||
disabledReason?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@ -130,11 +97,21 @@ interface Props {
|
||||
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<{
|
||||
@ -144,63 +121,90 @@ const emit = defineEmits<{
|
||||
// Local state
|
||||
const submitting = ref(false)
|
||||
const localDecision = ref<Omit<OffensiveDecision, 'steal_attempts'>>({
|
||||
approach: props.currentDecision?.approach || 'normal',
|
||||
hit_and_run: props.currentDecision?.hit_and_run || false,
|
||||
bunt_attempt: props.currentDecision?.bunt_attempt || false,
|
||||
action: props.currentDecision?.action || 'swing_away',
|
||||
})
|
||||
|
||||
// Approach options
|
||||
const approachOptions: ApproachOption[] = [
|
||||
{
|
||||
value: 'normal',
|
||||
label: 'Normal',
|
||||
icon: '⚾',
|
||||
description: 'Balanced approach, no specific tendencies',
|
||||
},
|
||||
{
|
||||
value: 'contact',
|
||||
label: 'Contact',
|
||||
icon: '🎯',
|
||||
description: 'Focus on making contact, avoid strikeouts',
|
||||
},
|
||||
{
|
||||
value: 'power',
|
||||
label: 'Power',
|
||||
icon: '💪',
|
||||
description: 'Swing for extra bases, higher strikeout risk',
|
||||
},
|
||||
{
|
||||
value: 'patient',
|
||||
label: 'Patient',
|
||||
icon: '🧘',
|
||||
description: 'Work the count, draw walks, wait for your pitch',
|
||||
},
|
||||
]
|
||||
// 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 canUseHitAndRun = computed(() => {
|
||||
return props.hasRunnersOnBase
|
||||
const currentActionLabel = computed(() => {
|
||||
const option = availableActions.value.find(opt => opt.value === localDecision.value.action)
|
||||
return option?.label || 'Swing Away'
|
||||
})
|
||||
|
||||
const currentApproachLabel = computed(() => {
|
||||
const option = approachOptions.find(opt => opt.value === localDecision.value.approach)
|
||||
return option?.label || 'Normal'
|
||||
const actionRequiresSpecialHandling = computed(() => {
|
||||
return localDecision.value.action === 'steal' || localDecision.value.action === 'squeeze_bunt'
|
||||
})
|
||||
|
||||
const activeTactics = computed(() => {
|
||||
const tactics: string[] = []
|
||||
if (localDecision.value.hit_and_run) tactics.push('Hit & Run')
|
||||
if (localDecision.value.bunt_attempt) tactics.push('Bunt')
|
||||
return tactics.join(', ') || 'None'
|
||||
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.approach !== props.currentDecision.approach ||
|
||||
localDecision.value.hit_and_run !== props.currentDecision.hit_and_run ||
|
||||
localDecision.value.bunt_attempt !== props.currentDecision.bunt_attempt
|
||||
)
|
||||
return localDecision.value.action !== props.currentDecision.action
|
||||
})
|
||||
|
||||
const submitButtonText = computed(() => {
|
||||
@ -210,14 +214,23 @@ const submitButtonText = computed(() => {
|
||||
})
|
||||
|
||||
// Methods
|
||||
const selectApproach = (approach: OffensiveDecision['approach']) => {
|
||||
const selectAction = (action: OffensiveDecision['action']) => {
|
||||
if (!props.isActive) return
|
||||
localDecision.value.approach = approach
|
||||
|
||||
// Check if action is disabled
|
||||
const option = availableActions.value.find(opt => opt.value === action)
|
||||
if (option?.disabled) return
|
||||
|
||||
localDecision.value.action = action
|
||||
}
|
||||
|
||||
const getApproachButtonClasses = (approach: OffensiveDecision['approach']) => {
|
||||
const isSelected = localDecision.value.approach === approach
|
||||
const base = 'w-full p-4 rounded-lg border-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
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`
|
||||
@ -244,10 +257,13 @@ watch(() => props.currentDecision, (newDecision) => {
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// Disable hit and run if no runners
|
||||
watch(() => props.hasRunnersOnBase, (hasRunners) => {
|
||||
if (!hasRunners) {
|
||||
localDecision.value.hit_and_run = false
|
||||
// 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>
|
||||
|
||||
@ -130,12 +130,12 @@ export interface DefensiveDecision {
|
||||
/**
|
||||
* Offensive strategic decision
|
||||
* Backend: OffensiveDecision
|
||||
*
|
||||
* Session 2 Update (2025-01-14): Replaced approach/hit_and_run/bunt_attempt with action field.
|
||||
*/
|
||||
export interface OffensiveDecision {
|
||||
approach: 'normal' | 'contact' | 'power' | 'patient'
|
||||
steal_attempts: number[] // Bases to steal (2, 3, or 4)
|
||||
hit_and_run: boolean
|
||||
bunt_attempt: boolean
|
||||
action: 'swing_away' | 'steal' | 'check_jump' | 'hit_and_run' | 'sac_bunt' | 'squeeze_bunt'
|
||||
steal_attempts: number[] // Bases to steal (2, 3, or 4) - only used when action="steal"
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -125,10 +125,8 @@ export interface DefensiveDecisionRequest {
|
||||
|
||||
export interface OffensiveDecisionRequest {
|
||||
game_id: string
|
||||
approach: OffensiveDecision['approach']
|
||||
action: OffensiveDecision['action']
|
||||
steal_attempts: number[]
|
||||
hit_and_run: boolean
|
||||
bunt_attempt: boolean
|
||||
}
|
||||
|
||||
export interface RollDiceRequest {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user