/** * @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, getWaveType, SPAWN_INTERVAL_BY_TYPE } 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; /** 逐个刷怪默认间隔(秒):保证怪物排成纵队,避免堆叠;运行时按回合类型查 SPAWN_INTERVAL_BY_TYPE 覆盖 */ 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; /** 当前回合的逐个刷怪间隔(秒),按回合类型查 SPAWN_INTERVAL_BY_TYPE */ private spawnInterval: number = MissionMonCompComp.MON_SPAWN_INTERVAL; /** 当前批已刷出的怪物数(清场加速存活比例分母) */ private batchReleasedCount: number = 0; /** 当前批是否已通过清场加速提前推进过(每批只触发一次) */ private batchFastForwarded: boolean = false; /** 清场加速检测节流计时器(秒) */ private aliveCheckTimer: number = 0; /** 本回合因清场加速累计节省的秒数(供 MissionComp 还原 DDA 判定口径,避免双重惩罚) */ public waveEarlySkipTotal: number = 0; /** 清场加速触发阈值:当前批存活比例低于此值时提前推进(0.25 = 清掉 75%) */ private static readonly BATCH_EARLY_RATIO = 0.25; /** 清场加速提前量(秒):等效 batchTimer 快进,保底批间隔不被瞬间叠爆 */ private static readonly BATCH_EARLY_SKIP = 2.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; // 场上怪物超阈值时暂停释放:冻结批次推进与逐个刷出计时(不推进计时器,恢复后从断点平滑续接) // 注意 pending 统计必须在 return 之前执行,保证回合结束检测(pending==0)语义不受暂停影响 if (smc.mission.stop_spawn_mon) return; // 分批释放:按 BATCH_INTERVAL 节奏推进批次 if (this.isReleasing) { this.batchTimer += dt; if (this.batchTimer >= BATCH_INTERVAL && this.currentBatch < BATCH_COUNT - 1) { this.batchTimer = 0; this.advanceBatch(); } // 清场加速:0.2s 节流检测当前批存活比例,清得快则快进批次计时(学 PvZ 血量阈值提前刷新) this.aliveCheckTimer += dt; if (this.aliveCheckTimer >= 0.2) { this.aliveCheckTimer = 0; this.checkBatchEarlyAdvance(); } } // 逐个刷怪:按 spawnInterval 节奏从队列释放 if (this.spawnQueue.length > 0) { this.spawnTimer += dt; if (this.spawnTimer >= this.spawnInterval) { 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; this.batchReleasedCount = 0; this.batchFastForwarded = false; this.aliveCheckTimer = 0; this.waveEarlySkipTotal = 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(); // 按回合类型确定本回合刷怪间隔(放松回合快速倾泻,压力回合稍慢聚焦) this.spawnInterval = SPAWN_INTERVAL_BY_TYPE[getWaveType(this.currentWave)] ?? MissionMonCompComp.MON_SPAWN_INTERVAL; // 准备结束阶段:启动分批释放, // 第一批立即转入 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); } this.batchReleasedCount = batch.length; this.batchFastForwarded = false; batch.length = 0; // 让首个怪物在下一帧立即刷出,避免额外延迟 this.spawnTimer = this.spawnInterval; } /** * 清场加速检测:当前批已放完且存活比例 ≤ BATCH_EARLY_RATIO 时,快进批次计时器。 * 只奖励"清得快"的 build(压缩批间垃圾时间),弱 build 清不完则不触发、不叠加压力。 */ private checkBatchEarlyAdvance() { if (this.batchFastForwarded) return; if (this.currentBatch >= BATCH_COUNT - 1) return; if (this.spawnQueue.length > 0) return; // 本批还没放完,不判断 if (this.batchReleasedCount <= 0) return; let alive = 0; ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => { const a = e.get(HeroAttrsComp); if (a && a.fac === FacSet.MON && !a.is_dead) alive++; }); // alive 含前几批残留,比例 clamp 防低估 const aliveRatio = Math.min(1, alive / this.batchReleasedCount); if (aliveRatio <= MissionMonCompComp.BATCH_EARLY_RATIO) { this.batchFastForwarded = true; this.batchTimer += MissionMonCompComp.BATCH_EARLY_SKIP; this.waveEarlySkipTotal += MissionMonCompComp.BATCH_EARLY_SKIP; smc.vmdata.mission_data.wave_early_skip = this.waveEarlySkipTotal; mLogger.log(this.debugMode, 'MissionMonComp', `[EarlyAdvance] batch=${this.currentBatch} alive=${alive}/${this.batchReleasedCount},快进 ${MissionMonCompComp.BATCH_EARLY_SKIP}s`); } } // ======================== 槽位管理 ======================== /** * 清理上一回合残留怪物,并重置生成计数与逐个刷怪队列 */ 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; this.batchReleasedCount = 0; this.batchFastForwarded = false; this.aliveCheckTimer = 0; this.waveEarlySkipTotal = 0; smc.vmdata.mission_data.wave_early_skip = 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); // oops.message 全局事件总线:Boss 登场(震屏/音效由表现层消费) if (monData.isBoss) { oops.message.dispatchEvent(GameEvent.BossSpawn, { pos: spawnPos.clone() }); } 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() { } }