/** * @file MissionMonComp.ts * @description 怪物(Monster)波次刷新管理组件(逻辑层) * * 职责: * 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。 * 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)启动分批释放。 * 3. 分批刷怪:每波固定 3 批,每 10 秒释放一批,批内按 MON_SPAWN_INTERVAL 逐个刷出。 * * 关键设计: * - 所有怪物统一从右侧 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, MAX_MONSTERS, BATCH_COUNT, BATCH_INTERVAL } 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 { // ======================== 常量 ======================== /** 怪物出生掉落高度 */ private static readonly MON_DROP_HEIGHT = 0; /** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */ private static readonly MON_SPAWN_X = 400; /** 逐个刷怪间隔(秒):保证怪物排成纵队,避免堆叠 */ private static readonly MON_SPAWN_INTERVAL = 0.3; /** * 怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标, * 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。 * 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。 * 超出 12 个槽位的怪物(放松波)按 spawnIndex % 12 复用槽位。 */ 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; /** 等待生成的怪物队列(按批次分组,batch 0~2) */ private pendingBatches: GeneratedMonster[][] = [[], [], []]; /** 当前正在释放的批次索引 */ private currentBatch: number = 0; /** 批次释放计时器(秒) */ private batchTimer: number = 0; /** 是否正在分批释放中 */ private isReleasing: boolean = false; /** 逐个刷怪队列:从当前批次转入,按节奏在 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 { // 统计待刷出的怪物总数(未释放的批次 + 正在释放的队列) let pendingCount = this.spawnQueue.length; for (let i = this.currentBatch; i < BATCH_COUNT; i++) { pendingCount += this.pendingBatches[i].length; } smc.vmdata.mission_data.pending_mon_num = pendingCount; // 分批释放:按 BATCH_INTERVAL 节奏推进批次 if (this.isReleasing) { this.batchTimer += dt; if (this.batchTimer >= BATCH_INTERVAL && this.currentBatch < BATCH_COUNT - 1) { this.batchTimer = 0; this.advanceBatch(); } } // 逐个刷怪:按 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.MON_POSITIONS.length; this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave); this.waveSpawnedCount++; } } } start() {} private setupWaveData(monsters: GeneratedMonster[]) { // 按批次分组 this.pendingBatches = [[], [], []]; for (const m of monsters) { const batch = Math.min(m.batch, BATCH_COUNT - 1); this.pendingBatches[batch].push(m); } this.waveTargetCount = monsters.length; smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount; 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.pendingBatches = [[], [], []]; this.currentBatch = 0; this.batchTimer = 0; this.isReleasing = false; 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(); // 准备结束阶段:启动分批释放, // 第一批立即转入 spawnQueue,后续批次由 update 按 BATCH_INTERVAL 推进。 this.startBatchRelease(); } // ======================== 分批释放 ======================== /** 启动分批释放:立即释放第一批,启动批次计时器 */ private startBatchRelease() { this.currentBatch = 0; this.batchTimer = 0; this.isReleasing = true; this.releaseCurrentBatch(); } /** 推进到下一批次 */ private advanceBatch() { if (this.currentBatch >= BATCH_COUNT - 1) { this.isReleasing = false; return; } this.currentBatch++; this.releaseCurrentBatch(); } /** 将当前批次的怪物转入 spawnQueue,等待逐个刷出 */ private releaseCurrentBatch() { const batch = this.pendingBatches[this.currentBatch]; if (batch.length === 0) { // 当前批次为空,尝试推进到下一批 if (this.currentBatch < BATCH_COUNT - 1) { this.currentBatch++; this.releaseCurrentBatch(); } else { this.isReleasing = false; } return; } // 将批次怪物转入逐个刷怪队列 for (const m of batch) { this.spawnQueue.push(m); } batch.length = 0; // 让首个怪物在下一帧立即刷出,避免额外延迟 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; this.isReleasing = false; this.currentBatch = 0; this.batchTimer = 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; // 技能套装注入:通过 _testSkills 通道传递给 Mon.load() if (monData.skills) { (mon as any)._testSkills = monData.skills; } 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() {} }