import Phaser from 'phaser'; export type UnitDirection = 'south' | 'east' | 'north' | 'west'; const unitIdleFrameCount = 8; const unitWalkFrameCount = 8; export const unitBaseFramesPerDirection = unitIdleFrameCount + unitWalkFrameCount; export const unitSheetFrameSize = 313; const unitWalkFrameRate = 10; const unitSheetRows: Record = { south: 0, east: 1, north: 2, west: 3 }; type UnitSheetAsset = { key: string; url: string; actionKey: string; actionUrl: string; }; const unitSheetModules = import.meta.glob('../../assets/images/units/unit-*.png', { eager: true, import: 'default' }) as Record; function textureKeyFromPath(path: string) { const fileName = path.split('/').pop() ?? path; return fileName.replace(/\.png$/i, ''); } const unitSheetUrls = Object.fromEntries( Object.entries(unitSheetModules).map(([path, url]) => [textureKeyFromPath(path), url]) ); const unitSheets = Object.entries(unitSheetUrls) .filter(([key]) => !key.endsWith('-actions')) .map(([key, url]) => ({ key, url, actionKey: `${key}-actions`, actionUrl: unitSheetUrls[`${key}-actions`] })) .filter((sheet): sheet is UnitSheetAsset => Boolean(sheet.actionUrl)) .sort((left, right) => left.key.localeCompare(right.key)); const unitSheetAssetsByKey = new Map(unitSheets.map((sheet) => [sheet.key, sheet])); const pendingUnitTextureKeys = new Set(); export function unitTextureVariantKeys(baseKey: string) { const prefix = `${baseKey}-`; const keys = unitSheets .map((sheet) => sheet.key) .filter((key) => key === baseKey || key.startsWith(prefix)) .sort(); return keys.length > 0 ? keys : [baseKey]; } export function hasUnitSheetAsset(key: string) { return unitSheetAssetsByKey.has(key); } export function unitBaseSheetsReady(scene: Phaser.Scene, keys: Iterable) { return Array.from(keys).every((key) => scene.textures.exists(key)); } export function waitForUnitBaseSheets(scene: Phaser.Scene, keys: Iterable, onReady: () => void) { waitForUnitBaseSheetsAttempt(scene, Array.from(new Set(keys)), onReady); } export function loadUnitBaseSheets(scene: Phaser.Scene, keys: Iterable, onReady: () => void) { const uniqueKeys = Array.from(new Set(keys)); if (unitBaseSheetsReady(scene, uniqueKeys)) { onReady(); return; } const pendingLoads: Array<{ key: string; url: string }> = []; uniqueKeys.forEach((key) => { const sheet = unitSheetAssetsByKey.get(key) ?? missingUnitSheet(key); if (!scene.textures.exists(sheet.key) && !pendingUnitTextureKeys.has(sheet.key)) { pendingUnitTextureKeys.add(sheet.key); pendingLoads.push({ key: sheet.key, url: sheet.url }); } }); if (pendingLoads.length <= 0) { waitForUnitBaseSheets(scene, uniqueKeys, onReady); return; } const clearPendingLoads = () => { pendingLoads.forEach(({ key }) => pendingUnitTextureKeys.delete(key)); }; scene.load.once('complete', () => { clearPendingLoads(); const missing = uniqueKeys.filter((key) => !scene.textures.exists(key)); if (missing.length > 0) { console.warn(`Failed to load unit base sheet textures: ${missing.join(', ')}`); } onReady(); }); scene.load.once('loaderror', clearPendingLoads); pendingLoads.forEach(({ key, url }) => { scene.load.spritesheet(key, url, { frameWidth: unitSheetFrameSize, frameHeight: unitSheetFrameSize }); }); scene.load.start(); } export function loadUnitSheets(scene: Phaser.Scene, keys: Iterable, onReady: () => void) { const uniqueKeys = Array.from(new Set(keys)); if (unitSheetsReady(scene, uniqueKeys)) { onReady(); return; } const pendingLoads: Array<{ key: string; url: string }> = []; uniqueKeys.forEach((key) => { const sheet = unitSheetAssetsByKey.get(key) ?? missingUnitSheet(key); if (!scene.textures.exists(sheet.key) && !pendingUnitTextureKeys.has(sheet.key)) { pendingUnitTextureKeys.add(sheet.key); pendingLoads.push({ key: sheet.key, url: sheet.url }); } if (!scene.textures.exists(sheet.actionKey) && !pendingUnitTextureKeys.has(sheet.actionKey)) { pendingUnitTextureKeys.add(sheet.actionKey); pendingLoads.push({ key: sheet.actionKey, url: sheet.actionUrl }); } }); if (pendingLoads.length <= 0) { waitForUnitSheets(scene, uniqueKeys, onReady); return; } const clearPendingLoads = () => { pendingLoads.forEach(({ key }) => pendingUnitTextureKeys.delete(key)); }; scene.load.once('complete', () => { clearPendingLoads(); waitForUnitSheets(scene, uniqueKeys, onReady); }); scene.load.once('loaderror', clearPendingLoads); pendingLoads.forEach(({ key, url }) => { scene.load.spritesheet(key, url, { frameWidth: unitSheetFrameSize, frameHeight: unitSheetFrameSize }); }); scene.load.start(); } function unitSheetsReady(scene: Phaser.Scene, keys: Iterable) { return Array.from(keys).every((key) => scene.textures.exists(key) && scene.textures.exists(`${key}-actions`)); } function waitForUnitBaseSheetsAttempt(scene: Phaser.Scene, keys: string[], onReady: () => void, attempts = 0) { if (unitBaseSheetsReady(scene, keys)) { onReady(); return; } if (attempts > 200) { const missing = keys.filter((key) => !scene.textures.exists(key)); console.warn(`Timed out waiting for unit base sheet textures: ${missing.join(', ')}`); onReady(); return; } scene.time.delayedCall(50, () => waitForUnitBaseSheetsAttempt(scene, keys, onReady, attempts + 1)); } function waitForUnitSheets(scene: Phaser.Scene, keys: string[], onReady: () => void, attempts = 0) { if (unitSheetsReady(scene, keys)) { onReady(); return; } if (attempts > 200) { const missing = keys.filter((key) => !scene.textures.exists(key) || !scene.textures.exists(`${key}-actions`)); throw new Error(`Timed out waiting for unit sheet textures: ${missing.join(', ')}`); } scene.time.delayedCall(50, () => waitForUnitSheets(scene, keys, onReady, attempts + 1)); } export function ensureUnitAnimations(scene: Phaser.Scene, keys: Iterable) { Array.from(new Set(keys)).forEach((key) => { if (!scene.textures.exists(key)) { return; } (['south', 'east', 'north', 'west'] as UnitDirection[]).forEach((direction) => { const frames = Array.from({ length: unitWalkFrameCount }, (_, frameIndex) => ({ key, frame: unitSheetRows[direction] * unitBaseFramesPerDirection + unitIdleFrameCount + frameIndex })); const idleFrames = [ { key, frame: unitSheetRows[direction] * unitBaseFramesPerDirection } ]; const walkAnimationKey = `${key}-walk-${direction}`; const idleAnimationKey = `${key}-idle-${direction}`; if (!scene.anims.exists(walkAnimationKey)) { scene.anims.create({ key: walkAnimationKey, frames, frameRate: unitWalkFrameRate, repeat: -1 }); } if (!scene.anims.exists(idleAnimationKey)) { scene.anims.create({ key: idleAnimationKey, frames: idleFrames, frameRate: 1, repeat: -1 }); } }); }); } function missingUnitSheet(key: string): never { throw new Error(`Missing unit sheet asset for ${key}`); }