Add card click event handling in GamePage

- Listen for card:clicked events from Phaser
- Log card clicks to console for debugging
- Add TODO for implementing game action logic based on phase

Cards now respond to clicks and emit events to Vue layer.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-02-01 20:50:35 -06:00
parent fdb5225356
commit a20e97a0a0

View File

@ -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<Phaser.Game | null>(null)
// Local state
const isLoading = ref(true)
const showExitConfirm = ref(false)
const connectionState = ref<ConnectionState>('disconnected')
const errorMessage = ref<string | null>(null)
const showAttackMenu = ref(false)
// Computed states
const isConnected = computed(() => connectionState.value === 'connected')
const isReconnecting = computed(() => connectionState.value === 'reconnecting')
// Computed states from game store
const isReconnecting = computed(() => gameStore.connectionStatus === ConnectionStatus.RECONNECTING)
const isDisconnected = computed(() => gameStore.connectionStatus === ConnectionStatus.DISCONNECTED)
const showConnectionOverlay = computed(() =>
!isConnected.value && !isLoading.value
(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<void> {
isLoading.value = true
errorMessage.value = null
gameStore.setError(null)
try {
// Connect to WebSocket server
await socketClient.connect()
// Connect via the useGameSocket composable
await gameSocket.connect(gameId.value)
// Subscribe to game events
setupGameEventHandlers()
// Join the game room
socketClient.joinGame(gameId.value)
gameStore.setConnected(true)
// 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<void> {
}
/**
* 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
// Disconnect socket (this also clears game state)
gameSocket.disconnect()
// Clear game state
gameStore.clearGameState()
// Disconnect socket
socketClient.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<void> {
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"
/>
<!-- Game UI Overlays (positioned over Phaser canvas) -->
<GameOverlay v-if="!isLoading && gameStore.gameState">
<!-- F4-008: Turn Indicator -->
<template #turn-indicator>
<TurnIndicator />
</template>
<!-- F4-010: Attack Menu -->
<template #attack-menu>
<AttackMenu
:show="showAttackMenu"
@close="closeAttackMenu"
@attack-selected="handleAttackSelected"
/>
</template>
<!-- Future overlay components:
- F4-009: PhaseActions in phase-actions slot
- F4-011: ForcedActionModal in forced-action slot
- F4-012: GameOverModal in game-over slot
-->
</GameOverlay>
<!-- Exit Button (top-right corner) -->
<button
type="button"