Newer
Older
rpg / packages / engine / src / assets / AssetLoader.ts
import { Assets, Spritesheet, Texture } from 'pixi.js';

/**
 * Загрузчик ассетов поверх Assets из PixiJS: группы, прогресс, готовые Texture.
 * URL ассетов передаётся функцией-резолвером (приложение решает, где лежат файлы).
 * Атласы (Spritesheet JSON + PNG) загружаются тем же механизмом: loadAtlas().
 */
export class AssetLoader {
    private textures = new Map<string, Texture>();
    private atlases = new Map<string, Spritesheet>();
    private initPromise: Promise<void> | null = null;

    constructor(private resolveUrl: (key: string) => string) {}

    /** Assets требует однократной инициализации до первой загрузки. */
    private ensureInit(): Promise<void> {
        if (!this.initPromise) {
            this.initPromise = Assets.init({});
        }
        return this.initPromise;
    }

    /** Загрузить группу ключей; onProgress получает долю 0..1. */
    async load(keys: string[], onProgress?: (p: number) => void): Promise<void> {
        await this.ensureInit();
        let done = 0;
        for (const key of keys) {
            try {
                const tex = await Assets.load<Texture>(this.resolveUrl(key));
                this.textures.set(key, tex);
            } catch (err) {
                throw new Error(`Не удалось загрузить ${this.resolveUrl(key)}: ${String(err)}`);
            }
            done++;
            onProgress?.(done / keys.length);
        }
    }

    /**
     * Загрузить атлас (Spritesheet JSON). JSON должен ссылаться на PNG
     * относительным путём — Pixi подгрузит картинку сам.
     */
    async loadAtlas(key: string): Promise<Spritesheet> {
        await this.ensureInit();
        const existing = this.atlases.get(key);
        if (existing) return existing;
        const sheet = await Assets.load<Spritesheet>(this.resolveUrl(key));
        this.atlases.set(key, sheet);
        return sheet;
    }

    /**
     * Кадры анимации из загруженного атласа: все кадры, чьё имя начинается
     * с prefix, отсортированные по числу в конце имени (hero_walk_1, hero_walk_2, ...).
     */
    frames(key: string, prefix: string): Texture[] {
        const sheet = this.atlases.get(key);
        if (!sheet) throw new Error(`Атлас не загружен: ${key}`);
        const entries = Object.keys(sheet.textures)
            .filter((name) => name.startsWith(prefix))
            .sort((a, b) => {
                const na = Number(a.match(/(\d+)$/)?.[1] ?? -1);
                const nb = Number(b.match(/(\d+)$/)?.[1] ?? -1);
                return na - nb;
            });
        if (entries.length === 0) {
            throw new Error(`В атласе ${key} нет кадров с префиксом ${prefix}`);
        }
        return entries.map((name) => sheet.textures[name]!);
    }

    /** Готовая анимация из sheet.animations (если генератор заполняет animations). */
    animation(key: string, name: string): Texture[] {
        const sheet = this.atlases.get(key);
        if (!sheet) throw new Error(`Атлас не загружен: ${key}`);
        const frames = sheet.animations[name];
        if (!frames) throw new Error(`В атласе ${key} нет анимации ${name}`);
        return frames;
    }

    texture(key: string): Texture {
        const t = this.textures.get(key);
        if (!t) throw new Error(`Текстура не загружена: ${key}`);
        return t;
    }

    has(key: string): boolean {
        return this.textures.has(key);
    }
}