6.4 KiB
Frontend POC - F3 Phaser Demo
This is a proof-of-concept implementation of the Mantimon TCG frontend through Phase F3 (Phaser Integration). It demonstrates Vue 3 + Phaser 3 integration with real card data.
What This POC Demonstrates
- Vue-Phaser Integration: Bidirectional communication between Vue components and Phaser scenes
- Card Rendering: Real card images with type-colored borders and damage counters
- State Synchronization: Game state flows from Vue to Phaser via the gameBridge
- Interactive Cards: Click and hover events propagate from Phaser back to Vue
Running the Demo
cd .claude/frontend-poc
npm install
npm run dev
Navigate to /demo after logging in (requires auth).
Key Lessons Learned
1. Scene Initialization Timing
Problem: PhaserGame.vue was calling createGame(container) without passing scenes, resulting in an empty canvas.
Solution: Import and pass the scenes array:
import { createGame, scenes } from '@/game'
game.value = createGame(container.value, scenes)
File: src/components/game/PhaserGame.vue
2. Event Bridge Timing (Critical)
Problem: DemoPage listened for PhaserGame's Vue ready event (Phaser core ready), but this fires BEFORE MatchScene's create() runs. When state was sent, MatchScene hadn't subscribed to events yet.
Solution: Listen for the gameBridge's ready event instead, which MatchScene emits at the END of create() after subscribing to events:
// WRONG - fires too early
function handlePhaserReady(game: Phaser.Game): void {
emitToBridge('state:updated', gameState.value) // MatchScene not ready!
}
// CORRECT - wait for MatchScene
onMounted(() => {
onBridgeEvent('ready', handleSceneReady)
})
function handleSceneReady(): void {
emitToBridge('state:updated', gameState.value) // MatchScene is subscribed
}
Files: src/pages/DemoPage.vue, src/game/scenes/MatchScene.ts
3. Card Image Loading
Problem: Card definitions have image_path field (e.g., "a1/094-pikachu.webp") but the loader was constructing paths from set_id and set_number.
Solution: Add loadCardImageFromPath() function that uses image_path directly:
// Card.ts - prefer image_path
if (this.cardDefinition.image_path) {
loadedKey = await loadCardImageFromPath(
this.scene,
textureKey,
this.cardDefinition.image_path
)
}
Files: src/game/assets/loader.ts, src/game/objects/Card.ts
4. Asset Base URL
Card images are served from /game/cards/{image_path}:
- Card back:
/game/cards/card_back.webp - Pokemon:
/game/cards/a1/094-pikachu.webp - Energy:
/game/cards/basic/lightning.webp
File: src/game/assets/manifest.ts (ASSET_BASE_URL = '/game')
Layout Issues (For Phase F4)
The demo revealed several layout issues that need fixing:
- Opponent cards upside down - Rotation not applied correctly for opponent's side
- Cards overlapping in zones - Zone arrangement logic needs work
- Hand cards cut off - Hand zone extends beyond viewport
- Zone positioning - Active/bench/pile positions need adjustment
- Card sizes - May need responsive sizing based on viewport
Architecture Overview
Vue Component (DemoPage/GamePage)
|
| emits 'state:updated'
v
gameBridge (mitt event emitter)
|
| subscribed in create()
v
MatchScene (Phaser Scene)
|
| delegates rendering
v
StateRenderer
|
| creates/updates
v
Zone objects (ActiveZone, BenchZone, HandZone, etc.)
|
| contain
v
Card objects (with image loading, damage counters)
Key Files
| File | Purpose |
|---|---|
src/game/bridge.ts |
GameBridge class - typed event emitter for Vue-Phaser communication |
src/composables/useGameBridge.ts |
Vue composable with auto-cleanup |
src/game/scenes/MatchScene.ts |
Main Phaser scene - subscribes to bridge events |
src/game/sync/StateRenderer.ts |
Converts VisibleGameState to Phaser objects |
src/game/objects/Card.ts |
Card game object with image loading |
src/game/objects/Zone.ts |
Base zone class for card containers |
src/game/layout.ts |
Board layout calculations |
State Flow
- Vue initializes state (mockGameState.ts or from WebSocket)
- Vue emits to bridge:
emitToBridge('state:updated', gameState) - MatchScene receives: Handler calls
stateRenderer.render(state) - StateRenderer processes:
- Creates zones if needed
- Updates zone positions from layout
- Creates/updates Card objects from state
- Sets cards face-up/face-down based on visibility
- User interacts with card (click/hover in Phaser)
- Card emits to bridge:
gameBridge.emit('card:clicked', {...}) - Vue receives: Handler updates UI (selected card, etc.)
Type Definitions
Key types are in src/types/game.ts:
interface VisibleGameState {
game_id: string
viewer_id: string
players: Record<string, VisiblePlayerState>
card_registry: Record<string, CardDefinition>
phase: TurnPhase
is_my_turn: boolean
// ...
}
interface CardDefinition {
id: string
name: string
card_type: 'pokemon' | 'trainer' | 'energy'
image_path?: string // Added for image loading
// ...
}
interface CardInstance {
instance_id: string
definition_id: string
damage: number
attached_energy: CardInstance[]
// ...
}
Testing the Demo
The demo page (/demo) provides:
- Debug Panel: Shows zone counts, game state, selected card
- Controls:
- Next Phase - cycles through turn phases
- Toggle Turn - switches between player/opponent turn
- +10 Damage buttons - adds damage to active Pokemon
- Reset State - returns to initial state
What's NOT in This POC
- Real WebSocket connection (uses mock state)
- Game actions (play card, attack, etc.)
- Animations for card movement
- Proper responsive scaling
- Prize card reveal
- Stadium cards
- Energy attachment display on cards
Recommendations for True Game Page
- Keep the bridge pattern - It works well for decoupling Vue and Phaser
- Fix layout before adding features - Get zone positions right first
- Add animation system - Cards should animate when moving between zones
- Implement action validation UI - Highlight valid targets before confirming
- Consider zone click handling - For dropping cards, not just card clicks
- Test on mobile early - Touch targets and scaling will need tuning