Backend: - Add home_team_dice_color and away_team_dice_color to GameState model - Extract dice_color from game metadata in StateManager (default: cc0000) - Add runners_on_base param to roll_ab for chaos check skipping Frontend - Dice Display: - Create DiceShapes.vue with SVG d6 (square) and d20 (hexagon) shapes - Apply home team's dice_color to d6 dice, white for resolution d20 - Show chaos d20 in amber only when WP/PB check triggered - Add automatic text contrast based on color luminance - Reduce blank space and remove info bubble from dice results Frontend - Player Cards: - Consolidate pitcher/batter cards to single location below diamond - Add active card highlighting based on dice roll (d6_one: 1-3=batter, 4-6=pitcher) - New card header format: [Team] Position [Name] with full card image - Remove redundant card displays from GameBoard and GameplayPanel - Enlarge PlayerCardModal on desktop (max-w-3xl at 1024px+) Tests: - Add DiceShapes.spec.ts with 34 tests for color calculations and rendering - Update DiceRoller.spec.ts for new DiceShapes integration - Fix test_roll_dice_success for new runners_on_base parameter Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
252 lines
7.0 KiB
Vue
252 lines
7.0 KiB
Vue
<template>
|
|
<div class="dice-roller">
|
|
<!-- Roll Button (shown when no roll exists) -->
|
|
<div v-if="!pendingRoll" class="flex justify-center">
|
|
<button
|
|
:disabled="!canRoll || isRolling"
|
|
:class="[
|
|
'roll-button',
|
|
canRoll && !isRolling ? 'roll-button-enabled' : 'roll-button-disabled'
|
|
]"
|
|
@click="handleRoll"
|
|
>
|
|
<span v-if="isRolling" class="flex items-center gap-2">
|
|
<svg class="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" />
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
Rolling...
|
|
</span>
|
|
<span v-else class="flex items-center gap-2">
|
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M5 3a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2H5zm7 4a1.5 1.5 0 110 3 1.5 1.5 0 010-3z"/>
|
|
</svg>
|
|
Roll Dice
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Dice Results (shown after roll) -->
|
|
<div v-else class="dice-results">
|
|
<div class="dice-header">
|
|
<h3 class="text-lg font-bold text-white">Dice Results</h3>
|
|
<div class="text-sm text-blue-200">
|
|
{{ formatTimestamp(pendingRoll.timestamp) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dice Display Grid -->
|
|
<div class="dice-display" :class="{ 'dice-display-compact': !showChaosD20 }">
|
|
<!-- d6 One -->
|
|
<DiceShapes
|
|
type="d6"
|
|
:value="pendingRoll.d6_one"
|
|
:color="effectiveDiceColor"
|
|
:size="dieSize"
|
|
label="d6 (One)"
|
|
/>
|
|
|
|
<!-- d6 Two (showing total) -->
|
|
<DiceShapes
|
|
type="d6"
|
|
:value="pendingRoll.d6_two_total"
|
|
:color="effectiveDiceColor"
|
|
:size="dieSize"
|
|
label="d6 (Two)"
|
|
:sublabel="`(${pendingRoll.d6_two_a} + ${pendingRoll.d6_two_b})`"
|
|
/>
|
|
|
|
<!-- Chaos d20 - only shown when WP/PB check triggered -->
|
|
<DiceShapes
|
|
v-if="showChaosD20"
|
|
type="d20"
|
|
:value="pendingRoll.chaos_d20"
|
|
color="f59e0b"
|
|
:size="dieSize"
|
|
label="Chaos d20"
|
|
class="dice-chaos"
|
|
/>
|
|
|
|
<!-- Resolution d20 -->
|
|
<DiceShapes
|
|
type="d20"
|
|
:value="pendingRoll.resolution_d20"
|
|
color="ffffff"
|
|
:size="dieSize"
|
|
label="Resolution d20"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Special Event Indicators -->
|
|
<div v-if="pendingRoll.check_wild_pitch || pendingRoll.check_passed_ball" class="special-events">
|
|
<div v-if="pendingRoll.check_wild_pitch" class="special-event wild-pitch">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
|
</svg>
|
|
Wild Pitch Check
|
|
</div>
|
|
<div v-if="pendingRoll.check_passed_ball" class="special-event passed-ball">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
|
</svg>
|
|
Passed Ball Check
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import type { RollData } from '~/types'
|
|
import DiceShapes from './DiceShapes.vue'
|
|
|
|
interface Props {
|
|
canRoll: boolean
|
|
pendingRoll: RollData | null
|
|
diceColor?: string // Home team's dice_color (hex without #), default 'cc0000'
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
diceColor: 'cc0000', // Default red
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
roll: []
|
|
}>()
|
|
|
|
// Local state
|
|
const isRolling = ref(false)
|
|
|
|
// Die size - responsive
|
|
const dieSize = 90
|
|
|
|
// Effective dice color (use prop or default)
|
|
const effectiveDiceColor = computed(() => props.diceColor || 'cc0000')
|
|
|
|
// Computed: Only show chaos d20 when WP/PB check triggered (chaos_d20 == 1 or 2)
|
|
// Hide when: bases were empty OR chaos_d20 >= 3 (no effect)
|
|
const showChaosD20 = computed(() => {
|
|
if (!props.pendingRoll) return false
|
|
// Show chaos d20 only if a WP or PB check was triggered
|
|
return props.pendingRoll.check_wild_pitch || props.pendingRoll.check_passed_ball
|
|
})
|
|
|
|
// Methods
|
|
const handleRoll = () => {
|
|
if (!props.canRoll || isRolling.value) return
|
|
|
|
isRolling.value = true
|
|
emit('roll')
|
|
|
|
// Reset rolling state after animation (will be replaced by actual server response)
|
|
setTimeout(() => {
|
|
isRolling.value = false
|
|
}, 2000)
|
|
}
|
|
|
|
const formatTimestamp = (timestamp: string): string => {
|
|
const date = new Date(timestamp)
|
|
return date.toLocaleTimeString('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.dice-roller {
|
|
@apply w-full;
|
|
}
|
|
|
|
/* Roll Button */
|
|
.roll-button {
|
|
@apply px-8 py-4 rounded-lg font-bold text-lg transition-all duration-200;
|
|
@apply shadow-lg min-h-[60px] min-w-[200px];
|
|
}
|
|
|
|
.roll-button-enabled {
|
|
@apply bg-gradient-to-r from-green-500 to-green-600 text-white;
|
|
@apply hover:from-green-600 hover:to-green-700 hover:shadow-xl;
|
|
@apply active:scale-95;
|
|
}
|
|
|
|
.roll-button-disabled {
|
|
@apply bg-gray-300 text-gray-500 cursor-not-allowed;
|
|
}
|
|
|
|
/* Dice Results Container */
|
|
.dice-results {
|
|
@apply bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl p-4 shadow-xl;
|
|
@apply space-y-3;
|
|
animation: slideDown 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* Header */
|
|
.dice-header {
|
|
@apply flex justify-between items-center pb-3 border-b border-slate-700;
|
|
}
|
|
|
|
/* Dice Display Grid */
|
|
.dice-display {
|
|
@apply flex justify-center items-start gap-6 pt-2 pb-4;
|
|
@apply flex-wrap;
|
|
}
|
|
|
|
/* When only 3 dice shown, they center nicely */
|
|
.dice-display-compact {
|
|
@apply gap-8;
|
|
}
|
|
|
|
/* Chaos d20 styling */
|
|
.dice-chaos {
|
|
animation: pulseGlow 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulseGlow {
|
|
0%, 100% {
|
|
filter: drop-shadow(0 0 8px rgba(245, 158, 11, 0.4));
|
|
}
|
|
50% {
|
|
filter: drop-shadow(0 0 16px rgba(245, 158, 11, 0.7));
|
|
}
|
|
}
|
|
|
|
/* Special Events */
|
|
.special-events {
|
|
@apply flex flex-wrap justify-center gap-3;
|
|
}
|
|
|
|
.special-event {
|
|
@apply flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold;
|
|
@apply shadow-md;
|
|
}
|
|
|
|
.wild-pitch {
|
|
@apply bg-yellow-500 text-yellow-900;
|
|
}
|
|
|
|
.passed-ball {
|
|
@apply bg-orange-500 text-orange-900;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 480px) {
|
|
.dice-display {
|
|
@apply gap-4;
|
|
}
|
|
}
|
|
</style>
|