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">
|
<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">
|
<h3 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
<span class="text-2xl">⚔️</span>
|
<span class="text-2xl">⚔️</span>
|
||||||
Offensive Approach
|
Offensive Action
|
||||||
</h3>
|
</h3>
|
||||||
<span
|
<span
|
||||||
v-if="!isActive"
|
v-if="!isActive"
|
||||||
@ -16,27 +16,30 @@
|
|||||||
|
|
||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||||
<!-- Batting Approach -->
|
<!-- Action Selection -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||||
Batting Approach
|
Select Action
|
||||||
</label>
|
</label>
|
||||||
<div class="grid grid-cols-1 gap-3">
|
<div class="grid grid-cols-1 gap-3">
|
||||||
<button
|
<button
|
||||||
v-for="option in approachOptions"
|
v-for="option in availableActions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="!isActive"
|
:disabled="!isActive || option.disabled"
|
||||||
:class="getApproachButtonClasses(option.value)"
|
:class="getActionButtonClasses(option.value, option.disabled)"
|
||||||
@click="selectApproach(option.value)"
|
@click="selectAction(option.value)"
|
||||||
|
:title="option.disabledReason"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<span class="text-2xl flex-shrink-0">{{ option.icon }}</span>
|
<span class="text-2xl flex-shrink-0">{{ option.icon }}</span>
|
||||||
<div class="flex-1 text-left">
|
<div class="flex-1 text-left">
|
||||||
<div class="font-semibold text-base">{{ option.label }}</div>
|
<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>
|
||||||
<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>
|
<span class="text-xl">✓</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -44,40 +47,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- 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">
|
<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">
|
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||||
@ -85,14 +54,11 @@
|
|||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-1 text-xs">
|
<div class="space-y-1 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Approach:</span>
|
<span class="font-medium text-gray-600 dark:text-gray-400">Action:</span>
|
||||||
<span class="ml-1 text-gray-900 dark:text-white">{{ currentApproachLabel }}</span>
|
<span class="ml-1 text-gray-900 dark:text-white">{{ currentActionLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="localDecision.hit_and_run || localDecision.bunt_attempt">
|
<div v-if="actionRequiresSpecialHandling" class="mt-2 p-2 bg-yellow-100 dark:bg-yellow-900/30 rounded">
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Tactics:</span>
|
<span class="font-medium text-yellow-800 dark:text-yellow-200">{{ specialHandlingNote }}</span>
|
||||||
<span class="ml-1 text-gray-900 dark:text-white">
|
|
||||||
{{ activeTactics }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -115,14 +81,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import type { OffensiveDecision } from '~/types/game'
|
import type { OffensiveDecision } from '~/types/game'
|
||||||
import ToggleSwitch from '~/components/UI/ToggleSwitch.vue'
|
|
||||||
import ActionButton from '~/components/UI/ActionButton.vue'
|
import ActionButton from '~/components/UI/ActionButton.vue'
|
||||||
|
|
||||||
interface ApproachOption {
|
interface ActionOption {
|
||||||
value: OffensiveDecision['approach']
|
value: OffensiveDecision['action']
|
||||||
label: string
|
label: string
|
||||||
icon: string
|
icon: string
|
||||||
description: string
|
description: string
|
||||||
|
disabled: boolean
|
||||||
|
disabledReason?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -130,11 +97,21 @@ interface Props {
|
|||||||
isActive: boolean
|
isActive: boolean
|
||||||
currentDecision?: Omit<OffensiveDecision, 'steal_attempts'>
|
currentDecision?: Omit<OffensiveDecision, 'steal_attempts'>
|
||||||
hasRunnersOnBase?: boolean
|
hasRunnersOnBase?: boolean
|
||||||
|
runnerOnFirst?: boolean
|
||||||
|
runnerOnSecond?: boolean
|
||||||
|
runnerOnThird?: boolean
|
||||||
|
basesLoaded?: boolean
|
||||||
|
outs?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
hasRunnersOnBase: false,
|
hasRunnersOnBase: false,
|
||||||
|
runnerOnFirst: false,
|
||||||
|
runnerOnSecond: false,
|
||||||
|
runnerOnThird: false,
|
||||||
|
basesLoaded: false,
|
||||||
|
outs: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -144,63 +121,90 @@ const emit = defineEmits<{
|
|||||||
// Local state
|
// Local state
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const localDecision = ref<Omit<OffensiveDecision, 'steal_attempts'>>({
|
const localDecision = ref<Omit<OffensiveDecision, 'steal_attempts'>>({
|
||||||
approach: props.currentDecision?.approach || 'normal',
|
action: props.currentDecision?.action || 'swing_away',
|
||||||
hit_and_run: props.currentDecision?.hit_and_run || false,
|
|
||||||
bunt_attempt: props.currentDecision?.bunt_attempt || false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Approach options
|
// Action options with smart filtering
|
||||||
const approachOptions: ApproachOption[] = [
|
const availableActions = computed<ActionOption[]>(() => {
|
||||||
{
|
const twoOuts = props.outs >= 2
|
||||||
value: 'normal',
|
|
||||||
label: 'Normal',
|
return [
|
||||||
icon: '⚾',
|
{
|
||||||
description: 'Balanced approach, no specific tendencies',
|
value: 'swing_away',
|
||||||
},
|
label: 'Swing Away',
|
||||||
{
|
icon: '⚾',
|
||||||
value: 'contact',
|
description: 'Normal swing, no special tactics',
|
||||||
label: 'Contact',
|
disabled: false,
|
||||||
icon: '🎯',
|
},
|
||||||
description: 'Focus on making contact, avoid strikeouts',
|
{
|
||||||
},
|
value: 'steal',
|
||||||
{
|
label: 'Steal',
|
||||||
value: 'power',
|
icon: '🏃',
|
||||||
label: 'Power',
|
description: 'Attempt to steal base(s) - configure on steal inputs tab',
|
||||||
icon: '💪',
|
disabled: false,
|
||||||
description: 'Swing for extra bases, higher strikeout risk',
|
},
|
||||||
},
|
{
|
||||||
{
|
value: 'check_jump',
|
||||||
value: 'patient',
|
label: 'Check Jump',
|
||||||
label: 'Patient',
|
icon: '👀',
|
||||||
icon: '🧘',
|
description: 'Lead runner checks jump at start of delivery',
|
||||||
description: 'Work the count, draw walks, wait for your pitch',
|
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
|
// Computed
|
||||||
const canUseHitAndRun = computed(() => {
|
const currentActionLabel = computed(() => {
|
||||||
return props.hasRunnersOnBase
|
const option = availableActions.value.find(opt => opt.value === localDecision.value.action)
|
||||||
|
return option?.label || 'Swing Away'
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentApproachLabel = computed(() => {
|
const actionRequiresSpecialHandling = computed(() => {
|
||||||
const option = approachOptions.find(opt => opt.value === localDecision.value.approach)
|
return localDecision.value.action === 'steal' || localDecision.value.action === 'squeeze_bunt'
|
||||||
return option?.label || 'Normal'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeTactics = computed(() => {
|
const specialHandlingNote = computed(() => {
|
||||||
const tactics: string[] = []
|
if (localDecision.value.action === 'steal') {
|
||||||
if (localDecision.value.hit_and_run) tactics.push('Hit & Run')
|
return 'Configure which bases to steal on the Stolen Base Inputs tab'
|
||||||
if (localDecision.value.bunt_attempt) tactics.push('Bunt')
|
}
|
||||||
return tactics.join(', ') || 'None'
|
if (localDecision.value.action === 'squeeze_bunt') {
|
||||||
|
return 'R3 will break for home as pitcher delivers - high risk, high reward!'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasChanges = computed(() => {
|
const hasChanges = computed(() => {
|
||||||
if (!props.currentDecision) return true
|
if (!props.currentDecision) return true
|
||||||
return (
|
return localDecision.value.action !== props.currentDecision.action
|
||||||
localDecision.value.approach !== props.currentDecision.approach ||
|
|
||||||
localDecision.value.hit_and_run !== props.currentDecision.hit_and_run ||
|
|
||||||
localDecision.value.bunt_attempt !== props.currentDecision.bunt_attempt
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitButtonText = computed(() => {
|
const submitButtonText = computed(() => {
|
||||||
@ -210,14 +214,23 @@ const submitButtonText = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const selectApproach = (approach: OffensiveDecision['approach']) => {
|
const selectAction = (action: OffensiveDecision['action']) => {
|
||||||
if (!props.isActive) return
|
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 getActionButtonClasses = (action: OffensiveDecision['action'], disabled: boolean) => {
|
||||||
const isSelected = localDecision.value.approach === approach
|
const isSelected = localDecision.value.action === action
|
||||||
const base = 'w-full p-4 rounded-lg border-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed'
|
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) {
|
if (isSelected) {
|
||||||
return `${base} bg-gradient-to-r from-blue-600 to-blue-700 text-white border-blue-700 shadow-lg`
|
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 })
|
}, { deep: true })
|
||||||
|
|
||||||
// Disable hit and run if no runners
|
// Auto-reset to swing_away if current action becomes invalid
|
||||||
watch(() => props.hasRunnersOnBase, (hasRunners) => {
|
watch(() => availableActions.value, (actions) => {
|
||||||
if (!hasRunners) {
|
const currentAction = localDecision.value.action
|
||||||
localDecision.value.hit_and_run = false
|
const currentOption = actions.find(opt => opt.value === currentAction)
|
||||||
|
|
||||||
|
if (currentOption?.disabled) {
|
||||||
|
localDecision.value.action = 'swing_away'
|
||||||
}
|
}
|
||||||
})
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -130,12 +130,12 @@ export interface DefensiveDecision {
|
|||||||
/**
|
/**
|
||||||
* Offensive strategic decision
|
* Offensive strategic decision
|
||||||
* Backend: OffensiveDecision
|
* Backend: OffensiveDecision
|
||||||
|
*
|
||||||
|
* Session 2 Update (2025-01-14): Replaced approach/hit_and_run/bunt_attempt with action field.
|
||||||
*/
|
*/
|
||||||
export interface OffensiveDecision {
|
export interface OffensiveDecision {
|
||||||
approach: 'normal' | 'contact' | 'power' | 'patient'
|
action: 'swing_away' | 'steal' | 'check_jump' | 'hit_and_run' | 'sac_bunt' | 'squeeze_bunt'
|
||||||
steal_attempts: number[] // Bases to steal (2, 3, or 4)
|
steal_attempts: number[] // Bases to steal (2, 3, or 4) - only used when action="steal"
|
||||||
hit_and_run: boolean
|
|
||||||
bunt_attempt: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -125,10 +125,8 @@ export interface DefensiveDecisionRequest {
|
|||||||
|
|
||||||
export interface OffensiveDecisionRequest {
|
export interface OffensiveDecisionRequest {
|
||||||
game_id: string
|
game_id: string
|
||||||
approach: OffensiveDecision['approach']
|
action: OffensiveDecision['action']
|
||||||
steal_attempts: number[]
|
steal_attempts: number[]
|
||||||
hit_and_run: boolean
|
|
||||||
bunt_attempt: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RollDiceRequest {
|
export interface RollDiceRequest {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user