- 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.
327 lines
8.9 KiB
TypeScript
327 lines
8.9 KiB
TypeScript
/**
|
|
* Asset loader utilities for Phaser.
|
|
*
|
|
* This module provides helper functions for loading game assets,
|
|
* including the main preload function and lazy card image loading.
|
|
* It handles missing assets gracefully with fallback placeholders.
|
|
*/
|
|
|
|
import type Phaser from 'phaser'
|
|
|
|
import {
|
|
ASSET_MANIFEST,
|
|
ASSET_BASE_URL,
|
|
PLACEHOLDER_KEYS,
|
|
getAllAssets,
|
|
type AssetDefinition,
|
|
type AssetManifest,
|
|
} from './manifest'
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Options for loading assets.
|
|
*/
|
|
export interface LoadOptions {
|
|
/** Only load required assets (for faster initial load) */
|
|
requiredOnly?: boolean
|
|
/** Callback for load progress (0-1) */
|
|
onProgress?: (progress: number) => void
|
|
/** Callback when load completes */
|
|
onComplete?: () => void
|
|
/** Callback for load errors */
|
|
onError?: (file: Phaser.Loader.File) => void
|
|
}
|
|
|
|
/**
|
|
* Result of checking if an asset is loaded.
|
|
*/
|
|
export interface AssetStatus {
|
|
key: string
|
|
loaded: boolean
|
|
exists: boolean
|
|
}
|
|
|
|
// =============================================================================
|
|
// Main Loader Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Load all assets from the manifest into a Phaser scene.
|
|
*
|
|
* This function queues all assets defined in the manifest for loading.
|
|
* The actual loading happens when scene.load.start() is called or
|
|
* automatically during scene.preload().
|
|
*
|
|
* @param scene - The Phaser scene to load assets into
|
|
* @param manifest - Optional custom manifest (uses default if not provided)
|
|
* @param options - Loading options
|
|
*/
|
|
export function loadAssets(
|
|
scene: Phaser.Scene,
|
|
manifest: AssetManifest = ASSET_MANIFEST,
|
|
options: LoadOptions = {}
|
|
): void {
|
|
const { requiredOnly = false, onProgress, onComplete, onError } = options
|
|
|
|
// Set base path for all loads
|
|
scene.load.setBaseURL(manifest.baseUrl)
|
|
|
|
// Get assets to load
|
|
const assets = requiredOnly
|
|
? getAllAssets().filter((a) => a.required)
|
|
: getAllAssets()
|
|
|
|
// Queue each asset
|
|
for (const asset of assets) {
|
|
loadSingleAsset(scene, asset)
|
|
}
|
|
|
|
// Set up progress callback
|
|
if (onProgress) {
|
|
scene.load.on('progress', (value: number) => {
|
|
onProgress(value)
|
|
})
|
|
}
|
|
|
|
// Set up completion callback
|
|
if (onComplete) {
|
|
scene.load.on('complete', () => {
|
|
onComplete()
|
|
})
|
|
}
|
|
|
|
// Set up error handling
|
|
if (onError) {
|
|
scene.load.on('loaderror', (file: Phaser.Loader.File) => {
|
|
onError(file)
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a single asset based on its type.
|
|
*/
|
|
function loadSingleAsset(scene: Phaser.Scene, asset: AssetDefinition): void {
|
|
switch (asset.type) {
|
|
case 'image':
|
|
scene.load.image(asset.key, asset.path)
|
|
break
|
|
case 'spritesheet':
|
|
if (asset.frameConfig) {
|
|
scene.load.spritesheet(asset.key, asset.path, asset.frameConfig)
|
|
}
|
|
break
|
|
case 'atlas':
|
|
// Atlas requires both image and JSON paths
|
|
scene.load.atlas(
|
|
asset.key,
|
|
asset.path,
|
|
asset.path.replace('.png', '.json')
|
|
)
|
|
break
|
|
case 'audio':
|
|
scene.load.audio(asset.key, asset.path)
|
|
break
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Card Image Loading
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Build the URL for a card image from its image_path.
|
|
*
|
|
* Card definitions include an `image_path` field like "a1/094-pikachu.webp"
|
|
* 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
|
|
*/
|
|
export function getCardImageUrl(imagePath: string): string {
|
|
return `${ASSET_BASE_URL}/cards/${imagePath}`
|
|
}
|
|
|
|
/**
|
|
* 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 using the
|
|
* image_path from the card definition. If the image fails to load,
|
|
* it will fall back to the placeholder image.
|
|
*
|
|
* @param scene - The Phaser scene to load the image into
|
|
* @param cardId - Unique identifier for the card (used as texture key)
|
|
* @param imagePath - The image_path from card definition (e.g., "a1/094-pikachu.webp")
|
|
* @returns Promise that resolves when the image is loaded (or fallback is used)
|
|
*/
|
|
export async function loadCardImageFromPath(
|
|
scene: Phaser.Scene,
|
|
cardId: string,
|
|
imagePath: string
|
|
): Promise<string> {
|
|
// Check if already loaded
|
|
if (scene.textures.exists(cardId)) {
|
|
return cardId
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const fullUrl = getCardImageUrl(imagePath)
|
|
|
|
// Set up success handler
|
|
scene.load.once(`filecomplete-image-${cardId}`, () => {
|
|
resolve(cardId)
|
|
})
|
|
|
|
// Set up error handler - use placeholder on failure
|
|
scene.load.once('loaderror', (file: Phaser.Loader.File) => {
|
|
if (file.key === cardId) {
|
|
console.warn(`Failed to load card image: ${fullUrl}, using placeholder`)
|
|
resolve(PLACEHOLDER_KEYS.CARD)
|
|
}
|
|
})
|
|
|
|
// Queue the load
|
|
scene.load.image(cardId, fullUrl)
|
|
scene.load.start()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param scene - The Phaser scene to load images into
|
|
* @param cards - Array of card info objects
|
|
* @returns Promise that resolves when all images are loaded
|
|
*/
|
|
export async function loadCardImages(
|
|
scene: Phaser.Scene,
|
|
cards: Array<{ id: string; setId: string; cardNumber: string }>
|
|
): Promise<Map<string, string>> {
|
|
const results = new Map<string, string>()
|
|
|
|
const loadPromises = cards.map(async (card) => {
|
|
const textureKey = await loadCardImage(
|
|
scene,
|
|
card.id,
|
|
card.setId,
|
|
card.cardNumber
|
|
)
|
|
results.set(card.id, textureKey)
|
|
})
|
|
|
|
await Promise.all(loadPromises)
|
|
return results
|
|
}
|
|
|
|
// =============================================================================
|
|
// Utility Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Check if an asset is loaded in the scene.
|
|
*
|
|
* @param scene - The Phaser scene to check
|
|
* @param key - The asset key to check
|
|
* @returns Whether the asset texture exists
|
|
*/
|
|
export function isAssetLoaded(scene: Phaser.Scene, key: string): boolean {
|
|
return scene.textures.exists(key)
|
|
}
|
|
|
|
/**
|
|
* Get the texture key to use for a card, with fallback.
|
|
*
|
|
* @param scene - The Phaser scene
|
|
* @param cardId - The card's texture key
|
|
* @returns The card's texture key if loaded, or placeholder key
|
|
*/
|
|
export function getCardTextureKey(scene: Phaser.Scene, cardId: string): string {
|
|
if (scene.textures.exists(cardId)) {
|
|
return cardId
|
|
}
|
|
return PLACEHOLDER_KEYS.CARD
|
|
}
|
|
|
|
/**
|
|
* Get the card back texture key.
|
|
*
|
|
* @param scene - The Phaser scene
|
|
* @returns The card back texture key if loaded, or placeholder
|
|
*/
|
|
export function getCardBackTextureKey(scene: Phaser.Scene): string {
|
|
if (scene.textures.exists(PLACEHOLDER_KEYS.CARD_BACK)) {
|
|
return PLACEHOLDER_KEYS.CARD_BACK
|
|
}
|
|
return PLACEHOLDER_KEYS.CARD
|
|
}
|
|
|
|
/**
|
|
* Create a placeholder texture programmatically.
|
|
*
|
|
* This is used as a last resort if even the placeholder image fails to load.
|
|
*
|
|
* @param scene - The Phaser scene
|
|
* @param key - The texture key to create
|
|
* @param width - Texture width
|
|
* @param height - Texture height
|
|
* @param color - Fill color (hex number)
|
|
*/
|
|
export function createPlaceholderTexture(
|
|
scene: Phaser.Scene,
|
|
key: string,
|
|
width: number = 100,
|
|
height: number = 140,
|
|
color: number = 0x2d3748
|
|
): void {
|
|
if (scene.textures.exists(key)) {
|
|
return
|
|
}
|
|
|
|
const graphics = scene.add.graphics()
|
|
graphics.fillStyle(color, 1)
|
|
graphics.fillRoundedRect(0, 0, width, height, 8)
|
|
graphics.lineStyle(2, 0x4a5568, 1)
|
|
graphics.strokeRoundedRect(0, 0, width, height, 8)
|
|
|
|
// Add a simple pattern to indicate placeholder
|
|
graphics.lineStyle(1, 0x4a5568, 0.5)
|
|
for (let i = 0; i < height; i += 10) {
|
|
graphics.lineBetween(0, i, width, i)
|
|
}
|
|
|
|
graphics.generateTexture(key, width, height)
|
|
graphics.destroy()
|
|
}
|