strat-gameplay-webapp/frontend-sba/components/Gameplay/DiceRoller.vue
Cal Corum eab61ad966 CLAUDE: Phases 3.5, F1-F5 Complete - Statistics & Frontend Components
This commit captures work from multiple sessions building the statistics
system and frontend component library.

Backend - Phase 3.5: Statistics System
- Box score statistics with materialized views
- Play stat calculator for real-time updates
- Stat view refresher service
- Alembic migration for materialized views
- Test coverage: 41 new tests (all passing)

Frontend - Phase F1: Foundation
- Composables: useGameState, useGameActions, useWebSocket
- Type definitions and interfaces
- Store setup with Pinia

Frontend - Phase F2: Game Display
- ScoreBoard, GameBoard, CurrentSituation, PlayByPlay components
- Demo page at /demo

Frontend - Phase F3: Decision Inputs
- DefensiveSetup, OffensiveApproach, StolenBaseInputs components
- DecisionPanel orchestration
- Demo page at /demo-decisions
- Test coverage: 213 tests passing

Frontend - Phase F4: Dice & Manual Outcome
- DiceRoller component
- ManualOutcomeEntry with validation
- PlayResult display
- GameplayPanel orchestration
- Demo page at /demo-gameplay
- Test coverage: 119 tests passing

Frontend - Phase F5: Substitutions
- PinchHitterSelector, DefensiveReplacementSelector, PitchingChangeSelector
- SubstitutionPanel with tab navigation
- Demo page at /demo-substitutions
- Test coverage: 114 tests passing

Documentation:
- PHASE_3_5_HANDOFF.md - Statistics system handoff
- PHASE_F2_COMPLETE.md - Game display completion
- Frontend phase planning docs
- NEXT_SESSION.md updated for Phase F6

Configuration:
- Package updates (Nuxt 4 fixes)
- Tailwind config enhancements
- Game store updates

Test Status:
- Backend: 731/731 passing (100%)
- Frontend: 446/446 passing (100%)
- Total: 1,177 tests passing

Next Phase: F6 - Integration (wire all components into game page)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 09:52:30 -06:00

272 lines
7.3 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>
<!-- Main Dice Grid -->
<div class="dice-grid">
<!-- d6 One -->
<div class="dice-item dice-d6">
<div class="dice-label">d6 (One)</div>
<div class="dice-value">{{ pendingRoll.d6_one }}</div>
</div>
<!-- d6 Two (showing total) -->
<div class="dice-item dice-d6">
<div class="dice-label">d6 (Two)</div>
<div class="dice-value">{{ pendingRoll.d6_two_total }}</div>
<div class="dice-sublabel">
({{ pendingRoll.d6_two_a }} + {{ pendingRoll.d6_two_b }})
</div>
</div>
<!-- Chaos d20 -->
<div class="dice-item dice-d20">
<div class="dice-label">Chaos d20</div>
<div class="dice-value dice-value-large">{{ pendingRoll.chaos_d20 }}</div>
</div>
<!-- Resolution d20 -->
<div class="dice-item dice-d20">
<div class="dice-label">Resolution d20</div>
<div class="dice-value dice-value-large">{{ pendingRoll.resolution_d20 }}</div>
</div>
</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>
<!-- Card Reading Instructions -->
<div class="card-instructions">
<div class="instruction-icon">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
</div>
<div class="instruction-text">
Use these dice results to read the outcome from your player's card
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { RollData } from '~/types'
interface Props {
canRoll: boolean
pendingRoll: RollData | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
roll: []
}>()
// Local state
const isRolling = ref(false)
// 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-blue-600 to-blue-700 rounded-xl p-6 shadow-xl;
@apply space-y-4;
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-blue-500;
}
/* Dice Grid */
.dice-grid {
@apply grid grid-cols-2 gap-4;
}
@media (min-width: 640px) {
.dice-grid {
@apply grid-cols-4;
}
}
/* Individual Dice Items */
.dice-item {
@apply bg-white rounded-lg p-4 text-center shadow-md;
@apply flex flex-col items-center justify-center;
@apply min-h-[100px];
}
.dice-d6 {
@apply bg-gradient-to-br from-gray-50 to-gray-100;
}
.dice-d20 {
@apply bg-gradient-to-br from-yellow-50 to-yellow-100;
}
.dice-label {
@apply text-xs font-semibold text-gray-600 uppercase tracking-wide mb-2;
}
.dice-value {
@apply text-3xl font-bold text-gray-900;
}
.dice-value-large {
@apply text-4xl;
}
.dice-sublabel {
@apply text-xs text-gray-500 mt-1;
}
/* Special Events */
.special-events {
@apply flex flex-wrap gap-2 pt-2;
}
.special-event {
@apply flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-semibold;
}
.wild-pitch {
@apply bg-yellow-400 text-yellow-900;
}
.passed-ball {
@apply bg-orange-400 text-orange-900;
}
/* Card Instructions */
.card-instructions {
@apply bg-blue-500 bg-opacity-50 rounded-lg p-4;
@apply flex items-start gap-3;
}
.instruction-icon {
@apply text-blue-200 flex-shrink-0;
}
.instruction-text {
@apply text-white text-sm font-medium;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.dice-results {
@apply from-blue-800 to-blue-900;
}
.dice-item {
@apply shadow-lg;
}
.card-instructions {
@apply bg-blue-700 bg-opacity-50;
}
}
</style>