/** * 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 { // 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 { 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> { const results = new Map() 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() }