import { _decorator, AssetManager, assetManager, AudioClip, AudioSource, CCInteger, Collider2D, Component, find, Node, Vec3 } from 'cc'; import { SuDaceMap } from './SuDaceMap'; import { SuDaceEnemy } from './SuDaceEnemy'; const { ccclass, property } = _decorator; @ccclass('SuDace') export class SuDace extends Component { @property autoSpawn = true; /** 5 波触发时间(秒):A 波建议填 0 */ @property({ type: [CCInteger], displayName: "波次时间" }) waveTimes: number[] = [0, 4, 9, 15, 22]; /** 5 波出怪数:对应 A,C,E,G,I */ @property({ type: [CCInteger], displayName: "波次出怪数" }) waveCounts: number[] = [6, 8, 10, 12, 15]; @property({ displayName: "怪物血量" }) enemyHp = 3; @property({ displayName: "怪物移动速度" }) enemyMoveSpeed = 100; @property({ displayName: "武器攻击力" }) weaponAttack = 1; static instance: SuDace = null; /** 玩家阵亡后设为 true,停止刷怪等逻辑 */ gameFailed = false; map: SuDaceMap = null; enemyPool: Node[] = []; bulletPool: Node[] = []; stonePool: Node[] = []; private elapsed = 0; private nextWaveIndex = 0; private spawnedCount = 0; private killedCount = 0; public levelCleared = false; btnBack: Node = null; audioSource: AudioSource = null; audioKeys: string[] = ["开宝箱", "受击", "攻击"]; audios: Map = new Map(); protected onLoad(): void { SuDace.instance = this; this.map = find("map", this.node).getComponent(SuDaceMap); this.btnBack = find("btnBack", this.node); this.audioSource = this.node.getComponent(AudioSource); if (!this.audioSource) { this.audioSource = this.node.addComponent(AudioSource); } this.loadAudio(); } protected onEnable(): void { this.btnBack.on(Node.EventType.TOUCH_END, this.click_btnBack, this); } protected onDisable(): void { this.btnBack.off(Node.EventType.TOUCH_END, this.click_btnBack, this); } click_btnBack(): void { this.node.emit("_onMiniGameClose", false); } protected onDestroy(): void { // 只清理当前实例,避免“旧实例延迟销毁”把新实例误清空 if (SuDace.instance === this) { SuDace.instance = null; } } protected start(): void { this.restartStage(); } onPlayerDefeated(): void { if (this.gameFailed) return; this.gameFailed = true; this.node.emit("_onMiniGameEnd", false); console.log('[SuDace] game over — player defeated'); } protected update(dt: number): void { if (this.gameFailed) return; if (!SuDace.instance) { return; } if (!this.autoSpawn || !this.map?.enemyNode) return; this.elapsed += dt; // 按波次时间触发,可连续 5 波;时间和数量都允许为 0 while (this.nextWaveIndex < this.getWaveLimit()) { const triggerTime = this.getWaveTime(this.nextWaveIndex); if (this.elapsed < triggerTime) break; const count = this.getWaveCount(this.nextWaveIndex); if (count > 0) { for (let i = 0; i < count; i++) { const m = this.map.generateMonster(); if (!m) { console.warn('[SuDace] generateMonster returned null.'); break; } this.setupEnemy(m); this.spawnedCount++; } } this.nextWaveIndex++; } // 通关:5 波都触发完,且所有已生成怪物都被击杀 if (!this.levelCleared && this.nextWaveIndex >= this.getWaveLimit() && this.killedCount >= this.spawnedCount && this.map.enemyNode.children.length === 0) { this.levelCleared = true; console.log('[SuDace] stage clear'); this.node.emit("_onMiniGameEnd", true); } } private restartStage(): void { this.elapsed = 0; this.nextWaveIndex = 0; this.spawnedCount = 0; this.killedCount = 0; this.levelCleared = false; this.gameFailed = false; this.map?.player?.resetForStage(); console.log(`[SuDace] stage start, totalSpawn=${this.getTotalPlannedCount()}`); } private getWaveLimit(): number { // 最多 5 波 const n = Math.max(this.waveTimes.length, this.waveCounts.length); return Math.min(5, n); } private getWaveTime(index: number): number { const t = this.waveTimes[index] ?? 0; return Math.max(0, t); } private getWaveCount(index: number): number { const c = this.waveCounts[index] ?? 0; return Math.max(0, Math.floor(c)); } private getTotalPlannedCount(): number { let sum = 0; const n = this.getWaveLimit(); for (let i = 0; i < n; i++) sum += this.getWaveCount(i); return sum; } private setupEnemy(monster: Node): void { const enemy = monster.getComponent(SuDaceEnemy) ?? monster.addComponent(SuDaceEnemy); enemy.init(1, this.enemyHp, this.enemyMoveSpeed); } /**回收怪物 */ recycleMonster(monster: Node): void { if (!monster?.isValid) return; // 可能从碰撞回调触发:延迟到下一帧回收,避免 "Can not active RigidBody in contact listener." const col = monster.getComponent(Collider2D) ?? monster.getComponentInChildren(Collider2D); if (col) col.enabled = false; this.scheduleOnce(() => { if (!monster?.isValid) return; monster.active = false; monster.setParent(null); monster.setPosition(0, 0, 0); monster.destroy(); this.killedCount++; }, 0); } /**回收子弹 */ recycleBullet(bullet: Node): void { if (!bullet?.isValid) return; const col = bullet.getComponent(Collider2D) ?? bullet.getComponentInChildren(Collider2D); if (col) col.enabled = false; this.scheduleOnce(() => { if (!bullet?.isValid) return; bullet.active = false; bullet.setParent(null); bullet.setPosition(0, 0, 0); // this.bulletPool.push(bullet); bullet.destroy(); }, 0); } /**回收落石 */ recycleStone(stone: Node): void { if (!stone?.isValid) return; const col = stone.getComponent(Collider2D) ?? stone.getComponentInChildren(Collider2D); if (col) col.enabled = false; this.scheduleOnce(() => { if (!stone?.isValid) return; stone.active = false; stone.setParent(null); stone.setPosition(0, 0, 0); stone.destroy(); // this.stonePool.push(stone); }, 0); } /**获取一个离玩家最近的怪物 */ getNearestMonster(): Node { let nearestMonster: Node = null; let nearestDistance: number = 9999999999; for (let monster of this.map.enemyNode.children) { if (!monster.active) continue; if (this.map.player.node.scale.x < 0) { if (monster.worldPosition.x > this.map.player.node.worldPosition.x) continue; } else { if (monster.worldPosition.x < this.map.player.node.worldPosition.x) continue; } let distance = Vec3.distance(monster.worldPosition, this.map.player.node.worldPosition); if (distance < nearestDistance) { nearestDistance = distance; nearestMonster = monster; } } return nearestMonster as Node; } playAtk(): void { if (!this.audios.has("攻击")) return; this.audioSource.playOneShot(this.audios.get("攻击")); } playHit(): void { if (!this.audios.has("受击")) return; this.audioSource.playOneShot(this.audios.get("受击")); } playOpen(): void { if (!this.audios.has("开宝箱")) return; this.audioSource.playOneShot(this.audios.get("开宝箱")); } loadAudio(): void { let bundleName = "sound/"; let path = "sound/"; this.loadbundle(bundleName, (bundle) => { for (let audio of this.audioKeys) { this.loadRes(bundle, path + audio, (asset) => { this.audios.set(audio, asset); asset.addRef(); }); } }); } loadbundle(name, callback: Function) { let bundle = assetManager.getBundle("soudace"); if (!bundle) { assetManager.loadBundle("soudace", (err, bundle) => { if (err) { console.error(err); this.loadbundle(name, callback); } else { callback && callback(bundle); } }); } else { callback && callback(bundle); } } loadRes(bundle: AssetManager.Bundle, name: string, callback: Function) { bundle.load(name, AudioClip, (err, asset) => { if (err) { console.error(err); this.loadRes(bundle, name, callback); } else { callback && callback(asset); } }); } }