mantimon-tcg/frontend/src/game/assets/loader.ts
Cal Corum 2986eed142 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.
2026-01-31 21:58:26 -06:00

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()
}