strat-gameplay-webapp/frontend-sba/components/Gameplay/XCheckWizard.vue
Cal Corum f77666db87 CLAUDE: Add XCheckWizard component and result constants (step 6)
New files:
- constants/xCheckResults.ts - Labels, helpers for all result codes
- components/Gameplay/XCheckWizard.vue - Interactive x-check UI

XCheckWizard features:
 Displays d20 and 3d6 dice results prominently
 Shows 5-column chart row (Range 1-5) as selectable buttons
 Hash result sub-choices (G2#/G3# → pick G2 or SI2)
 SPD result sub-choice (click to reveal d20, pick safe/out)
 Error selection (NO/E1/E2/E3/RP) based on 3d6
 Submit validation (both result + error required)
 Read-only mode for transparency (opponent sees same UI)
 Mobile-responsive layout (stacks on small screens)
 Tailwind styling with clear visual hierarchy

Helper functions:
- getResultLabel() - Display names for all codes
- getErrorLabel() - Display names for error types
- isHashResult() - Detect G2#/G3#
- isSpdResult() - Detect SPD
- getHashConversions() - Get conversion options

Next: Integrate XCheckWizard into GameplayPanel

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-07 17:36:23 -06:00

507 lines
13 KiB
Vue

<template>
<div class="x-check-wizard" :class="{ 'read-only': readonly }">
<!-- Header -->
<div class="wizard-header">
<h3 class="wizard-title">
X-Check: {{ xCheckData.position }} ({{ getPositionName(xCheckData.position) }})
</h3>
<p v-if="readonly" class="waiting-message">
Waiting for opponent to select result...
</p>
</div>
<!-- Dice Display -->
<div class="dice-display">
<div class="dice-group">
<div class="dice-label">Range Roll (d20)</div>
<div class="dice-value d20">{{ xCheckData.d20_roll }}</div>
</div>
<div class="dice-group">
<div class="dice-label">Error Roll (3d6)</div>
<div class="dice-value d6">
{{ xCheckData.d6_total }}
<span class="dice-breakdown">({{ xCheckData.d6_individual.join(' + ') }})</span>
</div>
</div>
</div>
<!-- SPD d20 (click to reveal if present) -->
<div v-if="xCheckData.spd_d20 !== null" class="spd-container">
<button
v-if="!spdRevealed"
class="spd-reveal-button"
:disabled="readonly"
@click="spdRevealed = true"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Tap to Reveal SPD d20
</button>
<div v-else class="spd-revealed">
<div class="dice-label">Speed Check d20</div>
<div class="dice-value d20">{{ xCheckData.spd_d20 }}</div>
</div>
</div>
<!-- Defensive Positioning Note -->
<div v-if="defensivePositioningNote" class="positioning-note">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{{ defensivePositioningNote }}
</div>
<!-- Chart Row Selection -->
<div class="chart-selection">
<h4 class="selection-title">Select Range Result</h4>
<div class="chart-row">
<button
v-for="(resultCode, index) in xCheckData.chart_row"
:key="index"
class="chart-column"
:class="{
selected: selectedColumn === index && !showHashChoice && !showSpdChoice,
disabled: readonly,
}"
:disabled="readonly"
@click="selectColumn(index, resultCode)"
>
<div class="column-header">Range {{ index + 1 }}</div>
<div class="column-result">{{ resultCode }}</div>
<div class="column-label">{{ getResultLabel(resultCode) }}</div>
</button>
</div>
</div>
<!-- Hash Result Sub-Choice (G2#/G3# → G2 or SI2) -->
<div v-if="showHashChoice" class="sub-choice">
<h4 class="sub-choice-title">Speed Test Result</h4>
<p class="sub-choice-description">
Check batter's speed rating on their card. Did they pass the speed test?
</p>
<div class="sub-choice-buttons">
<button
v-for="option in hashOptions"
:key="option"
class="sub-choice-button"
:class="{ selected: selectedResult === option }"
:disabled="readonly"
@click="selectHashResult(option)"
>
<div class="option-code">{{ option }}</div>
<div class="option-label">{{ getResultLabel(option) }}</div>
</button>
</div>
</div>
<!-- SPD Result Sub-Choice -->
<div v-if="showSpdChoice" class="sub-choice">
<h4 class="sub-choice-title">Speed Check</h4>
<p class="sub-choice-description">
Check batter's speed rating vs. the d20 roll. Safe or out?
</p>
<div class="sub-choice-buttons">
<button
class="sub-choice-button"
:class="{ selected: selectedResult === 'G3' }"
:disabled="readonly"
@click="selectSpdResult('G3')"
>
<div class="option-code">G3</div>
<div class="option-label">Out (failed check)</div>
</button>
<button
class="sub-choice-button"
:class="{ selected: selectedResult === 'SI1' }"
:disabled="readonly"
@click="selectSpdResult('SI1')"
>
<div class="option-code">SI1</div>
<div class="option-label">Safe (passed check)</div>
</button>
</div>
</div>
<!-- Error Selection -->
<div class="error-selection">
<h4 class="selection-title">Select Error Result</h4>
<p class="selection-description">
Check defender's error rating on their card based on 3d6 total ({{ xCheckData.d6_total }})
</p>
<div class="error-row">
<button
v-for="errorCode in ERROR_OPTIONS"
:key="errorCode"
class="error-button"
:class="{
selected: selectedError === errorCode,
disabled: readonly,
'no-error': errorCode === 'NO',
}"
:disabled="readonly"
@click="selectError(errorCode)"
>
<div class="error-code">{{ errorCode }}</div>
<div class="error-label">{{ getErrorLabel(errorCode) }}</div>
</button>
</div>
</div>
<!-- Submit Button -->
<div class="wizard-actions">
<button
class="submit-button"
:disabled="!canSubmit || readonly"
@click="handleSubmit"
>
<svg v-if="!readonly" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{{ readonly ? 'Waiting...' : 'Submit Result' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { XCheckData } from '~/types/game'
import {
getResultLabel,
getErrorLabel,
isHashResult,
isSpdResult,
getHashConversions,
} from '~/constants/xCheckResults'
interface Props {
xCheckData: XCheckData
readonly: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
submit: [payload: { resultCode: string; errorResult: string }]
}>()
// State
const selectedColumn = ref<number | null>(null)
const selectedResult = ref<string | null>(null)
const selectedError = ref<string>('NO') // Default to no error
const showHashChoice = ref(false)
const showSpdChoice = ref(false)
const hashOptions = ref<string[]>([])
const spdRevealed = ref(false)
const ERROR_OPTIONS = ['NO', 'E1', 'E2', 'E3', 'RP']
// Position name mapping
const POSITION_NAMES: Record<string, string> = {
P: 'Pitcher',
C: 'Catcher',
'1B': 'First Base',
'2B': 'Second Base',
'3B': 'Third Base',
SS: 'Shortstop',
LF: 'Left Field',
CF: 'Center Field',
RF: 'Right Field',
}
function getPositionName(position: string): string {
return POSITION_NAMES[position] || position
}
// Defensive positioning note (if applicable)
const defensivePositioningNote = computed(() => {
// TODO: Get from game state - check if playing in affects range
// For now, just return null
return null
})
// Selection handlers
function selectColumn(index: number, resultCode: string) {
if (props.readonly) return
selectedColumn.value = index
// Check if this is a hash result (G2#/G3#)
if (isHashResult(resultCode)) {
const conversions = getHashConversions(resultCode)
if (conversions) {
showHashChoice.value = true
showSpdChoice.value = false
hashOptions.value = conversions
selectedResult.value = null // Reset until player picks
}
}
// Check if this is SPD
else if (isSpdResult(resultCode)) {
showSpdChoice.value = true
showHashChoice.value = false
selectedResult.value = null // Reset until player picks
}
// Simple result - select immediately
else {
showHashChoice.value = false
showSpdChoice.value = false
selectedResult.value = resultCode
}
}
function selectHashResult(option: string) {
if (props.readonly) return
selectedResult.value = option
showHashChoice.value = false
}
function selectSpdResult(option: string) {
if (props.readonly) return
selectedResult.value = option
showSpdChoice.value = false
}
function selectError(errorCode: string) {
if (props.readonly) return
selectedError.value = errorCode
}
// Can submit when both result and error are selected
const canSubmit = computed(() => {
return selectedResult.value !== null && selectedError.value !== null
})
function handleSubmit() {
if (!canSubmit.value || props.readonly) return
emit('submit', {
resultCode: selectedResult.value!,
errorResult: selectedError.value,
})
}
</script>
<style scoped>
.x-check-wizard {
@apply bg-white rounded-lg shadow-lg p-6 space-y-6 max-w-4xl mx-auto;
}
.x-check-wizard.read-only {
@apply opacity-75;
}
/* Header */
.wizard-header {
@apply text-center pb-4 border-b border-gray-200;
}
.wizard-title {
@apply text-2xl font-bold text-gray-900;
}
.waiting-message {
@apply mt-2 text-sm text-orange-600 font-medium;
}
/* Dice Display */
.dice-display {
@apply flex justify-center gap-8;
}
.dice-group {
@apply text-center;
}
.dice-label {
@apply text-sm font-medium text-gray-600 mb-2;
}
.dice-value {
@apply text-4xl font-bold rounded-lg px-6 py-3 shadow-md;
}
.dice-value.d20 {
@apply bg-blue-100 text-blue-900;
}
.dice-value.d6 {
@apply bg-green-100 text-green-900;
}
.dice-breakdown {
@apply text-lg text-gray-600 ml-2;
}
/* SPD Container */
.spd-container {
@apply text-center py-4 bg-yellow-50 rounded-lg border-2 border-yellow-200;
}
.spd-reveal-button {
@apply inline-flex items-center gap-2 px-6 py-3 bg-yellow-500 text-white font-semibold rounded-lg hover:bg-yellow-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
}
.spd-revealed {
@apply space-y-2;
}
/* Positioning Note */
.positioning-note {
@apply flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-800 rounded-lg text-sm;
}
/* Chart Selection */
.chart-selection {
@apply space-y-4;
}
.selection-title {
@apply text-lg font-semibold text-gray-900;
}
.selection-description {
@apply text-sm text-gray-600;
}
.chart-row {
@apply grid grid-cols-5 gap-3;
}
.chart-column {
@apply flex flex-col items-center p-4 border-2 border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all cursor-pointer disabled:cursor-not-allowed disabled:opacity-50;
}
.chart-column.selected {
@apply border-blue-600 bg-blue-100 ring-2 ring-blue-500;
}
.chart-column.disabled {
@apply hover:border-gray-300 hover:bg-white;
}
.column-header {
@apply text-xs font-medium text-gray-500 mb-1;
}
.column-result {
@apply text-2xl font-bold text-gray-900 mb-1;
}
.column-label {
@apply text-xs text-gray-600 text-center;
}
/* Sub-Choice */
.sub-choice {
@apply p-6 bg-gradient-to-br from-purple-50 to-blue-50 rounded-lg border-2 border-purple-300 space-y-4;
}
.sub-choice-title {
@apply text-lg font-bold text-purple-900;
}
.sub-choice-description {
@apply text-sm text-gray-700;
}
.sub-choice-buttons {
@apply flex gap-4 justify-center;
}
.sub-choice-button {
@apply flex flex-col items-center p-6 border-2 border-purple-300 rounded-lg hover:border-purple-600 hover:bg-purple-100 transition-all cursor-pointer min-w-[150px] disabled:cursor-not-allowed disabled:opacity-50;
}
.sub-choice-button.selected {
@apply border-purple-700 bg-purple-200 ring-2 ring-purple-600;
}
.option-code {
@apply text-3xl font-bold text-purple-900 mb-2;
}
.option-label {
@apply text-sm text-gray-700 text-center;
}
/* Error Selection */
.error-selection {
@apply space-y-4;
}
.error-row {
@apply flex gap-3 justify-center flex-wrap;
}
.error-button {
@apply flex flex-col items-center px-6 py-4 border-2 border-gray-300 rounded-lg hover:border-red-500 hover:bg-red-50 transition-all cursor-pointer min-w-[100px] disabled:cursor-not-allowed disabled:opacity-50;
}
.error-button.selected {
@apply border-red-600 bg-red-100 ring-2 ring-red-500;
}
.error-button.no-error {
@apply border-green-300 hover:border-green-500 hover:bg-green-50;
}
.error-button.no-error.selected {
@apply border-green-600 bg-green-100 ring-2 ring-green-500;
}
.error-code {
@apply text-xl font-bold text-gray-900 mb-1;
}
.error-label {
@apply text-xs text-gray-600 text-center;
}
/* Actions */
.wizard-actions {
@apply pt-4 border-t border-gray-200;
}
.submit-button {
@apply w-full inline-flex items-center justify-center gap-2 px-6 py-4 bg-blue-600 text-white font-bold text-lg rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.x-check-wizard {
@apply p-4 space-y-4;
}
.dice-display {
@apply flex-col gap-4;
}
.chart-row {
@apply grid-cols-2 gap-2;
}
.chart-column {
@apply p-3;
}
.column-result {
@apply text-xl;
}
.error-row {
@apply grid grid-cols-3 gap-2;
}
.error-button {
@apply px-4 py-3 min-w-0;
}
.sub-choice-buttons {
@apply flex-col;
}
}
</style>