/** * @file MissionMonComp.ts * @description 怪物(Monster)波次刷新管理组件(逻辑层) * * 职责: * 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。 * 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)把怪物转入逐个刷出队列。 * * 关键设计: * - 所有怪物统一从右侧 X=400 出生点逐个刷出(MON_SPAWN_INTERVAL 节奏控制)。 * - 实际阵型与推进由 MonMoveComp 在战斗中向左移动自然形成,不再使用固定网格点。 * - 槽位索引(3行×4列)仅用于 monGrid 寻路与 SCastSystem 索敌定位。 * - 上一波残留怪在波次结束/开始时统一清理。 */ import { _decorator, v3, Vec3 } from "cc"; import { mLogger } from "../common/Logger"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { Monster } from "../hero/Mon"; import { smc } from "../common/SingletonModuleComp"; import { GameEvent } from "../common/config/GameEvent"; import { BoxSet, FacSet } from "../common/config/GameSet"; import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { MonMoveComp } from "../hero/MonMoveComp"; const { ccclass, property } = _decorator; @ccclass('MissionMonCompComp') @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { // ======================== 常量 ======================== /** 怪物最多 12 个 */ private static readonly MAX_MONSTERS = 12; /** 怪物出生掉落高度 */ private static readonly MON_DROP_HEIGHT = 0; /** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */ private static readonly MON_SPAWN_X = 400; /** 逐个刷怪间隔(秒):保证怪物排成纵队,避免堆叠 */ private static readonly MON_SPAWN_INTERVAL = 0.3; /** * 12 个怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标, * 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。 * 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。 */ public static readonly MON_POSITIONS: Vec3[] = [ v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 0: Col1-Top v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 1: Col1-Mid v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 2: Col1-Bot v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 3: Col2-Top v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 4: Col2-Mid v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 5: Col2-Bot v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 6: Col3-Top v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 7: Col3-Mid v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 8: Col3-Bot v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 9: Col4-Top v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 10: Col4-Mid v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 11: Col4-Bot ]; // ======================== 编辑器属性 ======================== @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; // ======================== 运行时状态 ======================== /** 全局生成顺序计数器(用于渲染层级排序) */ private globalSpawnOrder: number = 0; /** 当前波数 */ private currentWave: number = 0; /** 当前波的目标怪物总数 */ private waveTargetCount: number = 0; /** 当前波已生成的怪物数量 */ private waveSpawnedCount: number = 0; /** 等待生成的怪物队列(波次总池) */ private pendingMonsters: GeneratedMonster[] = []; /** 逐个刷怪队列:从 pendingMonsters 转入,按节奏在 update 中释放 */ private spawnQueue: GeneratedMonster[] = []; /** 逐个刷怪累计计时(秒) */ private spawnTimer: number = 0; // ======================== 生命周期 ======================== onLoad() { this.on(GameEvent.FightReady, this.fight_ready, this); this.on("PhasePrepareEnd", this.onPhasePrepareEnd, this); this.on("TimeUpAdvanceWave", this.onTimeUpAdvanceWave, this); } protected update(dt: number): void { // pending_mon_num 同时统计未刷出的总池与正在释放的队列,确保 UI 与全灭判定一致 smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length + this.spawnQueue.length; // 逐个刷怪:按 MON_SPAWN_INTERVAL 节奏从队列释放 if (this.spawnQueue.length > 0) { this.spawnTimer += dt; if (this.spawnTimer >= MissionMonCompComp.MON_SPAWN_INTERVAL) { this.spawnTimer = 0; const monData = this.spawnQueue.shift()!; const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MAX_MONSTERS; this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave); this.waveSpawnedCount++; } } } start() {} private setupWaveData(monsters: GeneratedMonster[]) { this.pendingMonsters = monsters.slice(0, MissionMonCompComp.MAX_MONSTERS); smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length; this.waveTargetCount = this.pendingMonsters.length; let hasBoss = monsters.some(m => m.isBoss); mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`); oops.message.dispatchEvent(GameEvent.NewWave, { wave: this.currentWave, total: this.waveTargetCount, bossWave: hasBoss, }); } /** * 战斗准备:重置所有运行时状态并开始第一波。 */ fight_ready() { smc.vmdata.mission_data.mon_num = 0; smc.mission.stop_spawn_mon = false; this.globalSpawnOrder = 0; this.currentWave = 1; this.waveTargetCount = 0; this.waveSpawnedCount = 0; this.pendingMonsters = []; this.spawnQueue = []; this.spawnTimer = 0; // 预生成第一波数据以获取数量和 Boss 信息 const monsters = spawningEngine.generateWave(this.currentWave); this.setupWaveData(monsters); if (TestModeConfig.enable) { mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启"); } mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System"); } // ======================== 波次管理 ======================== /** * 开始下一波:波数 +1 并预生成数据 */ private onTimeUpAdvanceWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; const monsters = spawningEngine.generateWave(this.currentWave); this.setupWaveData(monsters); } private onPhasePrepareEnd() { this.resetSlotSpawnData(); // 准备结束阶段:把本波待刷怪物转入逐个释放队列, // 实际生成时机由 update() 按 MON_SPAWN_INTERVAL 节奏触发, // 让怪物从 X=400 出生点排成纵队依次向左推进。 if (this.pendingMonsters.length > 0) { const count = Math.min(this.pendingMonsters.length, MissionMonCompComp.MAX_MONSTERS); for (let i = 0; i < count; i++) { this.spawnQueue.push(this.pendingMonsters.shift()!); } // 让首个怪物在下一帧立即刷出,避免额外延迟 this.spawnTimer = MissionMonCompComp.MON_SPAWN_INTERVAL; } } // ======================== 槽位管理 ======================== /** * 清理上一波残留怪物,并重置生成计数与逐个刷怪队列 */ private resetSlotSpawnData() { ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => { const attrs = e.get(HeroAttrsComp); if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) { e.destroy(); } }); this.waveSpawnedCount = 0; // 同步丢弃上一波未释放的队列,避免与新一波混合 this.spawnQueue = []; this.spawnTimer = 0; } // ======================== 怪物生成 ======================== /** * 在指定位置索引处生成一个怪物 */ private addMonsterAtGrid( posIndex: number, monData: GeneratedMonster, monLv: number = 1 ) { let mon = ecs.getEntity(Monster); let scale = -1; const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length]; const spawnX = basePos.x; const landingY = basePos.y + (monData.isBoss ? 6 : 0); const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0); this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; if (monData.testSkills) { (mon as any)._testSkills = monData.testSkills; } mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex); const move = mon.get(MonMoveComp); if (move) { move.spawnOrder = this.globalSpawnOrder; } // 应用新引擎计算好的最终属性 const model = mon.get(HeroAttrsComp); if (model) { model.ap = monData.ap; model.hp_max = monData.hp; model.hp = model.hp_max; } } /** ECS 组件移除时触发 */ reset() {} }