Add comprehensive project documentation and Docker infrastructure for Paper Dynasty Real-Time Game Engine - a web-based multiplayer baseball simulation platform replacing the legacy Google Sheets system. Documentation Added: - Complete PRD (Product Requirements Document) - Project README with dual development workflows - Implementation guide with 5-phase roadmap - Architecture docs (backend, frontend, database, WebSocket) - CLAUDE.md context files for each major directory Infrastructure Added: - Root docker-compose.yml for full stack orchestration - Dockerfiles for backend and both frontends (multi-stage builds) - .dockerignore files for optimal build context - .env.example with all required configuration - Updated .gitignore for Python, Node, Nuxt, and Docker Project Structure: - backend/ - FastAPI + Socket.io game engine (Python 3.11+) - frontend-sba/ - SBA League Nuxt 3 frontend - frontend-pd/ - PD League Nuxt 3 frontend - .claude/implementation/ - Detailed implementation guides Supports two development workflows: 1. Local dev (recommended): Services run natively with hot-reload 2. Full Docker: One-command stack orchestration for testing/demos Next: Phase 1 implementation (backend/frontend foundations)
779 lines
19 KiB
Markdown
779 lines
19 KiB
Markdown
# Frontend Architecture
|
|
|
|
## Overview
|
|
|
|
Two separate Nuxt 3 applications (one per league) with maximum code reuse through a shared component library. Mobile-first design with real-time WebSocket updates.
|
|
|
|
## Directory Structure
|
|
|
|
### Per-League Frontend (`frontend-sba/` and `frontend-pd/`)
|
|
|
|
```
|
|
frontend-{league}/
|
|
├── assets/ # Static assets
|
|
│ ├── css/
|
|
│ │ └── tailwind.css # Tailwind imports
|
|
│ └── images/
|
|
│ ├── logo.png
|
|
│ └── field-bg.svg
|
|
│
|
|
├── components/ # League-specific components
|
|
│ ├── Branding/
|
|
│ │ ├── Header.vue
|
|
│ │ ├── Footer.vue
|
|
│ │ └── Logo.vue
|
|
│ └── League/
|
|
│ └── SpecialFeatures.vue # League-specific UI elements
|
|
│
|
|
├── composables/ # Vue composables
|
|
│ ├── useAuth.ts # Authentication state
|
|
│ ├── useWebSocket.ts # WebSocket connection
|
|
│ ├── useGameState.ts # Game state management
|
|
│ └── useLeagueConfig.ts # League-specific config
|
|
│
|
|
├── layouts/
|
|
│ ├── default.vue # Standard layout
|
|
│ ├── game.vue # Game view layout
|
|
│ └── auth.vue # Auth pages layout
|
|
│
|
|
├── pages/
|
|
│ ├── index.vue # Home/dashboard
|
|
│ ├── games/
|
|
│ │ ├── [id].vue # Game view
|
|
│ │ ├── create.vue # Create new game
|
|
│ │ └── history.vue # Completed games
|
|
│ ├── auth/
|
|
│ │ ├── login.vue
|
|
│ │ └── callback.vue # Discord OAuth callback
|
|
│ └── spectate/
|
|
│ └── [id].vue # Spectator view
|
|
│
|
|
├── plugins/
|
|
│ ├── socket.client.ts # Socket.io plugin
|
|
│ └── auth.ts # Auth plugin
|
|
│
|
|
├── store/ # Pinia stores
|
|
│ ├── auth.ts # Authentication state
|
|
│ ├── game.ts # Current game state
|
|
│ ├── games.ts # Games list
|
|
│ └── ui.ts # UI state (modals, toasts)
|
|
│
|
|
├── types/
|
|
│ ├── game.ts # Game-related types
|
|
│ ├── player.ts # Player types
|
|
│ ├── api.ts # API response types
|
|
│ └── websocket.ts # WebSocket event types
|
|
│
|
|
├── utils/
|
|
│ ├── api.ts # API client
|
|
│ ├── formatters.ts # Data formatting utilities
|
|
│ └── validators.ts # Input validation
|
|
│
|
|
├── middleware/
|
|
│ ├── auth.ts # Auth guard
|
|
│ └── game-access.ts # Game access validation
|
|
│
|
|
├── app.vue # Root component
|
|
├── nuxt.config.ts # Nuxt configuration
|
|
├── tailwind.config.js # Tailwind configuration
|
|
├── tsconfig.json # TypeScript configuration
|
|
└── package.json
|
|
|
|
```
|
|
|
|
### Shared Component Library (`shared-components/`)
|
|
|
|
```
|
|
shared-components/
|
|
├── src/
|
|
│ ├── components/
|
|
│ │ ├── Game/
|
|
│ │ │ ├── GameBoard.vue # Baseball diamond visualization
|
|
│ │ │ ├── ScoreBoard.vue # Score display
|
|
│ │ │ ├── PlayByPlay.vue # Play history feed
|
|
│ │ │ ├── CurrentSituation.vue # Current game context
|
|
│ │ │ └── BaseRunners.vue # Runner indicators
|
|
│ │ │
|
|
│ │ ├── Decisions/
|
|
│ │ │ ├── DefensivePositioning.vue
|
|
│ │ │ ├── StolenBaseAttempt.vue
|
|
│ │ │ ├── OffensiveApproach.vue
|
|
│ │ │ └── DecisionTimer.vue
|
|
│ │ │
|
|
│ │ ├── Actions/
|
|
│ │ │ ├── SubstitutionModal.vue
|
|
│ │ │ ├── PitchingChange.vue
|
|
│ │ │ └── ActionButton.vue
|
|
│ │ │
|
|
│ │ ├── Display/
|
|
│ │ │ ├── PlayerCard.vue # Player card display
|
|
│ │ │ ├── DiceRoll.vue # Dice animation
|
|
│ │ │ ├── PlayOutcome.vue # Play result display
|
|
│ │ │ └── ConnectionStatus.vue # WebSocket status
|
|
│ │ │
|
|
│ │ └── Common/
|
|
│ │ ├── Button.vue
|
|
│ │ ├── Modal.vue
|
|
│ │ ├── Toast.vue
|
|
│ │ └── Loading.vue
|
|
│ │
|
|
│ ├── composables/
|
|
│ │ ├── useGameActions.ts # Shared game actions
|
|
│ │ └── useGameDisplay.ts # Shared display logic
|
|
│ │
|
|
│ └── types/
|
|
│ └── index.ts # Shared TypeScript types
|
|
│
|
|
├── package.json
|
|
└── tsconfig.json
|
|
```
|
|
|
|
## Key Components
|
|
|
|
### 1. WebSocket Composable (`composables/useWebSocket.ts`)
|
|
|
|
**Responsibilities**:
|
|
- Establish and maintain WebSocket connection
|
|
- Handle reconnection logic
|
|
- Emit events to server
|
|
- Subscribe to server events
|
|
- Connection status monitoring
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
import { io, Socket } from 'socket.io-client'
|
|
import { ref, onUnmounted } from 'vue'
|
|
|
|
export const useWebSocket = () => {
|
|
const socket = ref<Socket | null>(null)
|
|
const connected = ref(false)
|
|
const reconnecting = ref(false)
|
|
|
|
const connect = (token: string) => {
|
|
const config = useRuntimeConfig()
|
|
|
|
socket.value = io(config.public.wsUrl, {
|
|
auth: { token },
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionAttempts: 5
|
|
})
|
|
|
|
socket.value.on('connect', () => {
|
|
connected.value = true
|
|
reconnecting.value = false
|
|
})
|
|
|
|
socket.value.on('disconnect', () => {
|
|
connected.value = false
|
|
})
|
|
|
|
socket.value.on('reconnecting', () => {
|
|
reconnecting.value = true
|
|
})
|
|
}
|
|
|
|
const disconnect = () => {
|
|
socket.value?.disconnect()
|
|
socket.value = null
|
|
connected.value = false
|
|
}
|
|
|
|
const emit = (event: string, data: any) => {
|
|
if (!socket.value?.connected) {
|
|
throw new Error('WebSocket not connected')
|
|
}
|
|
socket.value.emit(event, data)
|
|
}
|
|
|
|
const on = (event: string, handler: (...args: any[]) => void) => {
|
|
socket.value?.on(event, handler)
|
|
}
|
|
|
|
const off = (event: string, handler?: (...args: any[]) => void) => {
|
|
socket.value?.off(event, handler)
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
disconnect()
|
|
})
|
|
|
|
return {
|
|
socket,
|
|
connected,
|
|
reconnecting,
|
|
connect,
|
|
disconnect,
|
|
emit,
|
|
on,
|
|
off
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Game State Store (`store/game.ts`)
|
|
|
|
**Responsibilities**:
|
|
- Hold current game state
|
|
- Update state from WebSocket events
|
|
- Provide computed properties for UI
|
|
- Handle optimistic updates
|
|
|
|
**Implementation**:
|
|
```typescript
|
|
import { defineStore } from 'pinia'
|
|
import type { GameState, PlayOutcome } from '~/types/game'
|
|
|
|
export const useGameStore = defineStore('game', () => {
|
|
// State
|
|
const gameState = ref<GameState | null>(null)
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const pendingAction = ref(false)
|
|
|
|
// Computed
|
|
const inning = computed(() => gameState.value?.inning ?? 1)
|
|
const half = computed(() => gameState.value?.half ?? 'top')
|
|
const outs = computed(() => gameState.value?.outs ?? 0)
|
|
const score = computed(() => ({
|
|
home: gameState.value?.home_score ?? 0,
|
|
away: gameState.value?.away_score ?? 0
|
|
}))
|
|
const runners = computed(() => gameState.value?.runners ?? {})
|
|
const currentBatter = computed(() => gameState.value?.current_batter)
|
|
const currentPitcher = computed(() => gameState.value?.current_pitcher)
|
|
const isMyTurn = computed(() => {
|
|
// Logic to determine if it's user's turn
|
|
return gameState.value?.decision_required?.user_id === useAuthStore().userId
|
|
})
|
|
|
|
// Actions
|
|
const setGameState = (state: GameState) => {
|
|
gameState.value = state
|
|
loading.value = false
|
|
error.value = null
|
|
}
|
|
|
|
const updateState = (updates: Partial<GameState>) => {
|
|
if (gameState.value) {
|
|
gameState.value = { ...gameState.value, ...updates }
|
|
}
|
|
}
|
|
|
|
const handlePlayCompleted = (outcome: PlayOutcome) => {
|
|
// Update state based on play outcome
|
|
if (gameState.value) {
|
|
updateState({
|
|
outs: outcome.outs_after,
|
|
runners: outcome.runners_after,
|
|
home_score: outcome.home_score,
|
|
away_score: outcome.away_score
|
|
})
|
|
}
|
|
pendingAction.value = false
|
|
}
|
|
|
|
const setError = (message: string) => {
|
|
error.value = message
|
|
loading.value = false
|
|
pendingAction.value = false
|
|
}
|
|
|
|
const reset = () => {
|
|
gameState.value = null
|
|
loading.value = false
|
|
error.value = null
|
|
pendingAction.value = false
|
|
}
|
|
|
|
return {
|
|
// State
|
|
gameState,
|
|
loading,
|
|
error,
|
|
pendingAction,
|
|
// Computed
|
|
inning,
|
|
half,
|
|
outs,
|
|
score,
|
|
runners,
|
|
currentBatter,
|
|
currentPitcher,
|
|
isMyTurn,
|
|
// Actions
|
|
setGameState,
|
|
updateState,
|
|
handlePlayCompleted,
|
|
setError,
|
|
reset
|
|
}
|
|
})
|
|
```
|
|
|
|
### 3. Game Board Component (`shared-components/Game/GameBoard.vue`)
|
|
|
|
**Responsibilities**:
|
|
- Visual baseball diamond
|
|
- Show runners on base
|
|
- Highlight active bases
|
|
- Responsive scaling
|
|
|
|
**Implementation**:
|
|
```vue
|
|
<template>
|
|
<div class="game-board relative w-full aspect-square max-w-md mx-auto">
|
|
<!-- Diamond SVG -->
|
|
<svg viewBox="0 0 100 100" class="w-full h-full">
|
|
<!-- Diamond shape -->
|
|
<path
|
|
d="M 50 10 L 90 50 L 50 90 L 10 50 Z"
|
|
class="fill-green-600 stroke-white stroke-2"
|
|
/>
|
|
|
|
<!-- Bases -->
|
|
<rect
|
|
v-for="base in bases"
|
|
:key="base.name"
|
|
:x="base.x"
|
|
:y="base.y"
|
|
width="8"
|
|
height="8"
|
|
:class="[
|
|
'stroke-white stroke-2',
|
|
hasRunner(base.name) ? 'fill-yellow-400' : 'fill-white'
|
|
]"
|
|
/>
|
|
|
|
<!-- Home plate -->
|
|
<polygon
|
|
points="50,88 48,90 52,90"
|
|
class="fill-white stroke-white"
|
|
/>
|
|
</svg>
|
|
|
|
<!-- Runner indicators -->
|
|
<div
|
|
v-for="(runner, base) in runners"
|
|
:key="base"
|
|
:class="getRunnerPositionClass(base)"
|
|
class="absolute"
|
|
>
|
|
<PlayerAvatar
|
|
v-if="runner"
|
|
:player-id="runner"
|
|
size="sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Batter indicator -->
|
|
<div class="absolute bottom-2 left-1/2 -translate-x-1/2">
|
|
<span class="text-xs text-white font-bold">
|
|
{{ currentBatter?.name }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Runners } from '~/types/game'
|
|
|
|
interface Props {
|
|
runners: Runners
|
|
currentBatter?: { name: string }
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const bases = [
|
|
{ name: 'first', x: 84, y: 46 },
|
|
{ name: 'second', x: 46, y: 6 },
|
|
{ name: 'third', x: 6, y: 46 }
|
|
]
|
|
|
|
const hasRunner = (base: string) => {
|
|
return props.runners[base] !== null
|
|
}
|
|
|
|
const getRunnerPositionClass = (base: string) => {
|
|
const positions = {
|
|
first: 'right-8 top-1/2 -translate-y-1/2',
|
|
second: 'top-8 left-1/2 -translate-x-1/2',
|
|
third: 'left-8 top-1/2 -translate-y-1/2'
|
|
}
|
|
return positions[base as keyof typeof positions]
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### 4. Decision Flow Component (`shared-components/Decisions/DefensivePositioning.vue`)
|
|
|
|
**Responsibilities**:
|
|
- Present defensive positioning options
|
|
- Validate selection
|
|
- Submit decision via WebSocket
|
|
- Show loading state
|
|
|
|
**Implementation**:
|
|
```vue
|
|
<template>
|
|
<div class="decision-card bg-white rounded-lg shadow-lg p-6">
|
|
<h3 class="text-lg font-bold mb-4">Set Defensive Positioning</h3>
|
|
|
|
<div class="space-y-3">
|
|
<button
|
|
v-for="option in positioningOptions"
|
|
:key="option.value"
|
|
@click="selectPositioning(option.value)"
|
|
:disabled="submitting"
|
|
class="w-full py-3 px-4 rounded-lg border-2 transition-colors"
|
|
:class="[
|
|
selected === option.value
|
|
? 'border-blue-500 bg-blue-50'
|
|
: 'border-gray-300 hover:border-blue-300',
|
|
submitting && 'opacity-50 cursor-not-allowed'
|
|
]"
|
|
>
|
|
<div class="text-left">
|
|
<div class="font-semibold">{{ option.label }}</div>
|
|
<div class="text-sm text-gray-600">{{ option.description }}</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mt-6 flex justify-between items-center">
|
|
<DecisionTimer
|
|
v-if="timeoutSeconds"
|
|
:seconds="timeoutSeconds"
|
|
@timeout="handleTimeout"
|
|
/>
|
|
<button
|
|
@click="submitDecision"
|
|
:disabled="!selected || submitting"
|
|
class="btn-primary"
|
|
>
|
|
{{ submitting ? 'Submitting...' : 'Confirm' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
timeoutSeconds?: number
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const emit = defineEmits<{
|
|
submit: [positioning: string]
|
|
timeout: []
|
|
}>()
|
|
|
|
const selected = ref<string | null>(null)
|
|
const submitting = ref(false)
|
|
|
|
const positioningOptions = [
|
|
{
|
|
value: 'standard',
|
|
label: 'Standard',
|
|
description: 'Normal defensive alignment'
|
|
},
|
|
{
|
|
value: 'infield_in',
|
|
label: 'Infield In',
|
|
description: 'Drawn in to prevent runs'
|
|
},
|
|
{
|
|
value: 'shift_left',
|
|
label: 'Shift Left',
|
|
description: 'Shifted for pull hitter'
|
|
},
|
|
{
|
|
value: 'shift_right',
|
|
label: 'Shift Right',
|
|
description: 'Shifted for opposite field'
|
|
}
|
|
]
|
|
|
|
const selectPositioning = (value: string) => {
|
|
if (!submitting.value) {
|
|
selected.value = value
|
|
}
|
|
}
|
|
|
|
const submitDecision = async () => {
|
|
if (!selected.value || submitting.value) return
|
|
|
|
submitting.value = true
|
|
emit('submit', selected.value)
|
|
}
|
|
|
|
const handleTimeout = () => {
|
|
emit('timeout')
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## State Management Architecture
|
|
|
|
### Pinia Stores
|
|
|
|
**Auth Store**:
|
|
- User authentication state
|
|
- Discord profile data
|
|
- Team ownership information
|
|
- Token management
|
|
|
|
**Game Store**:
|
|
- Current game state
|
|
- Real-time updates from WebSocket
|
|
- Pending actions
|
|
- Error handling
|
|
|
|
**Games List Store**:
|
|
- Active games list
|
|
- Completed games history
|
|
- Game filtering
|
|
|
|
**UI Store**:
|
|
- Modal states
|
|
- Toast notifications
|
|
- Loading indicators
|
|
- Theme preferences
|
|
|
|
## Mobile-First Responsive Design
|
|
|
|
### Breakpoints
|
|
```javascript
|
|
// tailwind.config.js
|
|
module.exports = {
|
|
theme: {
|
|
screens: {
|
|
'xs': '375px', // Small phones
|
|
'sm': '640px', // Large phones
|
|
'md': '768px', // Tablets
|
|
'lg': '1024px', // Desktop
|
|
'xl': '1280px', // Large desktop
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Layout Strategy
|
|
|
|
**Mobile (< 768px)**:
|
|
- Single column layout
|
|
- Bottom sheet for decisions
|
|
- Sticky scoreboard at top
|
|
- Collapsible play-by-play
|
|
- Full-screen game board
|
|
|
|
**Tablet (768px - 1024px)**:
|
|
- Two column layout (game + sidebar)
|
|
- Larger game board
|
|
- Side panel for decisions
|
|
- Expanded play history
|
|
|
|
**Desktop (> 1024px)**:
|
|
- Three column layout (optional)
|
|
- Full game board center
|
|
- Decision panel right
|
|
- Stats panel left
|
|
|
|
## WebSocket Event Handling
|
|
|
|
### Event Listeners Setup
|
|
|
|
```typescript
|
|
// composables/useGameActions.ts
|
|
export const useGameActions = (gameId: string) => {
|
|
const { socket, emit, on, off } = useWebSocket()
|
|
const gameStore = useGameStore()
|
|
|
|
onMounted(() => {
|
|
// Join game room
|
|
emit('join_game', { game_id: gameId, role: 'player' })
|
|
|
|
// Listen for state updates
|
|
on('game_state_update', (data: GameState) => {
|
|
gameStore.setGameState(data)
|
|
})
|
|
|
|
on('play_completed', (data: PlayOutcome) => {
|
|
gameStore.handlePlayCompleted(data)
|
|
})
|
|
|
|
on('dice_rolled', (data: DiceRoll) => {
|
|
// Trigger dice animation
|
|
showDiceRoll(data.roll, data.animation_duration)
|
|
})
|
|
|
|
on('decision_required', (data: DecisionPrompt) => {
|
|
// Show decision UI
|
|
gameStore.setDecisionRequired(data)
|
|
})
|
|
|
|
on('invalid_action', (data: ErrorData) => {
|
|
gameStore.setError(data.message)
|
|
})
|
|
|
|
on('game_error', (data: ErrorData) => {
|
|
gameStore.setError(data.message)
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
// Clean up listeners
|
|
off('game_state_update')
|
|
off('play_completed')
|
|
off('dice_rolled')
|
|
off('decision_required')
|
|
off('invalid_action')
|
|
off('game_error')
|
|
|
|
// Leave game room
|
|
emit('leave_game', { game_id: gameId })
|
|
})
|
|
|
|
// Action methods
|
|
const setDefense = (positioning: string) => {
|
|
emit('set_defense', { game_id: gameId, positioning })
|
|
}
|
|
|
|
const setStolenBase = (runners: string[]) => {
|
|
emit('set_stolen_base', { game_id: gameId, runners })
|
|
}
|
|
|
|
const setOffensiveApproach = (approach: string) => {
|
|
emit('set_offensive_approach', { game_id: gameId, approach })
|
|
}
|
|
|
|
return {
|
|
setDefense,
|
|
setStolenBase,
|
|
setOffensiveApproach
|
|
}
|
|
}
|
|
```
|
|
|
|
## League-Specific Customization
|
|
|
|
### Configuration
|
|
|
|
```typescript
|
|
// frontend-sba/nuxt.config.ts
|
|
export default defineNuxtConfig({
|
|
runtimeConfig: {
|
|
public: {
|
|
leagueId: 'sba',
|
|
leagueName: 'Super Baseball Alliance',
|
|
apiUrl: process.env.NUXT_PUBLIC_API_URL,
|
|
wsUrl: process.env.NUXT_PUBLIC_WS_URL,
|
|
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID,
|
|
primaryColor: '#1e40af', // Blue
|
|
secondaryColor: '#dc2626' // Red
|
|
}
|
|
}
|
|
})
|
|
|
|
// frontend-pd/nuxt.config.ts
|
|
export default defineNuxtConfig({
|
|
runtimeConfig: {
|
|
public: {
|
|
leagueId: 'pd',
|
|
leagueName: 'Paper Dynasty',
|
|
apiUrl: process.env.NUXT_PUBLIC_API_URL,
|
|
wsUrl: process.env.NUXT_PUBLIC_WS_URL,
|
|
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID,
|
|
primaryColor: '#16a34a', // Green
|
|
secondaryColor: '#ea580c' // Orange
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
### Theming
|
|
|
|
```typescript
|
|
// composables/useLeagueConfig.ts
|
|
export const useLeagueConfig = () => {
|
|
const config = useRuntimeConfig()
|
|
|
|
return {
|
|
leagueId: config.public.leagueId,
|
|
leagueName: config.public.leagueName,
|
|
colors: {
|
|
primary: config.public.primaryColor,
|
|
secondary: config.public.secondaryColor
|
|
},
|
|
features: {
|
|
showScoutingData: config.public.leagueId === 'pd',
|
|
useSimplePlayerCards: config.public.leagueId === 'sba'
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Performance Optimizations
|
|
|
|
### Code Splitting
|
|
- Lazy load game components
|
|
- Route-based code splitting
|
|
- Dynamic imports for heavy libraries
|
|
|
|
### Asset Optimization
|
|
- Image lazy loading
|
|
- SVG sprites for icons
|
|
- Optimized font loading
|
|
|
|
### State Updates
|
|
- Debounce non-critical updates
|
|
- Optimistic UI updates
|
|
- Efficient re-rendering with `v-memo`
|
|
|
|
## Error Handling
|
|
|
|
### Network Errors
|
|
```typescript
|
|
const handleNetworkError = (error: Error) => {
|
|
const uiStore = useUiStore()
|
|
|
|
if (error.message.includes('WebSocket')) {
|
|
uiStore.showToast({
|
|
type: 'error',
|
|
message: 'Connection lost. Reconnecting...'
|
|
})
|
|
} else {
|
|
uiStore.showToast({
|
|
type: 'error',
|
|
message: 'Network error. Please try again.'
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Game Errors
|
|
```typescript
|
|
const handleGameError = (error: GameError) => {
|
|
const uiStore = useUiStore()
|
|
|
|
uiStore.showModal({
|
|
title: 'Game Error',
|
|
message: error.message,
|
|
actions: [
|
|
{
|
|
label: 'Retry',
|
|
handler: () => retryLastAction()
|
|
},
|
|
{
|
|
label: 'Reload Game',
|
|
handler: () => reloadGameState()
|
|
}
|
|
]
|
|
})
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
**Next Steps**: See [03-gameplay-features.md](./03-gameplay-features.md) for gameplay implementation details. |