/** * @file MissionMonComp.ts * @description 怪物(Monster)波次刷新管理组件(逻辑层) * * 职责: * 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 生成怪物。 * 2. 处理特殊插队刷怪请求(MonQueue),优先于常规刷新。 * 3. 自动推进波次:当前波所有怪物被清除后自动进入下一波。 * * 关键设计: * - 突破 5 槽限制,怪物按刷怪顺序依次从 X=280 开始向右(每隔 50)排布。 * - 3 条刷怪线:地面、+120、+240。 * - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。 * - 去除跨波 HP 继承,上一波残留怪在波次结束/开始时销毁。 * * 怪物属性计算公式: * ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias) * hp = floor((base_hp + stage × grow_hp) × SpawnPowerBias) * 其中 stage = currentWave - 1 * * 依赖: * - RogueConfig —— 怪物类型、成长值、波次配置 * - Monster(hero/Mon.ts)—— 怪物 ECS 实体类 * - HeroInfo(heroSet)—— 怪物基础属性配置(与英雄共用配置) * - HeroAttrsComp / MonMoveComp —— 怪物属性和移动组件 * - BoxSet.GAME_LINE —— 地面基准 Y 坐标 */ 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 { HeroInfo, HType } from "../common/config/heroSet"; import { smc } from "../common/SingletonModuleComp"; import { GameEvent } from "../common/config/GameEvent"; import {BoxSet, FacSet } from "../common/config/GameSet"; import { spawningEngine, GeneratedMonster, AffixType, MonType, MonList } from "./RogueConfig"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { MonMoveComp } from "../hero/MonMoveComp"; const { ccclass, property } = _decorator; /** * MissionMonCompComp —— 怪物波次刷新管理器 * * 每波开始时根据 WaveSlotConfig 配置生成怪物, * 战斗中监控数量,所有怪物消灭后自动推进到下一波。 */ @ccclass('MissionMonCompComp') @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { // ======================== 常量 ======================== /** 怪物出生点起点 X */ private static readonly MON_SPAWN_START_X = 280; /** 怪物出生的 X 间距 */ private static readonly MON_SPAWN_GAP_X = 50; /** 怪物出生掉落高度 */ private static readonly MON_DROP_HEIGHT = 280; /** 三路高度偏移(上路, 中路, 下路) */ private static readonly LANE_Y_OFFSETS = [100, 0, -100]; // ======================== 编辑器属性 ======================== @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; // ======================== 插队刷怪队列 ======================== /** * 刷怪队列(优先于常规配置处理): * 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。 */ private MonQueue: Array<{ /** 怪物 UUID */ uuid: number, /** 怪物等级 */ level: number, /** 飞行层 */ flyLane: number, }> = []; // ======================== 运行时状态 ======================== /** 记录每条线当前排到的索引 */ private laneIndices: number[] = [0, 0, 0]; /** 全局生成顺序计数器(用于渲染层级排序) */ private globalSpawnOrder: number = 0; /** 插队刷怪处理计时器 */ private queueTimer: number = 0; /** 当前波数 */ private currentWave: number = 0; /** 当前波的目标怪物总数 */ private waveTargetCount: number = 0; /** 当前波已生成的怪物数量 */ private waveSpawnedCount: number = 0; /** 等待生成的怪物队列(由新肉鸽引擎提供) */ private pendingMonsters: GeneratedMonster[] = []; /** 增量刷怪计时器 */ private spawnTimer: number = 0; // ======================== 生命周期 ======================== onLoad(){ this.on(GameEvent.FightReady,this.fight_ready,this) this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this); this.on("PhasePrepareEnd", this.onPhasePrepareEnd, this); this.on("TimeUpAdvanceWave", this.onTimeUpAdvanceWave, this); } /** * 帧更新: * 1. 检查游戏是否运行中。 * 2. 处理插队刷怪队列。 * 3. 逐步从 pendingMonsters 队列中生成怪物(受 stop_spawn_mon 限制)。 */ protected update(dt: number): void { if(!smc.mission.play) return if(smc.mission.pause) return if(smc.mission.stop_mon_action) return; if(!smc.mission.in_fight) return; this.updateSpecialQueue(dt); if(smc.mission.stop_spawn_mon) return; // 逐步刷怪逻辑 if (this.pendingMonsters.length > 0) { this.spawnTimer += dt; // 控制刷怪速率:例如每 0.2 秒刷 1-2 只 if (this.spawnTimer > 0.2) { this.spawnTimer = 0; // 一次出 2 只,加快进度 for (let i = 0; i < 2; i++) { if (this.pendingMonsters.length === 0) break; const monData = this.pendingMonsters.shift()!; const lane = this.pickBalancedLane(); this.addMonsterAt(lane, this.laneIndices[lane], monData); this.laneIndices[lane]++; this.waveSpawnedCount++; } } } } // ======================== 事件处理 ======================== /** * 接收特殊刷怪事件并入队。 * @param event 事件名 * @param args { uuid: number, level: number, flyLane?: number } */ private onSpawnSpecialMonster(event: string, args: any) { if (!args) return; mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 收到特殊刷怪指令:`, args); this.MonQueue.push({ uuid: args.uuid, level: args.level, flyLane: args.flyLane || 0 }); // 加速队列消费 this.queueTimer = 1.0; } start() { } /** * 战斗准备:重置所有运行时状态并开始第一波。 */ fight_ready(){ smc.vmdata.mission_data.mon_num=0 smc.mission.stop_spawn_mon = false this.globalSpawnOrder = 0 this.queueTimer = 0 this.currentWave = 1 this.waveTargetCount = 0 this.waveSpawnedCount = 0 this.MonQueue = [] this.pendingMonsters = [] this.spawnTimer = 0 this.laneIndices = [0, 0, 0]; // 预生成第一波数据以获取数量和 Boss 信息 const monsters = spawningEngine.generateWave(this.currentWave); this.pendingMonsters = monsters; this.waveTargetCount = monsters.length; let hasBoss = monsters.some(m => m.isBoss); oops.message.dispatchEvent(GameEvent.NewWave, { wave: this.currentWave, total: this.waveTargetCount, bossWave: hasBoss, }); mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System"); } // ======================== 插队刷怪 ======================== /** 选取当前排数最少的路(均衡分配) */ private pickBalancedLane(): number { let min = this.laneIndices[0]; let lane = 0; for (let i = 1; i < 3; i++) { if (this.laneIndices[i] < min) { min = this.laneIndices[i]; lane = i; } } return lane; } /** * 处理插队刷怪队列(每 0.15 秒尝试消费一个): * 1. 根据指定路或均衡分路。 * 2. 找到后从队列中移除并生成怪物。 */ private updateSpecialQueue(dt: number) { if (this.MonQueue.length <= 0) return; this.queueTimer += dt; if (this.queueTimer < 0.15) return; const item = this.MonQueue.shift()!; this.queueTimer = 0; const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || MonList[MonType.LongBoss].includes(item.uuid); const lane = item.flyLane !== undefined && item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : this.pickBalancedLane(); // 构造一个模拟的 GeneratedMonster 数据传递给 addMonsterAt const base = HeroInfo[item.uuid]; const monData: GeneratedMonster = { uuid: item.uuid, type: MonType.Melee, // 简化的兜底,真实逻辑依赖 heroSet 配置 hp: base ? base.hp : 100, ap: base ? base.ap : 10, affixes: [], isBoss: isBoss, spawnIndex: 0 }; this.addMonsterAt(lane, this.laneIndices[lane], monData, item.level); this.laneIndices[lane]++; } // ======================== 波次管理 ======================== /** * 开始下一波: * 1. 波数 +1 并更新全局数据。 * 2. 分发 NewWave 事件(实际的生成在 resetSlotSpawnData 中触发)。 */ private onTimeUpAdvanceWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; // 预生成新一波数据以获取数量和 Boss 信息 const monsters = spawningEngine.generateWave(this.currentWave); this.pendingMonsters = monsters; this.waveTargetCount = monsters.length; let hasBoss = monsters.some(m => m.isBoss); oops.message.dispatchEvent(GameEvent.NewWave, { wave: this.currentWave, total: this.waveTargetCount, bossWave: hasBoss, }); } private onPhasePrepareEnd() { this.resetSlotSpawnData(this.currentWave); } // ======================== 槽位管理 ======================== /** * 重新分配本波所有怪物状态: * 1. 清理上一波残留怪物。 * 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好,只需重置 laneIndices 即可。 * * @param wave 当前波数 */ private resetSlotSpawnData(wave: number = 1) { // 1. 清理上一波残留怪物 ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => { const attrs = e.get(HeroAttrsComp); if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) { e.destroy(); } }); // 2. 重置排号索引 this.laneIndices = [0, 0, 0]; this.waveSpawnedCount = 0; } // ======================== 怪物生成 ======================== /** * 在指定层级、指定索引处生成一个怪物: * * @param laneIndex 三路索引 (0 上, 1 中, 2 下) * @param monIndex 该路级的第几个怪 (0, 1, 2...) * @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等) * @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定) */ private addMonsterAt( laneIndex: number, monIndex: number, monData: GeneratedMonster, monLv: number = 1 ) { let mon = ecs.getEntity(Monster); let scale = -1; // 计算坐标 const spawnX = MissionMonCompComp.MON_SPAWN_START_X + monIndex * MissionMonCompComp.MON_SPAWN_GAP_X; const landingY = BoxSet.GAME_LINE + MissionMonCompComp.LANE_Y_OFFSETS[laneIndex] + (monData.isBoss ? 6 : 0); const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0); this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, laneIndex); // 设置渲染排序 const move = mon.get(MonMoveComp); if (move) { move.spawnOrder = this.globalSpawnOrder; } // 应用新引擎计算好的最终属性和词缀 const model = mon.get(HeroAttrsComp); if (!model) return; model.ap = monData.ap; model.hp_max = monData.hp; model.hp = model.hp_max; // 将词缀记录到属性组件上,供战斗层使用 (model as any).affixes = monData.affixes || []; // 解析特定的抗性词缀 if (monData.affixes) { if (monData.affixes.includes(AffixType.CritRes)) { model.critical_res = 50; } if (monData.affixes.includes(AffixType.FreezeRes)) { model.freeze_res = 50; } if (monData.affixes.includes(AffixType.KnockbackRes)) { model.knockback_res = 50; } } } /** ECS 组件移除时触发(当前不销毁节点) */ reset() { // this.node.destroy(); } }