Add F3 demo page with real card data and fix Phaser initialization
- Add /demo route with full game board demo using real card images - Fix PhaserGame.vue to pass scenes array to createGame() - Fix timing issue: listen for gameBridge ready event instead of Phaser core ready - Add card images for Lightning and Fire starter decks (24 Pokemon + 5 energy) - Add mockGameState.ts with realistic Lightning vs Fire matchup - Add demoCards.json/ts with card definitions from backend - Update Card.ts to use image_path from card definitions - Add loadCardImageFromPath() to asset loader for new image format - Update CardDefinition type with image_path and rarity fields Demo verifies: Vue-Phaser state sync, card rendering, damage counters, card click events, and debug controls. Layout issues noted for Phase F4.
1
frontend/.gitignore
vendored
@ -11,6 +11,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
*.vite
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
BIN
frontend/public/game/cards/a1/033-charmander.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
frontend/public/game/cards/a1/034-charmeleon.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
frontend/public/game/cards/a1/035-charizard.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/public/game/cards/a1/037-vulpix.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
frontend/public/game/cards/a1/039-growlithe.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
frontend/public/game/cards/a1/040-arcanine.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
frontend/public/game/cards/a1/042-ponyta.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
frontend/public/game/cards/a1/043-rapidash.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
frontend/public/game/cards/a1/044-magmar.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/public/game/cards/a1/094-pikachu.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
frontend/public/game/cards/a1/095-raichu.webp
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
frontend/public/game/cards/a1/097-magnemite.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/game/cards/a1/098-magneton.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
frontend/public/game/cards/a1/099-voltorb.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
frontend/public/game/cards/a1/100-electrode.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
frontend/public/game/cards/a1/101-electabuzz.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
frontend/public/game/cards/a1/105-blitzle.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
frontend/public/game/cards/a1/106-zebstrika.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
frontend/public/game/cards/a1/216-helix-fossil.webp
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
frontend/public/game/cards/a1/217-dome-fossil.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
frontend/public/game/cards/a1/218-old-amber.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
frontend/public/game/cards/a1/221-blaine.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
frontend/public/game/cards/a1/224-brock.webp
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
frontend/public/game/cards/a1/226-lt-surge.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/game/cards/basic/fire.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/game/cards/basic/grass.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/public/game/cards/basic/lightning.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/game/cards/basic/psychic.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/game/cards/basic/water.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/public/game/cards/card_back.webp
Normal file
|
After Width: | Height: | Size: 77 KiB |
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ref, onMounted, onUnmounted, shallowRef } from 'vue'
|
import { ref, onMounted, onUnmounted, shallowRef } from 'vue'
|
||||||
|
|
||||||
import { createGame } from '@/game'
|
import { createGame, scenes } from '@/game'
|
||||||
import type Phaser from 'phaser'
|
import type Phaser from 'phaser'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,7 +44,7 @@ function initGame(): void {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Create the Phaser game instance
|
// Create the Phaser game instance
|
||||||
game.value = createGame(container.value)
|
game.value = createGame(container.value, scenes)
|
||||||
|
|
||||||
// Listen for Phaser ready event (emitted when game is fully initialized)
|
// Listen for Phaser ready event (emitted when game is fully initialized)
|
||||||
// Using PHASER_READY_EVENT constant to avoid runtime Phaser dependency
|
// Using PHASER_READY_EVENT constant to avoid runtime Phaser dependency
|
||||||
|
|||||||
700
frontend/src/data/demoCards.json
Normal file
@ -0,0 +1,700 @@
|
|||||||
|
{
|
||||||
|
"a1-094-pikachu": {
|
||||||
|
"id": "a1-094-pikachu",
|
||||||
|
"name": "Pikachu",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 60,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Gnaw",
|
||||||
|
"cost": [
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"damage": 20,
|
||||||
|
"damage_display": "20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Mitsuhiro Arita",
|
||||||
|
"image_path": "a1/094-pikachu.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/094-pikachu.webp"
|
||||||
|
},
|
||||||
|
"a1-095-raichu": {
|
||||||
|
"id": "a1-095-raichu",
|
||||||
|
"name": "Raichu",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 100,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "stage_1",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "rare",
|
||||||
|
"evolves_from": "Pikachu",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Thunderbolt",
|
||||||
|
"cost": [
|
||||||
|
"lightning",
|
||||||
|
"lightning",
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"damage": 140,
|
||||||
|
"damage_display": "140",
|
||||||
|
"effect_description": "Discard all Energy from this Pok\u00e9mon."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "AKIRA EGAWA",
|
||||||
|
"image_path": "a1/095-raichu.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/095-raichu.webp"
|
||||||
|
},
|
||||||
|
"a1-097-magnemite": {
|
||||||
|
"id": "a1-097-magnemite",
|
||||||
|
"name": "Magnemite",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 60,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Lightning Ball",
|
||||||
|
"cost": [
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"damage": 20,
|
||||||
|
"damage_display": "20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "sowsow",
|
||||||
|
"image_path": "a1/097-magnemite.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/097-magnemite.webp"
|
||||||
|
},
|
||||||
|
"a1-098-magneton": {
|
||||||
|
"id": "a1-098-magneton",
|
||||||
|
"name": "Magneton",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 80,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "stage_1",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 2,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "rare",
|
||||||
|
"evolves_from": "Magnemite",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Spinning Attack",
|
||||||
|
"cost": [
|
||||||
|
"lightning",
|
||||||
|
"colorless",
|
||||||
|
"colorless",
|
||||||
|
"colorless"
|
||||||
|
],
|
||||||
|
"damage": 60,
|
||||||
|
"damage_display": "60"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abilities": [
|
||||||
|
{
|
||||||
|
"name": "Volt Charge",
|
||||||
|
"effect_id": "unimplemented",
|
||||||
|
"effect_description": "Once during your turn, you may take a Lightning Energy from your Energy Zone and attach it to this Pok\u00e9mon."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "kirisAki",
|
||||||
|
"image_path": "a1/098-magneton.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/098-magneton.webp"
|
||||||
|
},
|
||||||
|
"a1-099-voltorb": {
|
||||||
|
"id": "a1-099-voltorb",
|
||||||
|
"name": "Voltorb",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 60,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Tackle",
|
||||||
|
"cost": [
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"damage": 20,
|
||||||
|
"damage_display": "20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "SATOSHI NAKAI",
|
||||||
|
"image_path": "a1/099-voltorb.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/099-voltorb.webp"
|
||||||
|
},
|
||||||
|
"a1-100-electrode": {
|
||||||
|
"id": "a1-100-electrode",
|
||||||
|
"name": "Electrode",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 80,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "stage_1",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 0,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"evolves_from": "Voltorb",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Electro Ball",
|
||||||
|
"cost": [
|
||||||
|
"lightning",
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"damage": 70,
|
||||||
|
"damage_display": "70"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Asako Ito",
|
||||||
|
"image_path": "a1/100-electrode.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/100-electrode.webp"
|
||||||
|
},
|
||||||
|
"a1-101-electabuzz": {
|
||||||
|
"id": "a1-101-electabuzz",
|
||||||
|
"name": "Electabuzz",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 70,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Thunder Punch",
|
||||||
|
"cost": [
|
||||||
|
"lightning",
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"damage": 40,
|
||||||
|
"damage_display": "40x",
|
||||||
|
"effect_description": "Flip a coin. If heads, this attack does 40 more damage. If tails, this Pok\u00e9mon also does 20 damage to itself.",
|
||||||
|
"effect_params": {
|
||||||
|
"damage_modifier": "x"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Ryuta Fuse",
|
||||||
|
"image_path": "a1/101-electabuzz.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/101-electabuzz.webp"
|
||||||
|
},
|
||||||
|
"a1-105-blitzle": {
|
||||||
|
"id": "a1-105-blitzle",
|
||||||
|
"name": "Blitzle",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 60,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Zap Kick",
|
||||||
|
"cost": [
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"damage": 20,
|
||||||
|
"damage_display": "20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Shin Nagasawa",
|
||||||
|
"image_path": "a1/105-blitzle.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/105-blitzle.webp"
|
||||||
|
},
|
||||||
|
"a1-106-zebstrika": {
|
||||||
|
"id": "a1-106-zebstrika",
|
||||||
|
"name": "Zebstrika",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 90,
|
||||||
|
"pokemon_type": "lightning",
|
||||||
|
"stage": "stage_1",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"evolves_from": "Blitzle",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Thunder Spear",
|
||||||
|
"cost": [
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"damage": 0,
|
||||||
|
"effect_description": "This attack does 30 damage to 1 of your opponent\u2019s Pok\u00e9mon."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "fighting",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Misa Tsutsui",
|
||||||
|
"image_path": "a1/106-zebstrika.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/106-zebstrika.webp"
|
||||||
|
},
|
||||||
|
"a1-033-charmander": {
|
||||||
|
"id": "a1-033-charmander",
|
||||||
|
"name": "Charmander",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 60,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Ember",
|
||||||
|
"cost": [
|
||||||
|
"fire"
|
||||||
|
],
|
||||||
|
"damage": 30,
|
||||||
|
"damage_display": "30",
|
||||||
|
"effect_description": "Discard a Fire Energy from this Pok\u00e9mon."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Teeziro",
|
||||||
|
"image_path": "a1/033-charmander.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/033-charmander.webp"
|
||||||
|
},
|
||||||
|
"a1-034-charmeleon": {
|
||||||
|
"id": "a1-034-charmeleon",
|
||||||
|
"name": "Charmeleon",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 90,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "stage_1",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 2,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"evolves_from": "Charmander",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Fire Claws",
|
||||||
|
"cost": [
|
||||||
|
"fire",
|
||||||
|
"colorless",
|
||||||
|
"colorless"
|
||||||
|
],
|
||||||
|
"damage": 60,
|
||||||
|
"damage_display": "60"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "kantaro",
|
||||||
|
"image_path": "a1/034-charmeleon.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/034-charmeleon.webp"
|
||||||
|
},
|
||||||
|
"a1-035-charizard": {
|
||||||
|
"id": "a1-035-charizard",
|
||||||
|
"name": "Charizard",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 150,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "stage_2",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 2,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "rare",
|
||||||
|
"evolves_from": "Charmeleon",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Fire Spin",
|
||||||
|
"cost": [
|
||||||
|
"fire",
|
||||||
|
"fire",
|
||||||
|
"colorless",
|
||||||
|
"colorless"
|
||||||
|
],
|
||||||
|
"damage": 150,
|
||||||
|
"damage_display": "150",
|
||||||
|
"effect_description": "Discard 2 Fire Energy from this Pok\u00e9mon."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "takuyoa",
|
||||||
|
"image_path": "a1/035-charizard.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/035-charizard.webp"
|
||||||
|
},
|
||||||
|
"a1-039-growlithe": {
|
||||||
|
"id": "a1-039-growlithe",
|
||||||
|
"name": "Growlithe",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 70,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Bite",
|
||||||
|
"cost": [
|
||||||
|
"colorless",
|
||||||
|
"colorless"
|
||||||
|
],
|
||||||
|
"damage": 20,
|
||||||
|
"damage_display": "20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Mizue",
|
||||||
|
"image_path": "a1/039-growlithe.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/039-growlithe.webp"
|
||||||
|
},
|
||||||
|
"a1-040-arcanine": {
|
||||||
|
"id": "a1-040-arcanine",
|
||||||
|
"name": "Arcanine",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 130,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "stage_1",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 2,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "rare",
|
||||||
|
"evolves_from": "Growlithe",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Heat Tackle",
|
||||||
|
"cost": [
|
||||||
|
"fire",
|
||||||
|
"fire",
|
||||||
|
"colorless"
|
||||||
|
],
|
||||||
|
"damage": 100,
|
||||||
|
"damage_display": "100",
|
||||||
|
"effect_description": "This Pok\u00e9mon also does 20 damage to itself."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "kodama",
|
||||||
|
"image_path": "a1/040-arcanine.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/040-arcanine.webp"
|
||||||
|
},
|
||||||
|
"a1-042-ponyta": {
|
||||||
|
"id": "a1-042-ponyta",
|
||||||
|
"name": "Ponyta",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 60,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Flare",
|
||||||
|
"cost": [
|
||||||
|
"fire"
|
||||||
|
],
|
||||||
|
"damage": 20,
|
||||||
|
"damage_display": "20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Uta",
|
||||||
|
"image_path": "a1/042-ponyta.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/042-ponyta.webp"
|
||||||
|
},
|
||||||
|
"a1-043-rapidash": {
|
||||||
|
"id": "a1-043-rapidash",
|
||||||
|
"name": "Rapidash",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 100,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "stage_1",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"evolves_from": "Ponyta",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Fire Mane",
|
||||||
|
"cost": [
|
||||||
|
"fire"
|
||||||
|
],
|
||||||
|
"damage": 40,
|
||||||
|
"damage_display": "40"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Misa Tsutsui",
|
||||||
|
"image_path": "a1/043-rapidash.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/043-rapidash.webp"
|
||||||
|
},
|
||||||
|
"a1-037-vulpix": {
|
||||||
|
"id": "a1-037-vulpix",
|
||||||
|
"name": "Vulpix",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 50,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 1,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Tail Whip",
|
||||||
|
"cost": [
|
||||||
|
"colorless"
|
||||||
|
],
|
||||||
|
"damage": 0,
|
||||||
|
"effect_description": "Flip a coin. If heads, the Defending Pok\u00e9mon can\u2019t attack during your opponent\u2019s next turn."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Toshinao Aoki",
|
||||||
|
"image_path": "a1/037-vulpix.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/037-vulpix.webp"
|
||||||
|
},
|
||||||
|
"a1-044-magmar": {
|
||||||
|
"id": "a1-044-magmar",
|
||||||
|
"name": "Magmar",
|
||||||
|
"card_type": "pokemon",
|
||||||
|
"hp": 80,
|
||||||
|
"pokemon_type": "fire",
|
||||||
|
"stage": "basic",
|
||||||
|
"variant": "normal",
|
||||||
|
"retreat_cost": 2,
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Magma Punch",
|
||||||
|
"cost": [
|
||||||
|
"fire",
|
||||||
|
"fire"
|
||||||
|
],
|
||||||
|
"damage": 50,
|
||||||
|
"damage_display": "50"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weakness": {
|
||||||
|
"energy_type": "water",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
"illustrator": "Ryuta Fuse",
|
||||||
|
"image_path": "a1/044-magmar.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/044-magmar.webp"
|
||||||
|
},
|
||||||
|
"a1-221-blaine": {
|
||||||
|
"id": "a1-221-blaine",
|
||||||
|
"name": "Blaine",
|
||||||
|
"card_type": "trainer",
|
||||||
|
"trainer_type": "supporter",
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"effect_description": "During this turn, attacks used by your Ninetales , Rapidash , or Magmar do +30 damage to your opponent\u2019s Active Pok\u00e9mon. You may play only 1 Supporter card during your turn.",
|
||||||
|
"illustrator": "GOSSAN",
|
||||||
|
"image_path": "a1/221-blaine.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/221-blaine.webp"
|
||||||
|
},
|
||||||
|
"a1-224-brock": {
|
||||||
|
"id": "a1-224-brock",
|
||||||
|
"name": "Brock",
|
||||||
|
"card_type": "trainer",
|
||||||
|
"trainer_type": "supporter",
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"effect_description": "Take a Fighting Energy from your Energy Zone and attach it to Golem or Onix . You may play only 1 Supporter card during your turn.",
|
||||||
|
"illustrator": "Taira Akitsu",
|
||||||
|
"image_path": "a1/224-brock.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/224-brock.webp"
|
||||||
|
},
|
||||||
|
"a1-216-helix-fossil": {
|
||||||
|
"id": "a1-216-helix-fossil",
|
||||||
|
"name": "Helix Fossil",
|
||||||
|
"card_type": "trainer",
|
||||||
|
"trainer_type": "item",
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"effect_description": "Play this card as if it were a 40 HP Basic Colorless Pok\u00e9mon. At any time during your turn, you may discard this card from play. This card can\u2019t retreat. You may play any number of Item cards during your turn.",
|
||||||
|
"illustrator": "Toyste Beach",
|
||||||
|
"image_path": "a1/216-helix-fossil.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/216-helix-fossil.webp"
|
||||||
|
},
|
||||||
|
"a1-217-dome-fossil": {
|
||||||
|
"id": "a1-217-dome-fossil",
|
||||||
|
"name": "Dome Fossil",
|
||||||
|
"card_type": "trainer",
|
||||||
|
"trainer_type": "item",
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"effect_description": "Play this card as if it were a 40 HP Basic Colorless Pok\u00e9mon. At any time during your turn, you may discard this card from play. This card can\u2019t retreat. You may play any number of Item cards during your turn.",
|
||||||
|
"illustrator": "Toyste Beach",
|
||||||
|
"image_path": "a1/217-dome-fossil.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/217-dome-fossil.webp"
|
||||||
|
},
|
||||||
|
"a1-218-old-amber": {
|
||||||
|
"id": "a1-218-old-amber",
|
||||||
|
"name": "Old Amber",
|
||||||
|
"card_type": "trainer",
|
||||||
|
"trainer_type": "item",
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "common",
|
||||||
|
"effect_description": "Play this card as if it were a 40 HP Basic Colorless Pok\u00e9mon. At any time during your turn, you may discard this card from play. This card can\u2019t retreat. You may play any number of Item cards during your turn.",
|
||||||
|
"illustrator": "Toyste Beach",
|
||||||
|
"image_path": "a1/218-old-amber.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/218-old-amber.webp"
|
||||||
|
},
|
||||||
|
"a1-226-lt-surge": {
|
||||||
|
"id": "a1-226-lt-surge",
|
||||||
|
"name": "Lt. Surge",
|
||||||
|
"card_type": "trainer",
|
||||||
|
"trainer_type": "supporter",
|
||||||
|
"set_id": "a1",
|
||||||
|
"rarity": "uncommon",
|
||||||
|
"effect_description": "Move all Lightning Energy from your Benched Pok\u00e9mon to your Raichu , Electrode , or Electabuzz in the Active Spot. You may play only 1 Supporter card during your turn.",
|
||||||
|
"illustrator": "nagimiso",
|
||||||
|
"image_path": "a1/226-lt-surge.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/a1/226-lt-surge.webp"
|
||||||
|
},
|
||||||
|
"energy-basic-lightning": {
|
||||||
|
"id": "energy-basic-lightning",
|
||||||
|
"name": "Lightning Energy",
|
||||||
|
"card_type": "energy",
|
||||||
|
"energy_type": "lightning",
|
||||||
|
"energy_provides": [
|
||||||
|
"lightning"
|
||||||
|
],
|
||||||
|
"rarity": "common",
|
||||||
|
"set_id": "basic",
|
||||||
|
"image_path": "basic/lightning.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/basic/lightning.webp"
|
||||||
|
},
|
||||||
|
"energy-basic-fire": {
|
||||||
|
"id": "energy-basic-fire",
|
||||||
|
"name": "Fire Energy",
|
||||||
|
"card_type": "energy",
|
||||||
|
"energy_type": "fire",
|
||||||
|
"energy_provides": [
|
||||||
|
"fire"
|
||||||
|
],
|
||||||
|
"rarity": "common",
|
||||||
|
"set_id": "basic",
|
||||||
|
"image_path": "basic/fire.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/basic/fire.webp"
|
||||||
|
},
|
||||||
|
"energy-basic-water": {
|
||||||
|
"id": "energy-basic-water",
|
||||||
|
"name": "Water Energy",
|
||||||
|
"card_type": "energy",
|
||||||
|
"energy_type": "water",
|
||||||
|
"energy_provides": [
|
||||||
|
"water"
|
||||||
|
],
|
||||||
|
"rarity": "common",
|
||||||
|
"set_id": "basic",
|
||||||
|
"image_path": "basic/water.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/basic/water.webp"
|
||||||
|
},
|
||||||
|
"energy-basic-grass": {
|
||||||
|
"id": "energy-basic-grass",
|
||||||
|
"name": "Grass Energy",
|
||||||
|
"card_type": "energy",
|
||||||
|
"energy_type": "grass",
|
||||||
|
"energy_provides": [
|
||||||
|
"grass"
|
||||||
|
],
|
||||||
|
"rarity": "common",
|
||||||
|
"set_id": "basic",
|
||||||
|
"image_path": "basic/grass.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/basic/grass.webp"
|
||||||
|
},
|
||||||
|
"energy-basic-psychic": {
|
||||||
|
"id": "energy-basic-psychic",
|
||||||
|
"name": "Psychic Energy",
|
||||||
|
"card_type": "energy",
|
||||||
|
"energy_type": "psychic",
|
||||||
|
"energy_provides": [
|
||||||
|
"psychic"
|
||||||
|
],
|
||||||
|
"rarity": "common",
|
||||||
|
"set_id": "basic",
|
||||||
|
"image_path": "basic/psychic.webp",
|
||||||
|
"image_url": "https://mantipocket.s3.us-east-1.amazonaws.com/card-images/basic/psychic.webp"
|
||||||
|
}
|
||||||
|
}
|
||||||
75
frontend/src/data/demoCards.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Demo card data for F3 Phaser integration testing.
|
||||||
|
*
|
||||||
|
* Contains real card definitions from the backend, bundled statically
|
||||||
|
* for the demo page. Includes:
|
||||||
|
* - Lightning starter deck Pokemon (Pikachu line, Magnemite line, etc.)
|
||||||
|
* - Fire starter deck Pokemon (Charmander line, Growlithe line, etc.)
|
||||||
|
* - Trainer cards (supporters, items)
|
||||||
|
* - Basic energy cards
|
||||||
|
*
|
||||||
|
* Card images are served from /public/game/cards/ (copied from backend).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CardDefinition } from '@/types/game'
|
||||||
|
|
||||||
|
import demoCardsJson from './demoCards.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All demo card definitions, keyed by card ID.
|
||||||
|
*/
|
||||||
|
export const DEMO_CARD_DEFINITIONS: Record<string, CardDefinition> =
|
||||||
|
demoCardsJson as Record<string, CardDefinition>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a demo card definition by ID.
|
||||||
|
*
|
||||||
|
* @param id - The card ID (e.g., "a1-094-pikachu")
|
||||||
|
* @returns The card definition, or undefined if not found
|
||||||
|
*/
|
||||||
|
export function getDemoCard(id: string): CardDefinition | undefined {
|
||||||
|
return DEMO_CARD_DEFINITIONS[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all demo card IDs.
|
||||||
|
*
|
||||||
|
* @returns Array of all card IDs in the demo set
|
||||||
|
*/
|
||||||
|
export function getDemoCardIds(): string[] {
|
||||||
|
return Object.keys(DEMO_CARD_DEFINITIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get demo cards filtered by type.
|
||||||
|
*
|
||||||
|
* @param cardType - The card type to filter by
|
||||||
|
* @returns Array of matching card definitions
|
||||||
|
*/
|
||||||
|
export function getDemoCardsByType(
|
||||||
|
cardType: 'pokemon' | 'trainer' | 'energy'
|
||||||
|
): CardDefinition[] {
|
||||||
|
return Object.values(DEMO_CARD_DEFINITIONS).filter(
|
||||||
|
(card) => card.card_type === cardType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get demo Pokemon cards filtered by energy type.
|
||||||
|
*
|
||||||
|
* @param energyType - The Pokemon type to filter by
|
||||||
|
* @returns Array of matching Pokemon card definitions
|
||||||
|
*/
|
||||||
|
export function getDemoPokemonByType(
|
||||||
|
energyType: 'lightning' | 'fire' | 'water' | 'grass' | 'psychic'
|
||||||
|
): CardDefinition[] {
|
||||||
|
return Object.values(DEMO_CARD_DEFINITIONS).filter(
|
||||||
|
(card) => card.card_type === 'pokemon' && card.pokemon_type === energyType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-filtered card lists for easy access
|
||||||
|
export const LIGHTNING_POKEMON = getDemoPokemonByType('lightning')
|
||||||
|
export const FIRE_POKEMON = getDemoPokemonByType('fire')
|
||||||
|
export const TRAINER_CARDS = getDemoCardsByType('trainer')
|
||||||
|
export const ENERGY_CARDS = getDemoCardsByType('energy')
|
||||||
@ -133,43 +133,42 @@ function loadSingleAsset(scene: Phaser.Scene, asset: AssetDefinition): void {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Card image URL pattern.
|
* Build the URL for a card image from its image_path.
|
||||||
* Format: /game/cards/{setId}/{cardNumber}.png
|
|
||||||
*/
|
|
||||||
const CARD_IMAGE_PATTERN = 'cards/{setId}/{cardNumber}.png'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the URL for a card image.
|
|
||||||
*
|
*
|
||||||
* @param setId - The card set identifier (e.g., 'base', 'jungle')
|
* Card definitions include an `image_path` field like "a1/094-pikachu.webp"
|
||||||
* @param cardNumber - The card number within the set
|
* or "basic/lightning.webp". This function prepends the base cards path.
|
||||||
|
*
|
||||||
|
* @param imagePath - The image path from card definition
|
||||||
* @returns Full URL path to the card image
|
* @returns Full URL path to the card image
|
||||||
*/
|
*/
|
||||||
export function getCardImagePath(setId: string, cardNumber: string): string {
|
export function getCardImageUrl(imagePath: string): string {
|
||||||
return CARD_IMAGE_PATTERN.replace('{setId}', setId).replace(
|
return `${ASSET_BASE_URL}/cards/${imagePath}`
|
||||||
'{cardNumber}',
|
|
||||||
cardNumber
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily load a card image when needed.
|
* Legacy function for backwards compatibility.
|
||||||
|
* @deprecated Use getCardImageUrl with image_path instead
|
||||||
|
*/
|
||||||
|
export function getCardImagePath(setId: string, cardNumber: string): string {
|
||||||
|
return `cards/${setId}/${cardNumber}.webp`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily load a card image from its image_path.
|
||||||
*
|
*
|
||||||
* This function loads a specific card image on demand, rather than
|
* This function loads a specific card image on demand using the
|
||||||
* preloading all card images upfront. If the image fails to load,
|
* image_path from the card definition. If the image fails to load,
|
||||||
* it will fall back to the placeholder image.
|
* it will fall back to the placeholder image.
|
||||||
*
|
*
|
||||||
* @param scene - The Phaser scene to load the image into
|
* @param scene - The Phaser scene to load the image into
|
||||||
* @param cardId - Unique identifier for the card (used as texture key)
|
* @param cardId - Unique identifier for the card (used as texture key)
|
||||||
* @param setId - The card set identifier
|
* @param imagePath - The image_path from card definition (e.g., "a1/094-pikachu.webp")
|
||||||
* @param cardNumber - The card number within the set
|
|
||||||
* @returns Promise that resolves when the image is loaded (or fallback is used)
|
* @returns Promise that resolves when the image is loaded (or fallback is used)
|
||||||
*/
|
*/
|
||||||
export async function loadCardImage(
|
export async function loadCardImageFromPath(
|
||||||
scene: Phaser.Scene,
|
scene: Phaser.Scene,
|
||||||
cardId: string,
|
cardId: string,
|
||||||
setId: string,
|
imagePath: string
|
||||||
cardNumber: string
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Check if already loaded
|
// Check if already loaded
|
||||||
if (scene.textures.exists(cardId)) {
|
if (scene.textures.exists(cardId)) {
|
||||||
@ -177,8 +176,7 @@ export async function loadCardImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const imagePath = getCardImagePath(setId, cardNumber)
|
const fullUrl = getCardImageUrl(imagePath)
|
||||||
const fullUrl = `${ASSET_BASE_URL}/${imagePath}`
|
|
||||||
|
|
||||||
// Set up success handler
|
// Set up success handler
|
||||||
scene.load.once(`filecomplete-image-${cardId}`, () => {
|
scene.load.once(`filecomplete-image-${cardId}`, () => {
|
||||||
@ -199,6 +197,27 @@ export async function loadCardImage(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily load a card image when needed.
|
||||||
|
*
|
||||||
|
* @deprecated Use loadCardImageFromPath with image_path instead
|
||||||
|
*
|
||||||
|
* @param scene - The Phaser scene to load the image into
|
||||||
|
* @param cardId - Unique identifier for the card (used as texture key)
|
||||||
|
* @param setId - The card set identifier
|
||||||
|
* @param cardNumber - The card number within the set
|
||||||
|
* @returns Promise that resolves when the image is loaded (or fallback is used)
|
||||||
|
*/
|
||||||
|
export async function loadCardImage(
|
||||||
|
scene: Phaser.Scene,
|
||||||
|
cardId: string,
|
||||||
|
setId: string,
|
||||||
|
cardNumber: string
|
||||||
|
): Promise<string> {
|
||||||
|
const imagePath = `${setId}/${cardNumber}.webp`
|
||||||
|
return loadCardImageFromPath(scene, cardId, imagePath)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load multiple card images in parallel.
|
* Load multiple card images in parallel.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -86,13 +86,13 @@ export const ASSET_MANIFEST: AssetManifest = {
|
|||||||
{
|
{
|
||||||
key: 'card_back',
|
key: 'card_back',
|
||||||
type: 'image',
|
type: 'image',
|
||||||
path: 'cards/card_back.png',
|
path: 'cards/card_back.webp',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'placeholder_card',
|
key: 'placeholder_card',
|
||||||
type: 'image',
|
type: 'image',
|
||||||
path: 'cards/placeholder_card.png',
|
path: 'cards/card_back.webp', // Use card back as placeholder for now
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -31,8 +31,8 @@ import { CARD_SIZES } from '@/types/phaser'
|
|||||||
import { gameBridge } from '../bridge'
|
import { gameBridge } from '../bridge'
|
||||||
import { DamageCounter } from './DamageCounter'
|
import { DamageCounter } from './DamageCounter'
|
||||||
import {
|
import {
|
||||||
|
loadCardImageFromPath,
|
||||||
loadCardImage,
|
loadCardImage,
|
||||||
getCardTextureKey,
|
|
||||||
createPlaceholderTexture,
|
createPlaceholderTexture,
|
||||||
} from '../assets/loader'
|
} from '../assets/loader'
|
||||||
import { PLACEHOLDER_KEYS } from '../assets/manifest'
|
import { PLACEHOLDER_KEYS } from '../assets/manifest'
|
||||||
@ -434,6 +434,9 @@ export class Card extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load and display the card face image.
|
* Load and display the card face image.
|
||||||
|
*
|
||||||
|
* Uses image_path from card definition if available (preferred),
|
||||||
|
* otherwise falls back to constructing path from set_id and set_number.
|
||||||
*/
|
*/
|
||||||
private async loadCardFace(): Promise<void> {
|
private async loadCardFace(): Promise<void> {
|
||||||
if (!this.cardDefinition || this.isLoading) return
|
if (!this.cardDefinition || this.isLoading) return
|
||||||
@ -450,16 +453,26 @@ export class Card extends Phaser.GameObjects.Container {
|
|||||||
this.showLoadingIndicator()
|
this.showLoadingIndicator()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load the card image
|
let loadedKey: string
|
||||||
|
|
||||||
|
// Prefer image_path from card definition (new format)
|
||||||
|
if (this.cardDefinition.image_path) {
|
||||||
|
loadedKey = await loadCardImageFromPath(
|
||||||
|
this.scene,
|
||||||
|
textureKey,
|
||||||
|
this.cardDefinition.image_path
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Fallback to legacy set_id/set_number construction
|
||||||
const setId = this.cardDefinition.set_id || 'base'
|
const setId = this.cardDefinition.set_id || 'base'
|
||||||
const cardNumber = this.cardDefinition.set_number?.toString() || '001'
|
const cardNumber = this.cardDefinition.set_number?.toString() || '001'
|
||||||
|
loadedKey = await loadCardImage(
|
||||||
const loadedKey = await loadCardImage(
|
|
||||||
this.scene,
|
this.scene,
|
||||||
textureKey,
|
textureKey,
|
||||||
setId,
|
setId,
|
||||||
cardNumber
|
cardNumber
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Display the loaded image
|
// Display the loaded image
|
||||||
this.displayCardSprite(loadedKey)
|
this.displayCardSprite(loadedKey)
|
||||||
|
|||||||
461
frontend/src/pages/DemoPage.vue
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* F3 Demo Page - Testing Phaser integration with real card data.
|
||||||
|
*
|
||||||
|
* This page provides a full game board demo with real card data to test:
|
||||||
|
* - Board zone layout and positioning
|
||||||
|
* - Card rendering with actual images
|
||||||
|
* - Card interactions (hover, click)
|
||||||
|
* - State sync between Vue and Phaser
|
||||||
|
* - Responsive scaling
|
||||||
|
* - Damage counters
|
||||||
|
*
|
||||||
|
* Uses Lightning starter deck (player) vs Fire starter deck (opponent).
|
||||||
|
*/
|
||||||
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
|
|
||||||
|
import PhaserGame from '@/components/game/PhaserGame.vue'
|
||||||
|
import { useGameBridge } from '@/composables/useGameBridge'
|
||||||
|
import { createMockGameState, MOCK_CARD_DEFINITIONS } from '@/utils/mockGameState'
|
||||||
|
import type { VisibleGameState, TurnPhase } from '@/types/game'
|
||||||
|
import type Phaser from 'phaser'
|
||||||
|
|
||||||
|
// Phaser game refs
|
||||||
|
const phaserGameRef = ref<InstanceType<typeof PhaserGame> | null>(null)
|
||||||
|
const phaserGame = ref<Phaser.Game | null>(null)
|
||||||
|
const isReady = ref(false)
|
||||||
|
const initError = ref<string | null>(null)
|
||||||
|
|
||||||
|
// Game bridge for Vue-Phaser communication
|
||||||
|
const { emit: emitToBridge, on: onBridgeEvent } = useGameBridge()
|
||||||
|
|
||||||
|
// Mock game state
|
||||||
|
const gameState = ref<VisibleGameState | null>(null)
|
||||||
|
|
||||||
|
// Demo controls
|
||||||
|
const currentPhase = ref<TurnPhase>('main')
|
||||||
|
const isMyTurn = ref(true)
|
||||||
|
const showDebugPanel = ref(true)
|
||||||
|
const selectedCard = ref<{ instanceId: string; zone: string; name: string } | null>(null)
|
||||||
|
|
||||||
|
// Turn phases for cycling
|
||||||
|
const phases: TurnPhase[] = ['setup', 'draw', 'main', 'attack', 'end']
|
||||||
|
|
||||||
|
// Stats computed from state
|
||||||
|
const stats = computed(() => {
|
||||||
|
if (!gameState.value) return null
|
||||||
|
const myPlayer = Object.values(gameState.value.players).find(p => p.is_current_player)
|
||||||
|
const oppPlayer = Object.values(gameState.value.players).find(p => !p.is_current_player)
|
||||||
|
return {
|
||||||
|
myHand: myPlayer?.hand.count ?? 0,
|
||||||
|
myBench: myPlayer?.bench.cards.length ?? 0,
|
||||||
|
myDeck: myPlayer?.deck_count ?? 0,
|
||||||
|
myPrizes: myPlayer?.prizes_count ?? 0,
|
||||||
|
myActive: myPlayer?.active.cards[0]?.definition_id ?? 'None',
|
||||||
|
myActiveDamage: myPlayer?.active.cards[0]?.damage ?? 0,
|
||||||
|
oppHand: oppPlayer?.hand.count ?? 0,
|
||||||
|
oppBench: oppPlayer?.bench.cards.length ?? 0,
|
||||||
|
oppDeck: oppPlayer?.deck_count ?? 0,
|
||||||
|
oppPrizes: oppPlayer?.prizes_count ?? 0,
|
||||||
|
oppActive: oppPlayer?.active.cards[0]?.definition_id ?? 'None',
|
||||||
|
oppActiveDamage: oppPlayer?.active.cards[0]?.damage ?? 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize mock game state.
|
||||||
|
*/
|
||||||
|
function initializeState(): void {
|
||||||
|
gameState.value = createMockGameState({
|
||||||
|
phase: currentPhase.value,
|
||||||
|
isMyTurn: isMyTurn.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Phaser game ready event (from PhaserGame component).
|
||||||
|
* Note: This fires when Phaser core is ready, but scenes may not be created yet.
|
||||||
|
*/
|
||||||
|
function handlePhaserReady(game: Phaser.Game): void {
|
||||||
|
phaserGame.value = game
|
||||||
|
initError.value = null
|
||||||
|
console.log('[DemoPage] Phaser game instance ready')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle MatchScene ready event (from gameBridge).
|
||||||
|
* This fires after the MatchScene has subscribed to events.
|
||||||
|
*/
|
||||||
|
function handleSceneReady(): void {
|
||||||
|
isReady.value = true
|
||||||
|
console.log('[DemoPage] MatchScene ready, sending initial state')
|
||||||
|
|
||||||
|
// Send initial state to Phaser
|
||||||
|
if (gameState.value) {
|
||||||
|
emitToBridge('state:updated', gameState.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Phaser game error.
|
||||||
|
*/
|
||||||
|
function handlePhaserError(error: Error): void {
|
||||||
|
console.error('[DemoPage] Phaser error:', error)
|
||||||
|
initError.value = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle card click from Phaser.
|
||||||
|
*/
|
||||||
|
function handleCardClicked(data: {
|
||||||
|
instanceId: string
|
||||||
|
definitionId: string
|
||||||
|
zone: string
|
||||||
|
playerId: string
|
||||||
|
}): void {
|
||||||
|
console.log('[DemoPage] Card clicked:', data)
|
||||||
|
|
||||||
|
// Get card definition for display
|
||||||
|
const def = MOCK_CARD_DEFINITIONS[data.definitionId]
|
||||||
|
selectedCard.value = {
|
||||||
|
instanceId: data.instanceId,
|
||||||
|
zone: data.zone,
|
||||||
|
name: def?.name ?? data.definitionId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycle to next turn phase.
|
||||||
|
*/
|
||||||
|
function nextPhase(): void {
|
||||||
|
const currentIndex = phases.indexOf(currentPhase.value)
|
||||||
|
const nextIndex = (currentIndex + 1) % phases.length
|
||||||
|
currentPhase.value = phases[nextIndex]
|
||||||
|
updateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle whose turn it is.
|
||||||
|
*/
|
||||||
|
function toggleTurn(): void {
|
||||||
|
isMyTurn.value = !isMyTurn.value
|
||||||
|
updateState()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add damage to my active Pokemon.
|
||||||
|
*/
|
||||||
|
function addDamageToMine(): void {
|
||||||
|
if (!gameState.value) return
|
||||||
|
|
||||||
|
const myPlayer = Object.values(gameState.value.players).find(p => p.is_current_player)
|
||||||
|
if (!myPlayer || !myPlayer.active.cards[0]) return
|
||||||
|
|
||||||
|
myPlayer.active.cards[0].damage += 10
|
||||||
|
emitToBridge('state:updated', gameState.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add damage to opponent's active Pokemon.
|
||||||
|
*/
|
||||||
|
function addDamageToOpp(): void {
|
||||||
|
if (!gameState.value) return
|
||||||
|
|
||||||
|
const oppPlayer = Object.values(gameState.value.players).find(p => !p.is_current_player)
|
||||||
|
if (!oppPlayer || !oppPlayer.active.cards[0]) return
|
||||||
|
|
||||||
|
oppPlayer.active.cards[0].damage += 10
|
||||||
|
emitToBridge('state:updated', gameState.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset to initial state.
|
||||||
|
*/
|
||||||
|
function resetState(): void {
|
||||||
|
currentPhase.value = 'main'
|
||||||
|
isMyTurn.value = true
|
||||||
|
selectedCard.value = null
|
||||||
|
initializeState()
|
||||||
|
if (gameState.value) {
|
||||||
|
emitToBridge('state:updated', gameState.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update state and sync to Phaser.
|
||||||
|
*/
|
||||||
|
function updateState(): void {
|
||||||
|
if (!gameState.value) return
|
||||||
|
|
||||||
|
gameState.value.phase = currentPhase.value
|
||||||
|
gameState.value.is_my_turn = isMyTurn.value
|
||||||
|
gameState.value.current_player_id = isMyTurn.value
|
||||||
|
? gameState.value.viewer_id
|
||||||
|
: Object.keys(gameState.value.players).find(id => id !== gameState.value!.viewer_id) ?? ''
|
||||||
|
|
||||||
|
emitToBridge('state:updated', gameState.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for state changes
|
||||||
|
watch(
|
||||||
|
gameState,
|
||||||
|
(newState) => {
|
||||||
|
if (newState && isReady.value) {
|
||||||
|
emitToBridge('state:updated', newState)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
initializeState()
|
||||||
|
|
||||||
|
// Listen for MatchScene ready event (fires after scene subscribes to events)
|
||||||
|
onBridgeEvent('ready', handleSceneReady)
|
||||||
|
|
||||||
|
// Listen for card clicks from Phaser
|
||||||
|
onBridgeEvent('card:clicked', handleCardClicked)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-gray-900 overflow-hidden flex flex-col"
|
||||||
|
data-testid="demo-page"
|
||||||
|
>
|
||||||
|
<!-- Demo Header -->
|
||||||
|
<header
|
||||||
|
class="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between z-10 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<h1 class="text-lg font-bold text-white">
|
||||||
|
F3 Phaser Demo
|
||||||
|
</h1>
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 text-xs rounded"
|
||||||
|
:class="isReady ? 'bg-green-600 text-white' : 'bg-yellow-600 text-white'"
|
||||||
|
>
|
||||||
|
{{ isReady ? 'Ready' : 'Loading...' }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="initError"
|
||||||
|
class="px-2 py-1 text-xs rounded bg-red-600 text-white"
|
||||||
|
>
|
||||||
|
Error: {{ initError }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-1 text-sm bg-gray-700 hover:bg-gray-600 text-white rounded transition-colors"
|
||||||
|
@click="showDebugPanel = !showDebugPanel"
|
||||||
|
>
|
||||||
|
{{ showDebugPanel ? 'Hide' : 'Show' }} Debug
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="px-3 py-1 text-sm bg-gray-700 hover:bg-gray-600 text-white rounded transition-colors"
|
||||||
|
>
|
||||||
|
Exit Demo
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="flex-1 flex overflow-hidden min-h-0">
|
||||||
|
<!-- Phaser Game Canvas -->
|
||||||
|
<div class="flex-1 relative min-w-0">
|
||||||
|
<PhaserGame
|
||||||
|
ref="phaserGameRef"
|
||||||
|
class="w-full h-full"
|
||||||
|
@ready="handlePhaserReady"
|
||||||
|
@error="handlePhaserError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug Panel -->
|
||||||
|
<aside
|
||||||
|
v-if="showDebugPanel"
|
||||||
|
class="w-72 bg-gray-800 border-l border-gray-700 p-4 overflow-y-auto flex-shrink-0"
|
||||||
|
>
|
||||||
|
<!-- Controls -->
|
||||||
|
<section class="mb-6">
|
||||||
|
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
|
||||||
|
Controls
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-sm bg-blue-600 hover:bg-blue-500 text-white rounded transition-colors"
|
||||||
|
@click="nextPhase"
|
||||||
|
>
|
||||||
|
Next Phase ({{ currentPhase }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-sm bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors"
|
||||||
|
@click="toggleTurn"
|
||||||
|
>
|
||||||
|
Toggle Turn ({{ isMyTurn ? 'My Turn' : 'Opp Turn' }})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-sm bg-yellow-600 hover:bg-yellow-500 text-white rounded transition-colors"
|
||||||
|
@click="addDamageToMine"
|
||||||
|
>
|
||||||
|
+10 Damage (My Active)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-sm bg-red-600 hover:bg-red-500 text-white rounded transition-colors"
|
||||||
|
@click="addDamageToOpp"
|
||||||
|
>
|
||||||
|
+10 Damage (Opp Active)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-sm bg-gray-600 hover:bg-gray-500 text-white rounded transition-colors"
|
||||||
|
@click="resetState"
|
||||||
|
>
|
||||||
|
Reset State
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Game State -->
|
||||||
|
<section class="mb-6">
|
||||||
|
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
|
||||||
|
Game State
|
||||||
|
</h2>
|
||||||
|
<div class="text-sm text-gray-300 space-y-1">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Phase:</span>
|
||||||
|
<span class="text-white font-medium">{{ currentPhase }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Turn:</span>
|
||||||
|
<span class="text-white font-medium">{{ isMyTurn ? 'Mine' : 'Opponent' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Turn #:</span>
|
||||||
|
<span class="text-white font-medium">{{ gameState?.turn_number ?? 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Zone Counts -->
|
||||||
|
<section
|
||||||
|
v-if="stats"
|
||||||
|
class="mb-6"
|
||||||
|
>
|
||||||
|
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
|
||||||
|
Zone Counts
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-green-400 font-medium mb-1">
|
||||||
|
My Zones
|
||||||
|
</h3>
|
||||||
|
<div class="text-gray-300 space-y-1">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Active:</span>
|
||||||
|
<span class="text-xs truncate max-w-[80px]">{{ stats.myActive.split('-').pop() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Damage:</span>
|
||||||
|
<span :class="stats.myActiveDamage > 0 ? 'text-red-400' : ''">{{ stats.myActiveDamage }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Hand:</span>
|
||||||
|
<span>{{ stats.myHand }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Bench:</span>
|
||||||
|
<span>{{ stats.myBench }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Deck:</span>
|
||||||
|
<span>{{ stats.myDeck }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Prizes:</span>
|
||||||
|
<span>{{ stats.myPrizes }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-red-400 font-medium mb-1">
|
||||||
|
Opp Zones
|
||||||
|
</h3>
|
||||||
|
<div class="text-gray-300 space-y-1">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Active:</span>
|
||||||
|
<span class="text-xs truncate max-w-[80px]">{{ stats.oppActive.split('-').pop() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Damage:</span>
|
||||||
|
<span :class="stats.oppActiveDamage > 0 ? 'text-red-400' : ''">{{ stats.oppActiveDamage }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Hand:</span>
|
||||||
|
<span>{{ stats.oppHand }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Bench:</span>
|
||||||
|
<span>{{ stats.oppBench }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Deck:</span>
|
||||||
|
<span>{{ stats.oppDeck }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Prizes:</span>
|
||||||
|
<span>{{ stats.oppPrizes }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Selected Card -->
|
||||||
|
<section
|
||||||
|
v-if="selectedCard"
|
||||||
|
class="mb-6"
|
||||||
|
>
|
||||||
|
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
|
||||||
|
Selected Card
|
||||||
|
</h2>
|
||||||
|
<div class="text-sm text-gray-300 space-y-1">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Name:</span>
|
||||||
|
<span class="text-white font-medium">{{ selectedCard.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Zone:</span>
|
||||||
|
<span class="text-white font-medium">{{ selectedCard.zone }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 truncate">
|
||||||
|
{{ selectedCard.instanceId }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Instructions -->
|
||||||
|
<section>
|
||||||
|
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
|
||||||
|
Instructions
|
||||||
|
</h2>
|
||||||
|
<ul class="text-xs text-gray-400 space-y-1 list-disc list-inside">
|
||||||
|
<li>Click cards to select them</li>
|
||||||
|
<li>Hover over cards for scale effect</li>
|
||||||
|
<li>Use controls to modify game state</li>
|
||||||
|
<li>Resize window to test scaling</li>
|
||||||
|
<li>Opponent hand shows card backs</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -250,8 +250,18 @@ onUnmounted(() => {
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
<line
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
x1="18"
|
||||||
|
y1="6"
|
||||||
|
x2="6"
|
||||||
|
y2="18"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="6"
|
||||||
|
y1="6"
|
||||||
|
x2="18"
|
||||||
|
y2="18"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -268,7 +278,9 @@ onUnmounted(() => {
|
|||||||
data-testid="loading-overlay"
|
data-testid="loading-overlay"
|
||||||
>
|
>
|
||||||
<div class="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin mb-4" />
|
<div class="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin mb-4" />
|
||||||
<p class="text-lg">Connecting to game...</p>
|
<p class="text-lg">
|
||||||
|
Connecting to game...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
@ -286,19 +298,48 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<template v-if="isReconnecting">
|
<template v-if="isReconnecting">
|
||||||
<div class="w-8 h-8 border-2 border-warning border-t-transparent rounded-full animate-spin mb-4" />
|
<div class="w-8 h-8 border-2 border-warning border-t-transparent rounded-full animate-spin mb-4" />
|
||||||
<p class="text-lg text-warning">Reconnecting...</p>
|
<p class="text-lg text-warning">
|
||||||
<p class="text-sm text-muted mt-2">Please wait while we restore your connection</p>
|
Reconnecting...
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-muted mt-2">
|
||||||
|
Please wait while we restore your connection
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="text-error mb-4">
|
<div class="text-error mb-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-12 h-12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<circle cx="12" cy="12" r="10" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<line x1="12" y1="8" x2="12" y2="12" />
|
class="w-12 h-12"
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
y1="8"
|
||||||
|
x2="12"
|
||||||
|
y2="12"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
y1="16"
|
||||||
|
x2="12.01"
|
||||||
|
y2="16"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg text-error">Connection Lost</p>
|
<p class="text-lg text-error">
|
||||||
<p class="text-sm text-muted mt-2">{{ errorMessage || 'Unable to connect to game server' }}</p>
|
Connection Lost
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-muted mt-2">
|
||||||
|
{{ errorMessage || 'Unable to connect to game server' }}
|
||||||
|
</p>
|
||||||
<div class="flex gap-3 mt-6">
|
<div class="flex gap-3 mt-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -95,6 +95,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/pages/GamePage.vue'),
|
component: () => import('@/pages/GamePage.vue'),
|
||||||
meta: { requiresAuth: true, requiresStarter: true, layout: 'game' },
|
meta: { requiresAuth: true, requiresStarter: true, layout: 'game' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/demo',
|
||||||
|
name: 'Demo',
|
||||||
|
component: () => import('@/pages/DemoPage.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresStarter: false, layout: 'game' },
|
||||||
|
},
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Future Routes (Campaign - Phase 5+)
|
// Future Routes (Campaign - Phase 5+)
|
||||||
|
|||||||
@ -217,18 +217,22 @@ export interface WeaknessResistance {
|
|||||||
* Additional fields can be added as needed for new features.
|
* Additional fields can be added as needed for new features.
|
||||||
*/
|
*/
|
||||||
export interface CardDefinition {
|
export interface CardDefinition {
|
||||||
/** Unique card identifier (e.g., "pikachu_base_001") */
|
/** Unique card identifier (e.g., "a1-094-pikachu") */
|
||||||
id: string
|
id: string
|
||||||
/** Display name of the card */
|
/** Display name of the card */
|
||||||
name: string
|
name: string
|
||||||
/** Primary card type */
|
/** Primary card type */
|
||||||
card_type: CardType
|
card_type: CardType
|
||||||
/** URL to card image */
|
/** Local image path relative to cards directory (e.g., "a1/094-pikachu.webp") */
|
||||||
|
image_path?: string
|
||||||
|
/** Full URL to card image (e.g., S3 URL) */
|
||||||
image_url?: string
|
image_url?: string
|
||||||
/** Set identifier */
|
/** Set identifier */
|
||||||
set_id?: string
|
set_id?: string
|
||||||
/** Card number within the set */
|
/** Card number within the set */
|
||||||
set_number?: number
|
set_number?: number
|
||||||
|
/** Card rarity */
|
||||||
|
rarity?: string
|
||||||
|
|
||||||
// Pokemon-specific fields
|
// Pokemon-specific fields
|
||||||
/** Evolution stage */
|
/** Evolution stage */
|
||||||
|
|||||||
279
frontend/src/utils/mockGameState.ts
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
/**
|
||||||
|
* Mock game state generator for F3 demo and testing.
|
||||||
|
*
|
||||||
|
* Uses REAL card data from the backend (bundled in demoCards.json).
|
||||||
|
* Creates a realistic Lightning vs Fire matchup for testing:
|
||||||
|
* - My side: Lightning starter deck (Pikachu, Magnemite, Voltorb lines)
|
||||||
|
* - Opponent: Fire starter deck (Charmander, Growlithe, Ponyta lines)
|
||||||
|
*
|
||||||
|
* Card images are served locally from /public/game/cards/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
VisibleGameState,
|
||||||
|
VisiblePlayerState,
|
||||||
|
CardDefinition,
|
||||||
|
CardInstance,
|
||||||
|
TurnPhase,
|
||||||
|
} from '@/types/game'
|
||||||
|
import { DEMO_CARD_DEFINITIONS } from '@/data/demoCards'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Card Registry
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card definitions for the demo.
|
||||||
|
* Uses real card data from the backend.
|
||||||
|
*/
|
||||||
|
export const MOCK_CARD_DEFINITIONS: Record<string, CardDefinition> = DEMO_CARD_DEFINITIONS
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Card Instance Factory
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
let instanceCounter = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the instance counter.
|
||||||
|
* Useful for creating deterministic states in tests.
|
||||||
|
*/
|
||||||
|
export function resetInstanceCounter(): void {
|
||||||
|
instanceCounter = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a card instance from a definition ID.
|
||||||
|
*
|
||||||
|
* @param definitionId - The card definition ID (e.g., "a1-094-pikachu")
|
||||||
|
* @param options - Optional overrides for instance properties
|
||||||
|
* @returns A CardInstance
|
||||||
|
*/
|
||||||
|
export function createMockCardInstance(
|
||||||
|
definitionId: string,
|
||||||
|
options: Partial<CardInstance> = {}
|
||||||
|
): CardInstance {
|
||||||
|
instanceCounter++
|
||||||
|
return {
|
||||||
|
instance_id: options.instance_id ?? `inst_${instanceCounter}`,
|
||||||
|
definition_id: definitionId,
|
||||||
|
damage: options.damage ?? 0,
|
||||||
|
attached_energy: options.attached_energy ?? [],
|
||||||
|
attached_tools: options.attached_tools ?? [],
|
||||||
|
status_conditions: options.status_conditions ?? [],
|
||||||
|
ability_uses_this_turn: options.ability_uses_this_turn ?? {},
|
||||||
|
evolution_turn: options.evolution_turn ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Player State Factory
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default card configurations for each player.
|
||||||
|
*/
|
||||||
|
const LIGHTNING_PLAYER_CONFIG = {
|
||||||
|
activeCard: 'a1-094-pikachu',
|
||||||
|
benchCards: ['a1-097-magnemite', 'a1-099-voltorb', 'a1-101-electabuzz'],
|
||||||
|
handCards: [
|
||||||
|
'a1-095-raichu',
|
||||||
|
'a1-226-lt-surge',
|
||||||
|
'energy-basic-lightning',
|
||||||
|
'energy-basic-lightning',
|
||||||
|
'a1-100-electrode',
|
||||||
|
],
|
||||||
|
attachedEnergy: ['energy-basic-lightning'],
|
||||||
|
energyZone: ['energy-basic-lightning', 'energy-basic-lightning', 'energy-basic-lightning'],
|
||||||
|
discardPile: ['a1-224-brock'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIRE_PLAYER_CONFIG = {
|
||||||
|
activeCard: 'a1-035-charizard',
|
||||||
|
benchCards: ['a1-039-growlithe', 'a1-037-vulpix'],
|
||||||
|
handCards: [
|
||||||
|
'a1-040-arcanine',
|
||||||
|
'a1-221-blaine',
|
||||||
|
'energy-basic-fire',
|
||||||
|
'energy-basic-fire',
|
||||||
|
],
|
||||||
|
attachedEnergy: ['energy-basic-fire', 'energy-basic-fire'],
|
||||||
|
energyZone: ['energy-basic-fire', 'energy-basic-fire', 'energy-basic-fire'],
|
||||||
|
discardPile: ['a1-042-ponyta'],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock player state with realistic card data.
|
||||||
|
*
|
||||||
|
* @param playerId - The player's unique ID
|
||||||
|
* @param isViewer - Whether this is the viewing player (determines visibility)
|
||||||
|
* @param config - Optional card configuration overrides
|
||||||
|
* @returns A VisiblePlayerState
|
||||||
|
*/
|
||||||
|
export function createMockPlayerState(
|
||||||
|
playerId: string,
|
||||||
|
isViewer: boolean,
|
||||||
|
config?: {
|
||||||
|
activeCard?: string
|
||||||
|
benchCards?: string[]
|
||||||
|
handCards?: string[]
|
||||||
|
attachedEnergy?: string[]
|
||||||
|
energyZone?: string[]
|
||||||
|
discardPile?: string[]
|
||||||
|
activeDamage?: number
|
||||||
|
benchDamage?: number
|
||||||
|
}
|
||||||
|
): VisiblePlayerState {
|
||||||
|
// Use Lightning config for viewer, Fire for opponent
|
||||||
|
const defaultConfig = isViewer ? LIGHTNING_PLAYER_CONFIG : FIRE_PLAYER_CONFIG
|
||||||
|
const finalConfig = { ...defaultConfig, ...config }
|
||||||
|
|
||||||
|
// Create active Pokemon with attached energy
|
||||||
|
const activeInstance = createMockCardInstance(finalConfig.activeCard, {
|
||||||
|
damage: config?.activeDamage ?? (isViewer ? 0 : 30),
|
||||||
|
attached_energy: finalConfig.attachedEnergy.map((defId) =>
|
||||||
|
createMockCardInstance(defId)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create bench Pokemon
|
||||||
|
const benchInstances = finalConfig.benchCards.map((defId) =>
|
||||||
|
createMockCardInstance(defId, {
|
||||||
|
damage: config?.benchDamage ?? (isViewer ? 0 : 10),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create hand cards
|
||||||
|
const handInstances = finalConfig.handCards.map((defId) =>
|
||||||
|
createMockCardInstance(defId)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create energy zone cards
|
||||||
|
const energyZoneInstances = finalConfig.energyZone.map((defId) =>
|
||||||
|
createMockCardInstance(defId)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create discard pile
|
||||||
|
const discardInstances = finalConfig.discardPile.map((defId) =>
|
||||||
|
createMockCardInstance(defId)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
player_id: playerId,
|
||||||
|
is_current_player: isViewer,
|
||||||
|
deck_count: isViewer ? 32 : 35,
|
||||||
|
hand: {
|
||||||
|
count: handInstances.length,
|
||||||
|
cards: handInstances,
|
||||||
|
zone_type: 'hand',
|
||||||
|
},
|
||||||
|
prizes_count: isViewer ? 6 : 5,
|
||||||
|
energy_deck_count: isViewer ? 17 : 17,
|
||||||
|
active: {
|
||||||
|
count: 1,
|
||||||
|
cards: [activeInstance],
|
||||||
|
zone_type: 'active',
|
||||||
|
},
|
||||||
|
bench: {
|
||||||
|
count: benchInstances.length,
|
||||||
|
cards: benchInstances,
|
||||||
|
zone_type: 'bench',
|
||||||
|
},
|
||||||
|
discard: {
|
||||||
|
count: discardInstances.length,
|
||||||
|
cards: discardInstances,
|
||||||
|
zone_type: 'discard',
|
||||||
|
},
|
||||||
|
energy_zone: {
|
||||||
|
count: energyZoneInstances.length,
|
||||||
|
cards: energyZoneInstances,
|
||||||
|
zone_type: 'energy_zone',
|
||||||
|
},
|
||||||
|
score: 0,
|
||||||
|
gx_attack_used: false,
|
||||||
|
vstar_power_used: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Game State Factory
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface MockGameStateOptions {
|
||||||
|
/** Current turn phase */
|
||||||
|
phase?: TurnPhase
|
||||||
|
/** Whether it's the viewer's turn */
|
||||||
|
isMyTurn?: boolean
|
||||||
|
/** Current turn number */
|
||||||
|
turnNumber?: number
|
||||||
|
/** Viewer's player ID */
|
||||||
|
myPlayerId?: string
|
||||||
|
/** Opponent's player ID */
|
||||||
|
oppPlayerId?: string
|
||||||
|
/** Custom player config overrides */
|
||||||
|
myConfig?: Parameters<typeof createMockPlayerState>[2]
|
||||||
|
oppConfig?: Parameters<typeof createMockPlayerState>[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a complete mock game state.
|
||||||
|
*
|
||||||
|
* Creates a realistic Lightning vs Fire matchup with:
|
||||||
|
* - My side (Lightning): Pikachu active, bench with Magnemite/Voltorb/Electabuzz
|
||||||
|
* - Opponent (Fire): Charizard active (with 30 damage), bench with Growlithe/Vulpix
|
||||||
|
*
|
||||||
|
* @param options - Configuration options
|
||||||
|
* @returns A VisibleGameState ready for Phaser rendering
|
||||||
|
*/
|
||||||
|
export function createMockGameState(
|
||||||
|
options: MockGameStateOptions = {}
|
||||||
|
): VisibleGameState {
|
||||||
|
const {
|
||||||
|
phase = 'main',
|
||||||
|
isMyTurn = true,
|
||||||
|
turnNumber = 3,
|
||||||
|
myPlayerId = 'player_me',
|
||||||
|
oppPlayerId = 'player_opp',
|
||||||
|
myConfig,
|
||||||
|
oppConfig,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
// Reset counter for consistent instance IDs
|
||||||
|
resetInstanceCounter()
|
||||||
|
|
||||||
|
const myPlayer = createMockPlayerState(myPlayerId, true, myConfig)
|
||||||
|
const oppPlayer = createMockPlayerState(oppPlayerId, false, oppConfig)
|
||||||
|
|
||||||
|
return {
|
||||||
|
game_id: 'demo_game_001',
|
||||||
|
viewer_id: myPlayerId,
|
||||||
|
players: {
|
||||||
|
[myPlayerId]: myPlayer,
|
||||||
|
[oppPlayerId]: oppPlayer,
|
||||||
|
},
|
||||||
|
current_player_id: isMyTurn ? myPlayerId : oppPlayerId,
|
||||||
|
turn_number: turnNumber,
|
||||||
|
phase,
|
||||||
|
is_my_turn: isMyTurn,
|
||||||
|
winner_id: null,
|
||||||
|
end_reason: null,
|
||||||
|
stadium_in_play: null,
|
||||||
|
stadium_owner_id: null,
|
||||||
|
forced_action_player: null,
|
||||||
|
forced_action_type: null,
|
||||||
|
forced_action_reason: null,
|
||||||
|
card_registry: MOCK_CARD_DEFINITIONS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Exports
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export default {
|
||||||
|
MOCK_CARD_DEFINITIONS,
|
||||||
|
resetInstanceCounter,
|
||||||
|
createMockCardInstance,
|
||||||
|
createMockPlayerState,
|
||||||
|
createMockGameState,
|
||||||
|
}
|
||||||