You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
295 lines
9.3 KiB
295 lines
9.3 KiB
|
1 week ago
|
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<string, AudioClip> = new Map<string, AudioClip>();
|
||
|
|
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
}
|