mantimon-tcg/frontend/project_plans/PHASE_F4_live_gameplay.json
Cal Corum 1a21d3d2d4 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>
2026-02-01 20:52:20 -06:00

823 lines
28 KiB
JSON

{
"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."
]
}