import { _decorator, Component, resources, SpriteFrame } from 'cc'; import { Card } from '../../core/cards/Card'; const { ccclass, property } = _decorator; /** Folder under `assets/resources` holding the 36 faces and the back. */ export const CARDS_DIR = 'cards'; const BACK_NAME = 'card_back'; /** * Turns a `Card` into the sprite frame that draws it. * * The frames are loaded as a folder out of the `resources` bundle rather than * being referenced one by one in the scene: thirty-seven inspector slots would * be miserable to maintain, and an auto-atlas (`.pac`) is only a build-time * packing instruction — it is not something that can be loaded and asked for a * frame by name at runtime. The `.pac` beside the images still does its job, * packing all thirty-seven into one texture so the table costs one draw call. * * The only contract between the rules and the art is `Card.assetName`. */ @ccclass('CardLibrary') export class CardLibrary extends Component { @property({ tooltip: 'Folder under assets/resources holding the card sprites.' }) directory = CARDS_DIR; private readonly _frames = new Map(); private readonly _missingReported = new Set(); private _ready = false; private _waiting: (() => void)[] = []; onLoad(): void { this.load(); } get ready(): boolean { return this._ready; } /** * Loads the folder. Bootstrap preloads the same folder before the lobby * opens, so in practice this resolves from cache within a frame. */ load(): void { if (this._ready) { return; } resources.loadDir(this.directory, SpriteFrame, (error, frames) => { if (error) { console.error(`[cards] could not load '${this.directory}'`, error); return; } for (const frame of frames) { this._frames.set(frame.name, frame); } this._ready = true; const waiting = this._waiting; this._waiting = []; for (const resolve of waiting) resolve(); }); } /** Runs `done` once the frames are available — immediately if they already are. */ whenReady(done: () => void): void { if (this._ready) { done(); } else { this._waiting.push(done); } } frameFor(card: Card): SpriteFrame | null { return this._frame(card.assetName); } get backFrame(): SpriteFrame | null { return this._frame(BACK_NAME); } private _frame(name: string): SpriteFrame | null { const frame = this._frames.get(name) ?? null; if (!frame && this._ready && !this._missingReported.has(name)) { this._missingReported.add(name); console.error(`[cards] '${name}' is missing — run 'npm run build:art'`); } return frame; } }