feat: Uncapped hit decision tree, x-check workflow, baserunner UI #8
@ -78,6 +78,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- State: X-Check Result Pending -->
|
||||
<div v-else-if="workflowState === 'x_check_result_pending'" class="state-x-check">
|
||||
<div v-if="!isXCheckInteractive" class="state-message">
|
||||
<svg class="w-12 h-12 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<div class="message-text">
|
||||
Waiting for defense to select x-check result...
|
||||
</div>
|
||||
</div>
|
||||
<XCheckWizard
|
||||
v-else-if="xCheckData"
|
||||
:x-check-data="xCheckData"
|
||||
:readonly="!isXCheckInteractive"
|
||||
@submit="handleXCheckSubmit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- State: Result (Play completed) -->
|
||||
<div v-else-if="workflowState === 'result'" class="state-result">
|
||||
<PlayResultDisplay
|
||||
@ -101,10 +119,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { RollData, PlayResult, PlayOutcome } from '~/types'
|
||||
import type { RollData, PlayResult, PlayOutcome, XCheckData } from '~/types'
|
||||
import { useGameStore } from '~/store/game'
|
||||
import DiceRoller from './DiceRoller.vue'
|
||||
import OutcomeWizard from './OutcomeWizard.vue'
|
||||
import PlayResultDisplay from './PlayResult.vue'
|
||||
import XCheckWizard from './XCheckWizard.vue'
|
||||
|
||||
interface Props {
|
||||
gameId: string
|
||||
@ -117,26 +137,44 @@ interface Props {
|
||||
hasRunners?: boolean
|
||||
// Dice color from home team (hex without #)
|
||||
diceColor?: string
|
||||
// User's team ID (for determining interactive mode in x-check)
|
||||
userTeamId?: number | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
outs: 0,
|
||||
hasRunners: false,
|
||||
diceColor: 'cc0000', // Default red
|
||||
userTeamId: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
rollDice: []
|
||||
submitOutcome: [{ outcome: PlayOutcome; hitLocation?: string }]
|
||||
dismissResult: []
|
||||
submitXCheckResult: [{ resultCode: string; errorResult: string }]
|
||||
}>()
|
||||
|
||||
// Store access
|
||||
const gameStore = useGameStore()
|
||||
|
||||
// Local state
|
||||
const error = ref<string | null>(null)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
// X-Check data from store
|
||||
const xCheckData = computed(() => gameStore.xCheckData)
|
||||
|
||||
// Determine if current user should have interactive mode
|
||||
// Uses active_team_id from x-check data (set by backend to indicate which team should interact)
|
||||
const isXCheckInteractive = computed(() => {
|
||||
if (!xCheckData.value || !props.userTeamId) return false
|
||||
// Backend sets active_team_id to indicate which team should have interactive controls
|
||||
return xCheckData.value.active_team_id === props.userTeamId
|
||||
})
|
||||
|
||||
// Workflow state computation
|
||||
type WorkflowState = 'idle' | 'ready_to_roll' | 'rolled' | 'submitted' | 'result'
|
||||
type WorkflowState = 'idle' | 'ready_to_roll' | 'rolled' | 'submitted' | 'result' | 'x_check_result_pending'
|
||||
|
||||
const workflowState = computed<WorkflowState>(() => {
|
||||
// Show result if we have one
|
||||
@ -144,6 +182,11 @@ const workflowState = computed<WorkflowState>(() => {
|
||||
return 'result'
|
||||
}
|
||||
|
||||
// Show x-check result selection if awaiting
|
||||
if (gameStore.needsXCheckResult && xCheckData.value) {
|
||||
return 'x_check_result_pending'
|
||||
}
|
||||
|
||||
// Show submitted/processing state
|
||||
if (isSubmitting.value) {
|
||||
return 'submitted'
|
||||
@ -167,6 +210,7 @@ const workflowState = computed<WorkflowState>(() => {
|
||||
const statusClass = computed(() => {
|
||||
if (error.value) return 'status-error'
|
||||
if (workflowState.value === 'result') return 'status-success'
|
||||
if (workflowState.value === 'x_check_result_pending') return 'status-active'
|
||||
if (workflowState.value === 'submitted') return 'status-processing'
|
||||
if (workflowState.value === 'rolled') return 'status-active'
|
||||
if (workflowState.value === 'ready_to_roll' && props.isMyTurn) return 'status-active'
|
||||
@ -176,6 +220,9 @@ const statusClass = computed(() => {
|
||||
const statusText = computed(() => {
|
||||
if (error.value) return 'Error'
|
||||
if (workflowState.value === 'result') return 'Play Complete'
|
||||
if (workflowState.value === 'x_check_result_pending') {
|
||||
return isXCheckInteractive.value ? 'Select X-Check Result' : 'Waiting for Defense'
|
||||
}
|
||||
if (workflowState.value === 'submitted') return 'Processing'
|
||||
if (workflowState.value === 'rolled') return 'Enter Outcome'
|
||||
if (workflowState.value === 'ready_to_roll') {
|
||||
@ -210,6 +257,17 @@ const handleDismissResult = () => {
|
||||
error.value = null
|
||||
emit('dismissResult')
|
||||
}
|
||||
|
||||
const handleXCheckSubmit = (payload: { resultCode: string; errorResult: string }) => {
|
||||
error.value = null
|
||||
isSubmitting.value = true
|
||||
emit('submitXCheckResult', payload)
|
||||
|
||||
// Reset submitting state after a delay
|
||||
setTimeout(() => {
|
||||
isSubmitting.value = false
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -300,6 +358,11 @@ const handleDismissResult = () => {
|
||||
@apply space-y-6;
|
||||
}
|
||||
|
||||
/* State: X-Check */
|
||||
.state-x-check {
|
||||
@apply space-y-4;
|
||||
}
|
||||
|
||||
/* State: Result */
|
||||
.state-result {
|
||||
@apply space-y-4;
|
||||
|
||||
@ -174,6 +174,75 @@ export function useGameActions(gameId?: string) {
|
||||
uiStore.showInfo('Submitting outcome...', 2000)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// X-Check Interactive Workflow
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Submit x-check result selection (result code + error)
|
||||
*/
|
||||
function submitXCheckResult(resultCode: string, errorResult: string) {
|
||||
if (!validateConnection()) return
|
||||
|
||||
console.log('[GameActions] Submitting x-check result:', resultCode, errorResult)
|
||||
|
||||
socket.value!.emit('submit_x_check_result', {
|
||||
game_id: currentGameId.value!,
|
||||
result_code: resultCode,
|
||||
error_result: errorResult,
|
||||
})
|
||||
|
||||
uiStore.showInfo('Submitting x-check result...', 2000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit DECIDE advance decision (offensive player)
|
||||
*/
|
||||
function submitDecideAdvance(advance: boolean) {
|
||||
if (!validateConnection()) return
|
||||
|
||||
console.log('[GameActions] Submitting DECIDE advance:', advance)
|
||||
|
||||
socket.value!.emit('submit_decide_advance', {
|
||||
game_id: currentGameId.value!,
|
||||
advance,
|
||||
})
|
||||
|
||||
uiStore.showInfo('Submitting advance decision...', 2000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit DECIDE throw target (defensive player)
|
||||
*/
|
||||
function submitDecideThrow(target: 'runner' | 'first') {
|
||||
if (!validateConnection()) return
|
||||
|
||||
console.log('[GameActions] Submitting DECIDE throw:', target)
|
||||
|
||||
socket.value!.emit('submit_decide_throw', {
|
||||
game_id: currentGameId.value!,
|
||||
target,
|
||||
})
|
||||
|
||||
uiStore.showInfo('Submitting throw decision...', 2000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit DECIDE speed check result (offensive player)
|
||||
*/
|
||||
function submitDecideResult(outcome: 'safe' | 'out') {
|
||||
if (!validateConnection()) return
|
||||
|
||||
console.log('[GameActions] Submitting DECIDE result:', outcome)
|
||||
|
||||
socket.value!.emit('submit_decide_result', {
|
||||
game_id: currentGameId.value!,
|
||||
outcome,
|
||||
})
|
||||
|
||||
uiStore.showInfo('Submitting speed check result...', 2000)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Substitution Actions
|
||||
// ============================================================================
|
||||
@ -374,6 +443,12 @@ export function useGameActions(gameId?: string) {
|
||||
rollDice,
|
||||
submitManualOutcome,
|
||||
|
||||
// X-Check interactive workflow
|
||||
submitXCheckResult,
|
||||
submitDecideAdvance,
|
||||
submitDecideThrow,
|
||||
submitDecideResult,
|
||||
|
||||
// Substitutions
|
||||
submitSubstitution,
|
||||
|
||||
|
||||
@ -497,8 +497,23 @@ export function useWebSocket() {
|
||||
// ========================================
|
||||
|
||||
state.socketInstance.on('decision_required', (prompt) => {
|
||||
console.log('[WebSocket] Decision required:', prompt.phase)
|
||||
console.log('[WebSocket] Decision required:', prompt.phase, 'type:', prompt.type)
|
||||
gameStore.setDecisionPrompt(prompt)
|
||||
|
||||
// Handle x-check specific decision types
|
||||
if (prompt.type === 'x_check_result' && prompt.data) {
|
||||
console.log('[WebSocket] X-Check result decision, position:', prompt.data.position)
|
||||
gameStore.setXCheckData(prompt.data)
|
||||
} else if (prompt.type === 'decide_advance' && prompt.data) {
|
||||
console.log('[WebSocket] DECIDE advance decision')
|
||||
gameStore.setDecideData(prompt.data)
|
||||
} else if (prompt.type === 'decide_throw' && prompt.data) {
|
||||
console.log('[WebSocket] DECIDE throw decision')
|
||||
gameStore.setDecideData(prompt.data)
|
||||
} else if (prompt.type === 'decide_speed_check' && prompt.data) {
|
||||
console.log('[WebSocket] DECIDE speed check decision')
|
||||
gameStore.setDecideData(prompt.data)
|
||||
}
|
||||
})
|
||||
|
||||
state.socketInstance.on('defensive_decision_submitted', (data) => {
|
||||
@ -587,6 +602,8 @@ export function useWebSocket() {
|
||||
|
||||
// Clear pending decisions since the play is complete and we'll need new ones for next batter
|
||||
gameStore.clearPendingDecisions()
|
||||
gameStore.clearXCheckData()
|
||||
gameStore.clearDecideData()
|
||||
|
||||
uiStore.showSuccess(data.description, 5000)
|
||||
})
|
||||
|
||||
@ -16,6 +16,10 @@ import type {
|
||||
RollData,
|
||||
Lineup,
|
||||
BenchPlayer,
|
||||
XCheckData,
|
||||
DecideAdvanceData,
|
||||
DecideThrowData,
|
||||
DecideSpeedCheckData,
|
||||
} from '~/types'
|
||||
|
||||
export const useGameStore = defineStore('game', () => {
|
||||
@ -36,6 +40,10 @@ export const useGameStore = defineStore('game', () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// X-Check workflow state
|
||||
const xCheckData = ref<XCheckData | null>(null)
|
||||
const decideData = ref<DecideAdvanceData | DecideThrowData | DecideSpeedCheckData | null>(null)
|
||||
|
||||
// Decision state (local pending decisions before submission)
|
||||
const pendingDefensiveSetup = ref<DefensiveDecision | null>(null)
|
||||
const pendingOffensiveDecision = ref<Omit<OffensiveDecision, 'steal_attempts'> | null>(null)
|
||||
@ -128,6 +136,26 @@ export const useGameStore = defineStore('game', () => {
|
||||
gameState.value?.decision_phase === 'awaiting_stolen_base'
|
||||
})
|
||||
|
||||
const needsXCheckResult = computed(() => {
|
||||
return currentDecisionPrompt.value?.phase === 'awaiting_x_check_result' ||
|
||||
gameState.value?.decision_phase === 'awaiting_x_check_result'
|
||||
})
|
||||
|
||||
const needsDecideAdvance = computed(() => {
|
||||
return currentDecisionPrompt.value?.phase === 'awaiting_decide_advance' ||
|
||||
gameState.value?.decision_phase === 'awaiting_decide_advance'
|
||||
})
|
||||
|
||||
const needsDecideThrow = computed(() => {
|
||||
return currentDecisionPrompt.value?.phase === 'awaiting_decide_throw' ||
|
||||
gameState.value?.decision_phase === 'awaiting_decide_throw'
|
||||
})
|
||||
|
||||
const needsDecideResult = computed(() => {
|
||||
return currentDecisionPrompt.value?.phase === 'awaiting_decide_result' ||
|
||||
gameState.value?.decision_phase === 'awaiting_decide_result'
|
||||
})
|
||||
|
||||
const canRollDice = computed(() => {
|
||||
return gameState.value?.decision_phase === 'resolution' && !pendingRoll.value
|
||||
})
|
||||
@ -332,6 +360,34 @@ export const useGameStore = defineStore('game', () => {
|
||||
pendingStealAttempts.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Set x-check data (from decision_required event)
|
||||
*/
|
||||
function setXCheckData(data: XCheckData | null) {
|
||||
xCheckData.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear x-check data after resolution
|
||||
*/
|
||||
function clearXCheckData() {
|
||||
xCheckData.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Set DECIDE data (from decision_required event)
|
||||
*/
|
||||
function setDecideData(data: DecideAdvanceData | DecideThrowData | DecideSpeedCheckData | null) {
|
||||
decideData.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear DECIDE data after resolution
|
||||
*/
|
||||
function clearDecideData() {
|
||||
decideData.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset game store (when leaving game)
|
||||
*/
|
||||
@ -351,6 +407,8 @@ export const useGameStore = defineStore('game', () => {
|
||||
pendingOffensiveDecision.value = null
|
||||
pendingStealAttempts.value = []
|
||||
decisionHistory.value = []
|
||||
xCheckData.value = null
|
||||
decideData.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -404,6 +462,8 @@ export const useGameStore = defineStore('game', () => {
|
||||
pendingOffensiveDecision: readonly(pendingOffensiveDecision),
|
||||
pendingStealAttempts: readonly(pendingStealAttempts),
|
||||
decisionHistory: readonly(decisionHistory),
|
||||
xCheckData: readonly(xCheckData),
|
||||
decideData: readonly(decideData),
|
||||
|
||||
// Getters
|
||||
gameId,
|
||||
@ -432,6 +492,10 @@ export const useGameStore = defineStore('game', () => {
|
||||
needsDefensiveDecision,
|
||||
needsOffensiveDecision,
|
||||
needsStolenBaseDecision,
|
||||
needsXCheckResult,
|
||||
needsDecideAdvance,
|
||||
needsDecideThrow,
|
||||
needsDecideResult,
|
||||
canRollDice,
|
||||
canSubmitOutcome,
|
||||
recentPlays,
|
||||
@ -458,6 +522,10 @@ export const useGameStore = defineStore('game', () => {
|
||||
setPendingStealAttempts,
|
||||
addDecisionToHistory,
|
||||
clearPendingDecisions,
|
||||
setXCheckData,
|
||||
clearXCheckData,
|
||||
setDecideData,
|
||||
clearDecideData,
|
||||
resetGame,
|
||||
getActiveLineup,
|
||||
getBenchPlayers,
|
||||
|
||||
@ -376,8 +376,8 @@ export interface XCheckData {
|
||||
position: string
|
||||
d20_roll: number
|
||||
d6_total: number
|
||||
d6_individual: number[]
|
||||
chart_row: string[] // 5 values: ["G1", "G2", "G3#", "SI1", "SI2"]
|
||||
d6_individual: readonly number[] | number[]
|
||||
chart_row: readonly string[] | string[] // 5 values: ["G1", "G2", "G3#", "SI1", "SI2"]
|
||||
chart_type: 'infield' | 'outfield' | 'catcher'
|
||||
spd_d20: number | null // Pre-rolled, shown on click-to-reveal
|
||||
defender_lineup_id: number
|
||||
|
||||
@ -30,6 +30,12 @@ export type {
|
||||
GameListItem,
|
||||
CreateGameRequest,
|
||||
CreateGameResponse,
|
||||
// X-Check workflow types
|
||||
XCheckData,
|
||||
DecideAdvanceData,
|
||||
DecideThrowData,
|
||||
DecideSpeedCheckData,
|
||||
PendingXCheck,
|
||||
} from './game'
|
||||
|
||||
// Player types
|
||||
@ -65,6 +71,11 @@ export type {
|
||||
GetLineupRequest,
|
||||
GetBoxScoreRequest,
|
||||
RequestGameStateRequest,
|
||||
// X-Check workflow request types
|
||||
SubmitXCheckResultRequest,
|
||||
SubmitDecideAdvanceRequest,
|
||||
SubmitDecideThrowRequest,
|
||||
SubmitDecideResultRequest,
|
||||
// Event types
|
||||
ConnectedEvent,
|
||||
GameJoinedEvent,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user