diff --git a/frontend/src/pages/GamePage.vue b/frontend/src/pages/GamePage.vue index 63331ae..1c613d7 100644 --- a/frontend/src/pages/GamePage.vue +++ b/frontend/src/pages/GamePage.vue @@ -3,10 +3,10 @@ * Game page - full viewport Phaser canvas for active matches. * * This page hosts the Phaser game instance and manages: - * - WebSocket connection to the game server - * - Game state synchronization between server and Phaser - * - Connection status overlays (reconnecting, disconnected) - * - Exit game confirmation + * - WebSocket connection to the game server via useGameSocket + * - Game state synchronization between server and Phaser via bridge + * - Connection status overlays (loading, reconnecting, error) + * - Exit game confirmation with resign option * * Uses the 'game' layout (no navigation, full viewport). */ @@ -14,9 +14,13 @@ import { ref, watch, onMounted, onUnmounted, computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import PhaserGame from '@/components/game/PhaserGame.vue' +import GameOverlay from '@/components/game/GameOverlay.vue' +import TurnIndicator from '@/components/game/TurnIndicator.vue' +import AttackMenu from '@/components/game/AttackMenu.vue' import { useGameStore } from '@/stores/game' +import { useGameSocket } from '@/composables/useGameSocket' import { useGameBridge } from '@/composables/useGameBridge' -import { socketClient, type ConnectionState } from '@/socket/client' +import { ConnectionStatus } from '@/types' import type Phaser from 'phaser' // Route and navigation @@ -26,6 +30,7 @@ const gameId = computed(() => route.params.id as string) // Stores and composables const gameStore = useGameStore() +const gameSocket = useGameSocket() const { emit: emitToBridge } = useGameBridge() // Component refs @@ -35,37 +40,31 @@ const phaserGame = ref(null) // Local state const isLoading = ref(true) const showExitConfirm = ref(false) -const connectionState = ref('disconnected') const errorMessage = ref(null) +const showAttackMenu = ref(false) -// Computed states -const isConnected = computed(() => connectionState.value === 'connected') -const isReconnecting = computed(() => connectionState.value === 'reconnecting') -const showConnectionOverlay = computed(() => - !isConnected.value && !isLoading.value +// Computed states from game store +const isReconnecting = computed(() => gameStore.connectionStatus === ConnectionStatus.RECONNECTING) +const isDisconnected = computed(() => gameStore.connectionStatus === ConnectionStatus.DISCONNECTED) +const showConnectionOverlay = computed(() => + (isReconnecting.value || isDisconnected.value) && !isLoading.value ) -// Event unsubscribe functions -const unsubscribers: (() => void)[] = [] - /** * Connect to the game server and join the game room. */ async function connectToGame(): Promise { isLoading.value = true errorMessage.value = null + gameStore.setError(null) try { - // Connect to WebSocket server - await socketClient.connect() - - // Subscribe to game events - setupGameEventHandlers() - - // Join the game room - socketClient.joinGame(gameId.value) - - gameStore.setConnected(true) + // Connect via the useGameSocket composable + await gameSocket.connect(gameId.value) + + // Wait for initial game state to be loaded + // The socket composable handles state updates automatically + isLoading.value = false } catch (error) { const message = error instanceof Error ? error.message : 'Failed to connect' errorMessage.value = message @@ -75,69 +74,14 @@ async function connectToGame(): Promise { } /** - * Set up handlers for game WebSocket events. - */ -function setupGameEventHandlers(): void { - // Connection state changes - const unsubConnection = socketClient.onConnectionStateChange((state) => { - connectionState.value = state - gameStore.setConnected(state === 'connected') - }) - unsubscribers.push(unsubConnection) - - // Game state updates - const unsubState = socketClient.onGameState((data) => { - gameStore.setGameState(data.state) - isLoading.value = false - errorMessage.value = null - }) - unsubscribers.push(unsubState) - - // Game errors - const unsubError = socketClient.onGameError((data) => { - errorMessage.value = data.message - gameStore.setError(data.message) - - // Handle specific error codes - if (data.code === 'game_not_found' || data.code === 'not_in_game') { - // Navigate away from invalid game - router.push({ name: 'PlayMenu' }) - } - }) - unsubscribers.push(unsubError) - - // Game over - const unsubGameOver = socketClient.onGameOver((data) => { - gameStore.setGameState(data.final_state) - // Game over UI handled by Phaser scene - }) - unsubscribers.push(unsubGameOver) - - // Opponent connection status - const unsubOpponent = socketClient.onOpponentConnected((data) => { - // Could show toast or indicator - console.log('[GamePage] Opponent status:', data) - }) - unsubscribers.push(unsubOpponent) -} - -/** - * Clean up WebSocket connections and handlers. + * Clean up WebSocket connection. */ function cleanup(): void { - // Unsubscribe from all events - unsubscribers.forEach(unsub => { - if (typeof unsub === 'function') { - unsub() - } - }) - unsubscribers.length = 0 - - // Clear game state - gameStore.clearGameState() - - // Disconnect socket - socketClient.disconnect() + // Disconnect socket (this also clears game state) + gameSocket.disconnect() + + // Clear any remaining error state + errorMessage.value = null } /** @@ -183,14 +127,79 @@ function cancelExit(): void { } /** - * Resign from the game. + * Resign from the game and exit. */ -function resignGame(): void { - socketClient.resign(gameId.value) - showExitConfirm.value = false +async function resignGame(): Promise { + try { + await gameSocket.sendResign() + showExitConfirm.value = false + // Give a moment for the server to process resignation + setTimeout(() => { + cleanup() + router.push({ name: 'PlayMenu' }) + }, 500) + } catch (error) { + console.error('[GamePage] Failed to resign:', error) + // Still exit even if resign fails + showExitConfirm.value = false + cleanup() + router.push({ name: 'PlayMenu' }) + } } -// Watch game state changes and sync to Phaser +/** + * Open the attack menu. + * + * Called when the player wants to perform an attack with their active Pokemon. + */ +function openAttackMenu(): void { + // Only allow opening during attack phase or main phase (for flexibility) + const phase = gameStore.currentPhase + if (!gameStore.isMyTurn || (phase !== 'attack' && phase !== 'main')) { + return + } + + showAttackMenu.value = true +} + +/** + * Close the attack menu. + */ +function closeAttackMenu(): void { + showAttackMenu.value = false +} + +/** + * Handle attack selection from the menu. + * + * @param attackIndex - Index of the selected attack + */ +function handleAttackSelected(attackIndex: number): void { + console.log('[GamePage] Attack selected:', attackIndex) + // Attack action is dispatched by AttackMenu component + // This handler can be used for additional UI state management if needed +} + +/** + * Handle when game state is loaded initially. + * + * Once we have the initial state, hide loading overlay. + */ +watch( + () => gameStore.gameState, + (newState) => { + if (newState && isLoading.value) { + isLoading.value = false + } + }, + { immediate: true } +) + +/** + * Watch game state changes and sync to Phaser via bridge. + * + * This keeps Phaser rendering in sync with server state updates. + */ watch( () => gameStore.gameState, (newState) => { @@ -201,9 +210,49 @@ watch( { deep: true } ) +/** + * Watch for game over and show appropriate UI. + */ +watch( + () => gameStore.isGameOver, + (isOver) => { + if (isOver) { + // Game over UI will be handled by GameOverModal component (F4-012) + // For now, just log it + console.log('[GamePage] Game over detected') + } + } +) + +/** + * Handle card clicked from Phaser. + * + * @param data - Card click event data + */ +function handleCardClicked(data: { instanceId: string; definitionId: string; zone: string; playerId: string }): void { + console.log('[GamePage] Card clicked:', data) + + // For now, just log the click + // TODO: Implement card action logic based on game phase and card zone + // - In setup phase: Select active/bench pokemon + // - In main phase: Play card from hand, attach energy, etc. + // - During attack selection: Select target +} + // Lifecycle onMounted(() => { connectToGame() + + // Listen for events from Phaser + const { on } = useGameBridge() + + // Handle card clicks + on('card:clicked', handleCardClicked) + + // Handle attack request from Phaser (e.g., when clicking active Pokemon) + on('attack:request', () => { + openAttackMenu() + }) }) onUnmounted(() => { @@ -224,6 +273,29 @@ onUnmounted(() => { @error="handlePhaserError" /> + + + + + + + + + + +