Complete Cocos Creator port with level bundles, themes, and tooling.

Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 15:30:58 +08:00
parent cba5105908
commit d393302388
6248 changed files with 17322729 additions and 11036 deletions

View File

@@ -0,0 +1,123 @@
import { AudioClip, AudioSource, director, Node, resources } from 'cc';
/** Unity Assets/Art/Audio → resources/audio/ */
const CLIPS = {
background: 'audio/Backgroud',
move: 'audio/Move',
jump: 'audio/Jump',
vehicleMove: 'audio/FlyingCarpetMove',
fail: 'audio/Fail',
success: 'audio/Success',
coins: 'audio/GetCoins',
} as const;
type SfxKey = Exclude<keyof typeof CLIPS, 'background'>;
/**
* 对齐 Unity关卡加载时 curLevel 上循环播放 Backgroud.mp3
* 音效路径与 Player.prefab 一致。
*/
export class GameAudio {
private static readonly cache = new Map<string, AudioClip>();
private static readonly loading = new Map<string, Promise<AudioClip | null>>();
private static sfxHost: Node | null = null;
static async preload(): Promise<void> {
await Promise.all(Object.values(CLIPS).map((p) => GameAudio.loadClip(p)));
}
static loadClip(path: string): Promise<AudioClip | null> {
const hit = GameAudio.cache.get(path);
if (hit) return Promise.resolve(hit);
const pending = GameAudio.loading.get(path);
if (pending) return pending;
const task = new Promise<AudioClip | null>((resolve) => {
resources.load(path, AudioClip, (err, clip) => {
GameAudio.loading.delete(path);
if (err || !clip) {
console.warn(`[GameAudio] 加载失败: ${path}`, err);
resolve(null);
return;
}
GameAudio.cache.set(path, clip);
resolve(clip);
});
});
GameAudio.loading.set(path, task);
return task;
}
/** 在关卡根节点播放循环背景音乐(对齐 Unity createNewLevel */
static async playBackground(levelRoot: Node): Promise<void> {
if (!levelRoot?.isValid) return;
const clip = await GameAudio.loadClip(CLIPS.background);
if (!clip || !levelRoot.isValid) return;
let host = levelRoot.getChildByName('_BGM');
if (!host) {
host = new Node('_BGM');
host.parent = levelRoot;
}
const src = host.getComponent(AudioSource) ?? host.addComponent(AudioSource);
src.clip = clip;
src.loop = true;
src.playOnAwake = false;
src.volume = 1;
if (!src.playing) {
src.play();
}
}
static playSfx(key: SfxKey, host?: Node) {
void GameAudio.playSfxAsync(key, host);
}
/** 对齐 Unity同一 AudioSource 播放中则不重复触发移动/跳跃音效 */
static async playSfxOnSource(src: AudioSource, key: SfxKey): Promise<boolean> {
if (!src?.node?.isValid || src.playing) return false;
const clip = await GameAudio.loadClip(CLIPS[key]);
if (!clip) return false;
src.clip = clip;
src.loop = false;
src.volume = 1;
src.play();
return true;
}
private static async playSfxAsync(key: SfxKey, host?: Node) {
const clip = await GameAudio.loadClip(CLIPS[key]);
if (!clip) return;
const root = host?.isValid ? host : GameAudio.ensureSfxHost();
if (!root?.isValid) return;
const src = root.getComponent(AudioSource) ?? root.addComponent(AudioSource);
src.playOneShot(clip, 1);
}
private static ensureSfxHost(): Node | null {
if (GameAudio.sfxHost?.isValid) return GameAudio.sfxHost;
const scene = director.getScene();
if (!scene) return null;
let host = scene.getChildByName('_GameSFX');
if (!host) {
host = new Node('_GameSFX');
host.parent = scene;
}
GameAudio.sfxHost = host;
return host;
}
/** 浏览器需用户交互后才能播放音频,首次点击 HUD 时恢复 */
static resumeAll() {
const scene = director.getScene();
if (!scene) return;
for (const src of scene.getComponentsInChildren(AudioSource)) {
if (src.clip && !src.playing) src.play();
}
}
}