Add Phase F4 live gameplay project plan
- Document Phase F4 implementation tasks - Track progress on live gameplay features - Define component structure and requirements Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f1b2647306
commit
1a21d3d2d4
822
frontend/project_plans/PHASE_F4_live_gameplay.json
Normal file
822
frontend/project_plans/PHASE_F4_live_gameplay.json
Normal file
@ -0,0 +1,822 @@
|
||||
{
|
||||
"meta": {
|
||||
"phaseId": "PHASE_F4",
|
||||
"name": "Live Gameplay",
|
||||
"version": "1.0.0",
|
||||
"created": "2026-01-31",
|
||||
"lastUpdated": "2026-02-01",
|
||||
"totalTasks": 14,
|
||||
"completedTasks": 14,
|
||||
"status": "complete",
|
||||
"description": "WebSocket integration, game state sync, action handling, complete game flow. This phase connects the Phaser rendering layer to the backend game engine via Socket.IO, enabling real-time multiplayer gameplay."
|
||||
},
|
||||
"stylingPrinciples": [
|
||||
"Game UI overlays use Vue components positioned over Phaser canvas",
|
||||
"Action menus are context-sensitive and appear near relevant game elements",
|
||||
"Turn phase indicator always visible but non-intrusive",
|
||||
"Loading states during action execution (optimistic UI optional)",
|
||||
"Error feedback via toast notifications, not modal interrupts",
|
||||
"Mobile touch-friendly: large buttons, swipe gestures where appropriate",
|
||||
"Consistent visual language between Vue overlays and Phaser objects"
|
||||
],
|
||||
"dependencies": {
|
||||
"phases": [
|
||||
"PHASE_F0",
|
||||
"PHASE_F1",
|
||||
"PHASE_F2",
|
||||
"PHASE_F3"
|
||||
],
|
||||
"backend": [
|
||||
"POST /api/games - Create new game",
|
||||
"GET /api/games/{id} - Get game info",
|
||||
"GET /api/games/me/active - List active games",
|
||||
"WebSocket namespace /game - All real-time events",
|
||||
"game:join -> game:state - Join game and receive state",
|
||||
"game:action -> game:action_result - Execute actions",
|
||||
"game:state broadcasts on state changes",
|
||||
"game:game_over on game completion"
|
||||
],
|
||||
"external": [
|
||||
"Socket.IO client (already installed from F0)",
|
||||
"Game types from src/types/game.ts (from F3)"
|
||||
]
|
||||
},
|
||||
"architectureNotes": {
|
||||
"stateFlow": {
|
||||
"principle": "Pinia game store is single source of truth. WebSocket updates store, store updates Phaser.",
|
||||
"flow": "Socket.IO -> game store -> Phaser (via bridge events)",
|
||||
"storeResponsibilities": [
|
||||
"Hold current VisibleGameState",
|
||||
"Track connection status",
|
||||
"Provide computed helpers (isMyTurn, myHand, etc.)",
|
||||
"Queue pending actions during reconnection"
|
||||
]
|
||||
},
|
||||
"actionFlow": {
|
||||
"principle": "Actions are intentions sent to server. Server is authoritative.",
|
||||
"flow": "User interaction -> composable -> Socket.IO emit -> server validates -> broadcasts new state",
|
||||
"optimisticUI": "Optional - can show pending state while awaiting confirmation",
|
||||
"errorHandling": "game:action_result with success=false shows toast, reverts any optimistic changes"
|
||||
},
|
||||
"componentStructure": {
|
||||
"GamePage.vue": "Main page, manages socket connection lifecycle",
|
||||
"PhaserGame.vue": "Phaser canvas (from F3)",
|
||||
"GameOverlay.vue": "Container for all Vue overlays on top of Phaser",
|
||||
"TurnIndicator.vue": "Shows current phase and whose turn",
|
||||
"AttackMenu.vue": "Attack selection when in attack phase",
|
||||
"HandPanel.vue": "Alternative hand display for complex interactions",
|
||||
"ForcedActionModal.vue": "Modal for required actions (prize selection, etc.)"
|
||||
},
|
||||
"socketEvents": {
|
||||
"clientToServer": [
|
||||
{
|
||||
"event": "join_game",
|
||||
"payload": "JoinGameMessage",
|
||||
"when": "On entering game page"
|
||||
},
|
||||
{
|
||||
"event": "action",
|
||||
"payload": "ActionMessage",
|
||||
"when": "On any game action"
|
||||
},
|
||||
{
|
||||
"event": "resign",
|
||||
"payload": "ResignMessage",
|
||||
"when": "On resign confirmation"
|
||||
},
|
||||
{
|
||||
"event": "heartbeat",
|
||||
"payload": "HeartbeatMessage",
|
||||
"when": "Every 30s while connected"
|
||||
}
|
||||
],
|
||||
"serverToClient": [
|
||||
{
|
||||
"event": "game_state",
|
||||
"payload": "GameStateMessage",
|
||||
"when": "On join, after actions"
|
||||
},
|
||||
{
|
||||
"event": "action_result",
|
||||
"payload": "ActionResultMessage",
|
||||
"when": "After action processed"
|
||||
},
|
||||
{
|
||||
"event": "error",
|
||||
"payload": "ErrorMessage",
|
||||
"when": "On validation failures"
|
||||
},
|
||||
{
|
||||
"event": "turn_start",
|
||||
"payload": "TurnStartMessage",
|
||||
"when": "Turn begins"
|
||||
},
|
||||
{
|
||||
"event": "turn_timeout",
|
||||
"payload": "TurnTimeoutMessage",
|
||||
"when": "Timer warning/expiry"
|
||||
},
|
||||
{
|
||||
"event": "game_over",
|
||||
"payload": "GameOverMessage",
|
||||
"when": "Game ends"
|
||||
},
|
||||
{
|
||||
"event": "opponent_status",
|
||||
"payload": "OpponentStatusMessage",
|
||||
"when": "Opponent connects/disconnects"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"id": "F4-001",
|
||||
"name": "Create WebSocket game types",
|
||||
"description": "TypeScript types for all WebSocket messages matching backend schemas",
|
||||
"category": "api",
|
||||
"priority": 1,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/types/ws.ts",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/types/index.ts",
|
||||
"status": "modify"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Define ClientMessage types: JoinGameMessage, ActionMessage, ResignMessage, HeartbeatMessage",
|
||||
"Define ServerMessage types: GameStateMessage, ActionResultMessage, ErrorMessage, TurnStartMessage, TurnTimeoutMessage, GameOverMessage, OpponentStatusMessage, HeartbeatAckMessage",
|
||||
"Define WSErrorCode enum matching backend WSErrorCode",
|
||||
"Define ConnectionStatus enum: connected, disconnected, reconnecting",
|
||||
"Create type guards: isGameStateMessage, isErrorMessage, etc.",
|
||||
"Export all from types/index.ts",
|
||||
"Reference backend/app/schemas/ws_messages.py for exact field names"
|
||||
],
|
||||
"acceptance": [
|
||||
"All WebSocket message types defined",
|
||||
"Types match backend schemas exactly",
|
||||
"Type guards work correctly",
|
||||
"Exported from types/index.ts"
|
||||
],
|
||||
"estimatedHours": 2,
|
||||
"notes": "Can be done in parallel with other tasks as foundation work"
|
||||
},
|
||||
{
|
||||
"id": "F4-002",
|
||||
"name": "Enhance game store for real-time state",
|
||||
"description": "Extend the Pinia game store to handle WebSocket state and actions",
|
||||
"category": "stores",
|
||||
"priority": 2,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-001"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/stores/game.ts",
|
||||
"status": "modify"
|
||||
},
|
||||
{
|
||||
"path": "src/stores/game.spec.ts",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Add state: currentGameId, connectionStatus, lastEventId, pendingActions",
|
||||
"Add computed: isMyTurn, myPlayerState, opponentPlayerState, currentPhase, isGameOver",
|
||||
"Add computed: myHand, myActive, myBench, myDeckCount, myDiscardCount",
|
||||
"Add computed: opponentActive, opponentBench, opponentHandCount, opponentDeckCount",
|
||||
"Add action: setGameState(state: VisibleGameState) - updates from server",
|
||||
"Add action: setConnectionStatus(status: ConnectionStatus)",
|
||||
"Add action: queueAction(action: Action) - for offline queueing",
|
||||
"Add action: clearGame() - reset all state on exit",
|
||||
"Use computed helpers from types/game.ts (getMyPlayerState, getOpponentState)",
|
||||
"Persist lastEventId for reconnection support"
|
||||
],
|
||||
"acceptance": [
|
||||
"Store holds complete game state",
|
||||
"Computed properties derive correct values",
|
||||
"Connection status tracked",
|
||||
"Tests verify all computed derivations",
|
||||
"Store resets cleanly on game exit"
|
||||
],
|
||||
"estimatedHours": 3
|
||||
},
|
||||
{
|
||||
"id": "F4-003",
|
||||
"name": "Create game socket composable",
|
||||
"description": "Composable for managing WebSocket connection to game namespace",
|
||||
"category": "composables",
|
||||
"priority": 3,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-001",
|
||||
"F4-002"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/composables/useGameSocket.ts",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/composables/useGameSocket.spec.ts",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Create singleton Socket.IO connection to /game namespace",
|
||||
"Include auth token in connection handshake (from auth store)",
|
||||
"Provide connect(gameId: string) - joins game room",
|
||||
"Provide disconnect() - leaves game room and cleans up",
|
||||
"Provide sendAction(action: Action) - emits action message",
|
||||
"Provide sendResign() - emits resign message",
|
||||
"Set up heartbeat interval (every 30s)",
|
||||
"Handle all server events and update game store",
|
||||
"Handle connection errors with toast notifications",
|
||||
"Implement automatic reconnection with exponential backoff",
|
||||
"Track lastEventId for reconnection replay",
|
||||
"Emit 'game:join' with last_event_id on reconnect"
|
||||
],
|
||||
"acceptance": [
|
||||
"Connection establishes with auth",
|
||||
"All server events handled correctly",
|
||||
"Actions sent and results processed",
|
||||
"Heartbeat keeps connection alive",
|
||||
"Reconnection works with event replay",
|
||||
"Tests verify event handling (mocked socket)"
|
||||
],
|
||||
"estimatedHours": 5
|
||||
},
|
||||
{
|
||||
"id": "F4-004",
|
||||
"name": "Create game actions composable",
|
||||
"description": "Composable providing typed action dispatch functions",
|
||||
"category": "composables",
|
||||
"priority": 4,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-003"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/composables/useGameActions.ts",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/composables/useGameActions.spec.ts",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Wraps useGameSocket for type-safe action dispatch",
|
||||
"playCard(instanceId: string, targetZone?: ZoneType, targetSlot?: number)",
|
||||
"attachEnergy(energyInstanceId: string, targetPokemonId: string)",
|
||||
"evolve(handCardId: string, targetPokemonId: string)",
|
||||
"attack(attackIndex: number, targetPokemonId?: string)",
|
||||
"retreat(newActiveId: string)",
|
||||
"useAbility(pokemonId: string, abilityIndex: number, targets?: string[])",
|
||||
"endTurn()",
|
||||
"selectPrize(prizeIndex: number)",
|
||||
"selectNewActive(pokemonId: string)",
|
||||
"Each function validates basic preconditions before sending",
|
||||
"Return Promise that resolves/rejects based on action_result",
|
||||
"Track pending action state for UI feedback"
|
||||
],
|
||||
"acceptance": [
|
||||
"All action types have typed functions",
|
||||
"Precondition validation prevents invalid sends",
|
||||
"Promises resolve/reject correctly",
|
||||
"Pending state trackable",
|
||||
"Tests verify each action type"
|
||||
],
|
||||
"estimatedHours": 4
|
||||
},
|
||||
{
|
||||
"id": "F4-005",
|
||||
"name": "Create game lobby page",
|
||||
"description": "Page for creating new games and viewing active games",
|
||||
"category": "pages",
|
||||
"priority": 5,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-002"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/pages/PlayPage.vue",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/pages/PlayPage.spec.ts",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/router/index.ts",
|
||||
"status": "modify"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Route: /play (already defined in router)",
|
||||
"Section: Active Games - list from GET /api/games/me/active",
|
||||
"Each active game shows: opponent name, turn indicator, resume button",
|
||||
"Section: Start New Game",
|
||||
"Deck selector dropdown (from user's valid decks)",
|
||||
"Create Game button -> POST /api/games with selected deck",
|
||||
"On game creation, navigate to /game/:id",
|
||||
"Show loading states during API calls",
|
||||
"Handle errors with toast notifications",
|
||||
"Future: invite link sharing (display game ID for now)"
|
||||
],
|
||||
"styling": [
|
||||
"Card-based layout for active games",
|
||||
"Visual distinction between 'your turn' and 'waiting'",
|
||||
"Prominent 'Create Game' CTA button",
|
||||
"Deck selector shows deck name and validation status",
|
||||
"Mobile: stack sections vertically"
|
||||
],
|
||||
"acceptance": [
|
||||
"Active games list displays correctly",
|
||||
"Can resume existing games",
|
||||
"Can create new game with deck selection",
|
||||
"Navigation to game page works",
|
||||
"Loading and error states handled"
|
||||
],
|
||||
"estimatedHours": 4
|
||||
},
|
||||
{
|
||||
"id": "F4-006",
|
||||
"name": "Enhance GamePage with socket lifecycle",
|
||||
"description": "Update GamePage to manage WebSocket connection and game state",
|
||||
"category": "pages",
|
||||
"priority": 6,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-003",
|
||||
"F4-004"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/pages/GamePage.vue",
|
||||
"status": "modify"
|
||||
},
|
||||
{
|
||||
"path": "src/pages/GamePage.spec.ts",
|
||||
"status": "modify"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"On mount: connect to game socket with game ID from route param",
|
||||
"Wait for game:state before showing game content",
|
||||
"Show loading overlay while connecting/loading state",
|
||||
"Watch game store state -> emit to Phaser bridge (game:state_updated)",
|
||||
"Handle connection errors with retry UI",
|
||||
"Handle game:game_over -> show GameOverModal",
|
||||
"On unmount: disconnect socket, clear game store",
|
||||
"Add exit button with confirmation dialog",
|
||||
"Exit navigates back to /play"
|
||||
],
|
||||
"acceptance": [
|
||||
"Socket connects on mount with correct game ID",
|
||||
"Game state flows to Phaser via bridge",
|
||||
"Loading state shown until ready",
|
||||
"Exit works with confirmation",
|
||||
"Clean disconnect on unmount"
|
||||
],
|
||||
"estimatedHours": 3
|
||||
},
|
||||
{
|
||||
"id": "F4-007",
|
||||
"name": "Create game overlay container",
|
||||
"description": "Vue component container for all UI overlays on the game canvas",
|
||||
"category": "components",
|
||||
"priority": 7,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-006"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/components/game/GameOverlay.vue",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Positioned absolutely over Phaser canvas (pointer-events: none on container)",
|
||||
"Contains slots for: turn-indicator, phase-actions, attack-menu, forced-action",
|
||||
"Each child overlay has pointer-events: auto to be interactive",
|
||||
"Responsive positioning for overlays",
|
||||
"Z-index above Phaser canvas",
|
||||
"Pass down game state via provide/inject or props"
|
||||
],
|
||||
"acceptance": [
|
||||
"Overlay container renders over Phaser",
|
||||
"Child components are interactive",
|
||||
"Phaser canvas still receives input in non-overlay areas",
|
||||
"Responsive layout works"
|
||||
],
|
||||
"estimatedHours": 2
|
||||
},
|
||||
{
|
||||
"id": "F4-008",
|
||||
"name": "Create turn indicator component",
|
||||
"description": "Display current turn phase and active player",
|
||||
"category": "components",
|
||||
"priority": 8,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-007"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/components/game/TurnIndicator.vue",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/components/game/TurnIndicator.spec.ts",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Show current phase: DRAW, MAIN, ATTACK, END",
|
||||
"Show whose turn it is: 'Your Turn' or 'Opponent's Turn'",
|
||||
"Turn number display (optional)",
|
||||
"Visual distinction for your turn vs waiting",
|
||||
"Phase icons or colored indicators",
|
||||
"Positioned top-center of game area",
|
||||
"Animate phase transitions"
|
||||
],
|
||||
"styling": [
|
||||
"Semi-transparent background",
|
||||
"Your turn: accent color highlight",
|
||||
"Opponent's turn: muted colors",
|
||||
"Phase displayed as badge/pill",
|
||||
"Compact on mobile, expanded on desktop"
|
||||
],
|
||||
"acceptance": [
|
||||
"Correctly displays current phase",
|
||||
"Correctly indicates whose turn",
|
||||
"Visual styles match design system",
|
||||
"Tests verify display logic"
|
||||
],
|
||||
"estimatedHours": 2
|
||||
},
|
||||
{
|
||||
"id": "F4-009",
|
||||
"name": "Create phase actions component",
|
||||
"description": "Action buttons available during each phase",
|
||||
"category": "components",
|
||||
"priority": 9,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-004",
|
||||
"F4-008"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/components/game/PhaseActions.vue",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/components/game/PhaseActions.spec.ts",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Show context-appropriate actions for current phase",
|
||||
"MAIN phase: End Turn button, Retreat button (if can retreat)",
|
||||
"ATTACK phase: shows after selecting 'Attack' action from card",
|
||||
"END phase: auto-advance (server handles)",
|
||||
"Disable buttons when not your turn",
|
||||
"Loading state on buttons during action execution",
|
||||
"Position: bottom-right of game area",
|
||||
"Use useGameActions composable for dispatch"
|
||||
],
|
||||
"acceptance": [
|
||||
"Correct buttons shown per phase",
|
||||
"Buttons disabled when not your turn",
|
||||
"Actions dispatch correctly",
|
||||
"Loading states work",
|
||||
"Tests verify button visibility logic"
|
||||
],
|
||||
"estimatedHours": 3
|
||||
},
|
||||
{
|
||||
"id": "F4-010",
|
||||
"name": "Create attack menu component",
|
||||
"description": "UI for selecting which attack to use",
|
||||
"category": "components",
|
||||
"priority": 10,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-004",
|
||||
"F4-007"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/components/game/AttackMenu.vue",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/components/game/AttackMenu.spec.ts",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Shows when user taps 'Attack' or taps active Pokemon during attack phase",
|
||||
"List all attacks for current active Pokemon",
|
||||
"Display: attack name, energy cost, damage, effect text",
|
||||
"Disable attacks that don't have enough energy attached",
|
||||
"Disable if Pokemon has status preventing attack (paralyzed)",
|
||||
"On select: check if target selection needed, then dispatch attack action",
|
||||
"Cancel button to close menu",
|
||||
"Position: centered modal or bottom sheet on mobile"
|
||||
],
|
||||
"styling": [
|
||||
"Card-like attack entries",
|
||||
"Energy cost icons in correct colors",
|
||||
"Disabled attacks grayed out with reason tooltip",
|
||||
"Selected attack highlighted",
|
||||
"Smooth open/close animation"
|
||||
],
|
||||
"acceptance": [
|
||||
"Lists all attacks correctly",
|
||||
"Energy requirements checked",
|
||||
"Status conditions prevent attacks",
|
||||
"Attack selection dispatches action",
|
||||
"Tests verify enable/disable logic"
|
||||
],
|
||||
"estimatedHours": 4
|
||||
},
|
||||
{
|
||||
"id": "F4-011",
|
||||
"name": "Create forced action modal",
|
||||
"description": "Modal for handling required player choices",
|
||||
"category": "components",
|
||||
"priority": 11,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-004",
|
||||
"F4-007"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/components/game/ForcedActionModal.vue",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/components/game/ForcedActionModal.spec.ts",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Shows when game state has forced_action_type set",
|
||||
"Types: prize_selection, new_active_selection, discard_selection",
|
||||
"Prize selection: show 6 prize card positions, select one to claim",
|
||||
"New active selection: show bench Pokemon, select one to promote",
|
||||
"Discard selection: show hand/bench cards, select required number to discard",
|
||||
"Cannot close without completing the action",
|
||||
"Display forced_action_reason as instruction text",
|
||||
"Dispatch appropriate action on selection"
|
||||
],
|
||||
"styling": [
|
||||
"Modal overlay blocks interaction with game",
|
||||
"Clear instruction header",
|
||||
"Selectable cards with visual feedback",
|
||||
"Confirm button after selection",
|
||||
"No close/cancel button (forced action)"
|
||||
],
|
||||
"acceptance": [
|
||||
"Shows for all forced action types",
|
||||
"Cannot be dismissed without action",
|
||||
"Selection dispatches correct action",
|
||||
"Modal closes after successful action",
|
||||
"Tests verify each action type"
|
||||
],
|
||||
"estimatedHours": 4
|
||||
},
|
||||
{
|
||||
"id": "F4-012",
|
||||
"name": "Create game over modal",
|
||||
"description": "Display game results when game ends",
|
||||
"category": "components",
|
||||
"priority": 12,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-007"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/components/game/GameOverModal.vue",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/components/game/GameOverModal.spec.ts",
|
||||
"status": "create"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"Shows when game store has winner_id or end_reason set",
|
||||
"Display: Victory or Defeat (or Draw)",
|
||||
"Show end reason: 'All prizes claimed', 'Opponent has no Pokemon', 'Deck empty', 'Resignation', 'Timeout'",
|
||||
"Show basic stats: turn count, prizes taken",
|
||||
"Return to Lobby button -> navigate to /play",
|
||||
"Future: Rematch button (not in v1)"
|
||||
],
|
||||
"styling": [
|
||||
"Celebratory for victory (subtle animation)",
|
||||
"Muted for defeat",
|
||||
"Large clear result text",
|
||||
"Stats in readable format",
|
||||
"Single prominent CTA button"
|
||||
],
|
||||
"acceptance": [
|
||||
"Correct victory/defeat display",
|
||||
"End reason shown",
|
||||
"Return button navigates correctly",
|
||||
"Tests verify display logic"
|
||||
],
|
||||
"estimatedHours": 2
|
||||
},
|
||||
{
|
||||
"id": "F4-013",
|
||||
"name": "Implement Phaser hand interactions",
|
||||
"description": "Handle card interactions in hand zone via Phaser",
|
||||
"category": "game",
|
||||
"priority": 13,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-004",
|
||||
"F4-006"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/game/interactions/HandManager.ts",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/game/scenes/MatchScene.ts",
|
||||
"status": "modify"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"HandManager class handles all hand card interactions",
|
||||
"Tap/click card to select -> emit card_clicked to bridge",
|
||||
"Vue handles selected card state and shows valid play options",
|
||||
"Drag card to bench zone -> emit play_card intention to bridge",
|
||||
"Drag energy card to Pokemon -> emit attach_energy intention",
|
||||
"Validate drop zones based on card type (Pokemon to bench, energy to Pokemon)",
|
||||
"Visual feedback during drag: valid zones highlight",
|
||||
"Cancel drag by releasing outside valid zone",
|
||||
"Bridge events trigger Vue action dispatch via useGameActions",
|
||||
"Integrate with existing Card and Zone objects from F3"
|
||||
],
|
||||
"acceptance": [
|
||||
"Cards in hand are interactive",
|
||||
"Tap selects card and shows options",
|
||||
"Drag to valid zone triggers action",
|
||||
"Invalid drops are cancelled",
|
||||
"Visual feedback during drag"
|
||||
],
|
||||
"estimatedHours": 6
|
||||
},
|
||||
{
|
||||
"id": "F4-014",
|
||||
"name": "Implement Phaser board interactions",
|
||||
"description": "Handle interactions with cards on the board",
|
||||
"category": "game",
|
||||
"priority": 14,
|
||||
"completed": true,
|
||||
"tested": true,
|
||||
"dependencies": [
|
||||
"F4-007"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "src/game/interactions/BoardManager.ts",
|
||||
"status": "create"
|
||||
},
|
||||
{
|
||||
"path": "src/game/scenes/MatchScene.ts",
|
||||
"status": "modify"
|
||||
}
|
||||
],
|
||||
"details": [
|
||||
"BoardManager handles active/bench Pokemon interactions",
|
||||
"Tap active Pokemon: show attack menu (if attack phase and your turn)",
|
||||
"Tap bench Pokemon: option to retreat (if your turn and can retreat)",
|
||||
"Tap opponent's Pokemon: for targeting (if attack/ability requires target)",
|
||||
"Target selection mode: highlight valid targets, emit on selection",
|
||||
"Long press/right-click: show card detail overlay",
|
||||
"Integrate targeting with AttackMenu and ability usage",
|
||||
"Zone clicks emit zone_clicked for general targeting",
|
||||
"All interactions respect current phase and turn"
|
||||
],
|
||||
"acceptance": [
|
||||
"Board cards are interactive",
|
||||
"Actions respect phase/turn rules",
|
||||
"Target selection works for attacks",
|
||||
"Card details accessible",
|
||||
"Opponent cards selectable as targets"
|
||||
],
|
||||
"estimatedHours": 5
|
||||
}
|
||||
],
|
||||
"testingApproach": {
|
||||
"unitTests": [
|
||||
"Game store computed derivations",
|
||||
"Action composable precondition checks",
|
||||
"WebSocket message type guards",
|
||||
"Component display logic (mocked store)"
|
||||
],
|
||||
"componentTests": [
|
||||
"TurnIndicator renders correctly per state",
|
||||
"AttackMenu enables/disables attacks correctly",
|
||||
"ForcedActionModal handles each action type",
|
||||
"GameOverModal shows correct result"
|
||||
],
|
||||
"integrationTests": [
|
||||
"PlayPage creates game and navigates",
|
||||
"GamePage connects and receives state",
|
||||
"Full action flow: play card -> state update -> render"
|
||||
],
|
||||
"manualTests": [
|
||||
"Complete game flow from lobby to game over",
|
||||
"Reconnection after disconnect",
|
||||
"All action types execute correctly",
|
||||
"Mobile touch interactions",
|
||||
"Multiple browser tabs (spectator mode future)"
|
||||
],
|
||||
"note": "Socket.IO tests should mock the socket, not connect to real server"
|
||||
},
|
||||
"backendIntegrationNotes": {
|
||||
"apiEndpoints": [
|
||||
{
|
||||
"endpoint": "POST /api/games",
|
||||
"request": "GameCreateRequest",
|
||||
"response": "GameResponse"
|
||||
},
|
||||
{
|
||||
"endpoint": "GET /api/games/{id}",
|
||||
"response": "GameResponse"
|
||||
},
|
||||
{
|
||||
"endpoint": "GET /api/games/me/active",
|
||||
"response": "List[GameResponse]"
|
||||
}
|
||||
],
|
||||
"socketNamespace": "/game",
|
||||
"authInHandshake": "Access token sent in auth object during connection",
|
||||
"messageIdempotency": "All client messages have message_id for tracking and duplicate detection",
|
||||
"eventIdReplay": "On reconnect, send last_event_id to receive missed events"
|
||||
},
|
||||
"riskMitigation": [
|
||||
{
|
||||
"risk": "Socket.IO connection reliability",
|
||||
"mitigation": "Implement robust reconnection with exponential backoff. Queue actions during disconnect. Show clear connection status to user."
|
||||
},
|
||||
{
|
||||
"risk": "State desync between client and server",
|
||||
"mitigation": "Server is authoritative. On any mismatch or error, request full state refresh. Never trust client-computed state."
|
||||
},
|
||||
{
|
||||
"risk": "Mobile performance with overlays",
|
||||
"mitigation": "Keep Vue overlays simple. Use CSS transforms for animations. Consider reducing overlay complexity on mobile."
|
||||
},
|
||||
{
|
||||
"risk": "Complex hand drag interactions on touch",
|
||||
"mitigation": "Provide tap-to-select alternative. Show clear valid drop zone indicators. Large touch targets."
|
||||
},
|
||||
{
|
||||
"risk": "Action timing conflicts",
|
||||
"mitigation": "Disable UI during pending actions. Show loading states. Handle race conditions gracefully."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"F4-001 (types) can be done first as foundation",
|
||||
"F4-002 through F4-004 establish the core state/action pipeline",
|
||||
"F4-005 and F4-006 create the page structure",
|
||||
"F4-007 through F4-012 add the Vue overlay components",
|
||||
"F4-013 and F4-014 complete the Phaser interaction layer",
|
||||
"Consider implementing a simple AI opponent for testing (backend task)",
|
||||
"Spectator mode is deferred to a future phase",
|
||||
"Animations are minimal in this phase - focus on functionality. Polish comes in F5."
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user