Includes core gameplay, 600 exported levels, visual assets, web bridge, and bootstrap scene. Co-authored-by: Cursor <cursoragent@cursor.com>
148 lines
5.6 KiB
TypeScript
148 lines
5.6 KiB
TypeScript
import {
|
|
Node, Sprite, SpriteFrame, UITransform, resources, Color, Graphics,
|
|
ImageAsset, Texture2D,
|
|
} from 'cc';
|
|
import { Direction } from '../core/Define';
|
|
import { SpawnKind } from '../level/LevelTypes';
|
|
|
|
type SpriteKey = 'player_F' | 'player_B' | 'ship_F' | 'ship_B' | 'coin' | 'tile';
|
|
|
|
const PATHS: Record<SpriteKey, string> = {
|
|
player_F: 'textures/silu/player_F',
|
|
player_B: 'textures/silu/player_B',
|
|
ship_F: 'textures/silu/ship_F',
|
|
ship_B: 'textures/silu/ship_B',
|
|
coin: 'textures/ui/coin',
|
|
tile: 'textures/silu/Baseblock',
|
|
};
|
|
|
|
export class VisualAssets {
|
|
private static frames = new Map<SpriteKey, SpriteFrame>();
|
|
private static loading: Promise<void> | null = null;
|
|
|
|
static async preload(): Promise<void> {
|
|
if (this.loading) return this.loading;
|
|
this.loading = (async () => {
|
|
const keys = Object.keys(PATHS) as SpriteKey[];
|
|
const results = await Promise.all(keys.map((k) => this.loadOne(k)));
|
|
const ok = results.filter(Boolean).length;
|
|
console.log(`[VisualAssets] 贴图加载 ${ok}/${keys.length}`);
|
|
if (ok === 0) {
|
|
console.warn('[VisualAssets] 未加载到贴图,将使用色块。请确认 assets/resources/textures 已导入');
|
|
}
|
|
})().catch((e) => {
|
|
console.error('[VisualAssets] preload failed', e);
|
|
this.loading = null;
|
|
});
|
|
return this.loading;
|
|
}
|
|
|
|
private static loadOne(key: SpriteKey): Promise<boolean> {
|
|
if (this.frames.has(key)) return Promise.resolve(true);
|
|
const base = PATHS[key];
|
|
return new Promise((resolve) => {
|
|
resources.load(`${base}/spriteFrame`, SpriteFrame, (err, sf) => {
|
|
if (!err && sf) {
|
|
this.frames.set(key, sf);
|
|
resolve(true);
|
|
return;
|
|
}
|
|
resources.load(base, SpriteFrame, (err2, sf2) => {
|
|
if (!err2 && sf2) {
|
|
this.frames.set(key, sf2);
|
|
resolve(true);
|
|
return;
|
|
}
|
|
resources.load(base, ImageAsset, (err3, img) => {
|
|
if (!err3 && img) {
|
|
const tex = new Texture2D();
|
|
tex.image = img;
|
|
const frame = new SpriteFrame();
|
|
frame.texture = tex;
|
|
this.frames.set(key, frame);
|
|
resolve(true);
|
|
} else {
|
|
console.warn(`[VisualAssets] 加载失败: ${base}`, err3 || err2 || err);
|
|
resolve(false);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
static getFrame(key: SpriteKey): SpriteFrame | null {
|
|
return this.frames.get(key) ?? null;
|
|
}
|
|
|
|
static applyPlayerSprite(node: Node, direction: Direction) {
|
|
const isFront = direction === Direction.South || direction === Direction.East;
|
|
const flipX = direction === Direction.West || direction === Direction.East;
|
|
const key: SpriteKey = isFront ? 'player_F' : 'player_B';
|
|
this.applySprite(node, key, flipX);
|
|
}
|
|
|
|
static applyVehicleSprite(node: Node, direction: Direction, uiStyle = 'default') {
|
|
void uiStyle;
|
|
const isFront = direction === Direction.South || direction === Direction.East;
|
|
const flipX = direction === Direction.West || direction === Direction.East;
|
|
const key: SpriteKey = isFront ? 'ship_F' : 'ship_B';
|
|
this.applySprite(node, key, flipX);
|
|
}
|
|
|
|
static setupEntityVisual(node: Node, kind: SpawnKind, direction?: Direction) {
|
|
if (kind === 'player') {
|
|
this.applyPlayerSprite(node, direction ?? Direction.South);
|
|
return;
|
|
}
|
|
if (kind === 'vehicle') {
|
|
this.applyVehicleSprite(node, direction ?? Direction.North);
|
|
return;
|
|
}
|
|
if (kind === 'prop') {
|
|
this.applySprite(node, 'coin', false, 0.85);
|
|
return;
|
|
}
|
|
if (kind === 'prop_decor') {
|
|
this.applySprite(node, 'coin', false, 0.6, 120);
|
|
}
|
|
}
|
|
|
|
/** Sprite 与 Graphics 不能共存,二选一 */
|
|
static applySprite(node: Node, key: SpriteKey, flipX: boolean, scale = 1, alpha = 255) {
|
|
let ui = node.getComponent(UITransform);
|
|
if (!ui) {
|
|
ui = node.addComponent(UITransform);
|
|
ui.setContentSize(48, 48);
|
|
}
|
|
|
|
const sf = this.getFrame(key);
|
|
const spr = node.getComponent(Sprite);
|
|
const g = node.getComponent(Graphics);
|
|
|
|
if (sf) {
|
|
if (g) g.destroy();
|
|
const sprite = spr || node.addComponent(Sprite);
|
|
sprite.spriteFrame = sf;
|
|
sprite.sizeMode = Sprite.SizeMode.CUSTOM;
|
|
const w = ui.contentSize.width * scale;
|
|
const h = ui.contentSize.height * scale;
|
|
ui.setContentSize(w, h);
|
|
sprite.color = new Color(255, 255, 255, alpha);
|
|
} else {
|
|
if (spr) spr.destroy();
|
|
const graphics = g || node.addComponent(Graphics);
|
|
graphics.fillColor = key === 'coin'
|
|
? new Color(255, 220, 0, alpha)
|
|
: new Color(80, 160, 255, alpha);
|
|
const w = ui.contentSize.width * 0.45 * scale;
|
|
graphics.clear();
|
|
graphics.rect(-w, -w, w * 2, w * 2);
|
|
graphics.fill();
|
|
}
|
|
|
|
const sx = flipX ? -Math.abs(scale) : Math.abs(scale);
|
|
node.setScale(sx, Math.abs(scale), 1);
|
|
}
|
|
}
|