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>
507 lines
13 KiB
Vue
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>
|