diff --git a/frontend-sba/components/Decisions/DefensiveSetup.vue b/frontend-sba/components/Decisions/DefensiveSetup.vue index 178e669..0f51df0 100644 --- a/frontend-sba/components/Decisions/DefensiveSetup.vue +++ b/frontend-sba/components/Decisions/DefensiveSetup.vue @@ -22,7 +22,7 @@ Infield Depth - -
- -
- - - -
-
-

@@ -113,14 +86,14 @@ 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 ToggleSwitch from '~/components/UI/ToggleSwitch.vue' import ActionButton from '~/components/UI/ActionButton.vue' +import { useDefensiveSetup } from '~/composables/useDefensiveSetup' interface Props { gameId: string isActive: boolean currentSetup?: DefensiveDecision - gameState?: GameState // Added for smart filtering + gameState?: GameState } const props = withDefaults(defineProps(), { @@ -131,27 +104,10 @@ const emit = defineEmits<{ submit: [setup: DefensiveDecision] }>() +const { infieldDepth, outfieldDepth, holdRunnersArray, getDecision, syncFromDecision } = useDefensiveSetup() + // Local state const submitting = ref(false) -const localSetup = ref({ - infield_depth: props.currentSetup?.infield_depth || 'normal', - outfield_depth: props.currentSetup?.outfield_depth || 'normal', - hold_runners: props.currentSetup?.hold_runners || [], -}) - -// Hold runner toggles -const holdFirst = ref(localSetup.value.hold_runners.includes(1)) -const holdSecond = ref(localSetup.value.hold_runners.includes(2)) -const holdThird = ref(localSetup.value.hold_runners.includes(3)) - -// Watch hold toggles and update hold_runners array -watch([holdFirst, holdSecond, holdThird], () => { - const runners: number[] = [] - if (holdFirst.value) runners.push(1) - if (holdSecond.value) runners.push(2) - if (holdThird.value) runners.push(3) - localSetup.value.hold_runners = runners -}) // Dynamic options based on game state const infieldDepthOptions = computed(() => { @@ -194,18 +150,19 @@ const outfieldDepthOptions = computed(() => { // Display helpers const infieldDisplay = computed(() => { - const option = infieldDepthOptions.value.find(opt => opt.value === localSetup.value.infield_depth) + 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 === localSetup.value.outfield_depth) + const option = outfieldDepthOptions.value.find(opt => opt.value === outfieldDepth.value) return option?.label || 'Normal' }) const holdingDisplay = computed(() => { - if (localSetup.value.hold_runners.length === 0) return 'None' - return localSetup.value.hold_runners.map(base => { + 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' @@ -213,19 +170,8 @@ const holdingDisplay = computed(() => { }).join(', ') }) -// Check if setup has changed from initial (for display only) -const hasChanges = computed(() => { - if (!props.currentSetup) return true - return ( - localSetup.value.infield_depth !== props.currentSetup.infield_depth || - localSetup.value.outfield_depth !== props.currentSetup.outfield_depth || - JSON.stringify(localSetup.value.hold_runners) !== JSON.stringify(props.currentSetup.hold_runners) - ) -}) - const submitButtonText = computed(() => { if (!props.isActive) return 'Wait for Your Turn' - if (!hasChanges.value) return 'Submit (Keep Setup)' return 'Submit Defensive Setup' }) @@ -235,19 +181,16 @@ const handleSubmit = async () => { submitting.value = true try { - emit('submit', { ...localSetup.value }) + emit('submit', getDecision()) } finally { submitting.value = false } } -// Watch for prop changes and update local state +// Sync composable state from prop when it changes (e.g. server-confirmed state) watch(() => props.currentSetup, (newSetup) => { if (newSetup) { - localSetup.value = { ...newSetup } - holdFirst.value = newSetup.hold_runners.includes(1) - holdSecond.value = newSetup.hold_runners.includes(2) - holdThird.value = newSetup.hold_runners.includes(3) + syncFromDecision(newSetup) } }, { deep: true }) diff --git a/frontend-sba/components/Game/GamePlay.vue b/frontend-sba/components/Game/GamePlay.vue index d4938cf..e36226f 100644 --- a/frontend-sba/components/Game/GamePlay.vue +++ b/frontend-sba/components/Game/GamePlay.vue @@ -79,6 +79,9 @@ :fielding-team-color="fieldingTeamColor" :batting-team-abbrev="batterTeamAbbrev" :fielding-team-abbrev="pitcherTeamAbbrev" + :hold-runners="defensiveSetup.holdRunnersArray.value" + :hold-interactive="holdInteractive" + @toggle-hold="handleToggleHold" /> @@ -146,6 +149,9 @@ :fielding-team-color="fieldingTeamColor" :batting-team-abbrev="batterTeamAbbrev" :fielding-team-abbrev="pitcherTeamAbbrev" + :hold-runners="defensiveSetup.holdRunnersArray.value" + :hold-interactive="holdInteractive" + @toggle-hold="handleToggleHold" /> @@ -328,6 +334,7 @@ import { useAuthStore } from '~/store/auth' import { useUiStore } from '~/store/ui' import { useWebSocket } from '~/composables/useWebSocket' import { useGameActions } from '~/composables/useGameActions' +import { useDefensiveSetup } from '~/composables/useDefensiveSetup' import CurrentSituation from '~/components/Game/CurrentSituation.vue' import RunnersOnBase from '~/components/Game/RunnersOnBase.vue' import PlayByPlay from '~/components/Game/PlayByPlay.vue' @@ -363,6 +370,9 @@ const actions = useGameActions(props.gameId) // Destructure undoLastPlay for the undo button const { undoLastPlay } = actions +// Defensive setup composable (shared with DefensiveSetup.vue and RunnersOnBase) +const defensiveSetup = useDefensiveSetup() + // Game state from store const gameState = computed(() => { const state = gameStore.gameState @@ -531,6 +541,9 @@ const decisionPhase = computed(() => { return 'idle' }) +// Hold runner toggles are interactive only during defensive decision phase +const holdInteractive = computed(() => needsDefensiveDecision.value && isMyTurn.value) + // Phase F6: Conditional panel rendering const showDecisions = computed(() => { // Don't show decision panels if there's a result pending dismissal @@ -643,6 +656,10 @@ const handleStealAttemptsSubmit = (attempts: number[]) => { gameStore.setPendingStealAttempts(attempts) } +const handleToggleHold = (base: number) => { + defensiveSetup.toggleHold(base) +} + // Undo handler const handleUndoLastPlay = () => { console.log('[GamePlay] Undoing last play') @@ -715,6 +732,18 @@ watch(gameState, (state, oldState) => { } }, { immediate: true }) +// Reset defensive setup composable when entering a new defensive decision phase +watch(needsDefensiveDecision, (needs) => { + if (needs) { + // Sync from existing setup if available, otherwise reset to defaults + if (pendingDefensiveSetup.value) { + defensiveSetup.syncFromDecision(pendingDefensiveSetup.value) + } else { + defensiveSetup.reset() + } + } +}) + // Quality of Life: Auto-submit default decisions when bases are empty watch([needsDefensiveDecision, needsOffensiveDecision, basesEmpty], ([defensive, offensive, empty]) => { // Only auto-submit if it's the player's turn and bases are empty diff --git a/frontend-sba/components/Game/RunnerCard.vue b/frontend-sba/components/Game/RunnerCard.vue index 39c08b6..85d0b44 100644 --- a/frontend-sba/components/Game/RunnerCard.vue +++ b/frontend-sba/components/Game/RunnerCard.vue @@ -3,7 +3,8 @@ :class="[ 'runner-pill', runner ? 'occupied' : 'empty', - isSelected ? 'selected' : '' + isSelected ? 'selected' : '', + isHeld ? 'held' : '' ]" @click="handleClick" > @@ -24,6 +25,30 @@
{{ runnerName }}
{{ base }}

+ + +