/** * @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 { MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType, WaveSlotConfig, DefaultWaveSlot, IWaveSlot } 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; // ======================== 生命周期 ======================== 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. 处理插队刷怪队列。 */ 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; if(smc.mission.stop_spawn_mon) return; this.updateSpecialQueue(dt); } // ======================== 事件处理 ======================== /** * 接收特殊刷怪事件并入队。 * @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.laneIndices = [0, 0, 0]; let hasBoss = false; const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot; for (const slot of config) { if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss) { hasBoss = true; } } 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) || (MonList[MonType.FlyBoss] && MonList[MonType.FlyBoss].includes(item.uuid)); const upType = this.getRandomUpType(); const lane = item.flyLane !== undefined && item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : this.pickBalancedLane(); this.addMonsterAt(lane, this.laneIndices[lane], item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1))); this.laneIndices[lane]++; } // ======================== 波次管理 ======================== /** * 开始下一波: * 1. 波数 +1 并更新全局数据。 * 2. 重置槽位并根据配置生成本波所有怪物。 * 3. 分发 NewWave 事件。 */ private onTimeUpAdvanceWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; // 检查本波是否有 Boss let hasBoss = false; const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot; for (const slot of config) { if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss) { hasBoss = true; } } oops.message.dispatchEvent(GameEvent.NewWave, { wave: this.currentWave, total: this.waveTargetCount, // 此时还是上一波的怪物数量,但可以不传或后续修正 bossWave: hasBoss, }); } private onPhasePrepareEnd() { this.resetSlotSpawnData(this.currentWave); } /** 获取当前阶段(stage = wave - 1,用于属性成长计算) */ private getCurrentStage(): number { return Math.max(0, this.currentWave - 1); } // ======================== 随机选取 ======================== /** 随机选取一种成长类型 */ private getRandomUpType(): UpType { const keys = Object.keys(StageGrow).map(v => Number(v) as UpType); const index = Math.floor(Math.random() * keys.length); return keys[index] ?? UpType.AP1_HP1; } /** * 根据怪物类型从对应池中随机选取 UUID。 * @param monType 怪物类型(MonType 枚举值) * @returns 怪物 UUID */ private getRandomUuidByType(monType: number): number { const pool = (MonList as any)[monType] || MonList[MonType.Melee]; if (!pool || pool.length === 0) return 6001; const index = Math.floor(Math.random() * pool.length); return pool[index]; } /** * 计算怪物属性成长值对。 * Boss 在普通成长基础上叠加 StageBossGrow。 * * @param upType 成长类型 * @param isBoss 是否为 Boss * @returns [AP 成长值, HP 成长值] */ private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] { const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1]; if (!isBoss) return [grow[0], grow[1]]; const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1]; return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]]; } /** 获取全局刷怪强度系数 */ private getSpawnPowerBias(): number { return SpawnPowerBias; } // ======================== 槽位管理 ======================== /** * 重新分配并生成本波所有怪物: * 1. 清理上一波残留怪物。 * 2. 读取波次配置。 * 3. 依据配置和 flyLane 属性,为每只怪物分配自增索引。 * 4. 立即实例化所有怪物。 * * @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. 读取波次配置 const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot; // 3. 重置排号索引 this.laneIndices = [0, 0, 0]; let allMons: any[] = []; // 解析配置 for (const slot of config) { const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss; for (let i = 0; i < slot.count; i++) { const uuid = this.getRandomUuidByType(slot.type); const upType = this.getRandomUpType(); // 优先使用配置的 lane,否则均衡分配 let lane = slot.flyLane !== undefined ? slot.flyLane : this.pickBalancedLane(); lane = Math.max(0, Math.min(2, lane)); const req = { uuid, isBoss, upType, monLv: wave, lane }; allMons.push(req); // 提前累加 laneIndices,以便本波内的均衡分配能正确计算 this.laneIndices[lane]++; } } this.waveTargetCount = allMons.length; this.waveSpawnedCount = 0; // 由于上面循环中已经累加了 laneIndices,这里需要重置,以便下面真正生成时再累加(或者直接利用 allMons 生成) this.laneIndices = [0, 0, 0]; // 4. 立即生成本波所有怪物 for (const req of allMons) { this.addMonsterAt(req.lane, this.laneIndices[req.lane], req.uuid, req.isBoss, req.upType, req.monLv); this.laneIndices[req.lane]++; } } // ======================== 怪物生成 ======================== /** * 在指定层级、指定索引处生成一个怪物: * * @param laneIndex 三路索引 (0 上, 1 中, 2 下) * @param monIndex 该路级的第几个怪 (0, 1, 2...) * @param uuid 怪物 UUID * @param isBoss 是否为 Boss * @param upType 属性成长类型 * @param monLv 怪物等级 */ private addMonsterAt( laneIndex: number, monIndex: number, uuid: number = 1001, isBoss: boolean = false, upType: UpType = UpType.AP1_HP1, 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] + (isBoss ? 6 : 0); const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0); this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv, laneIndex); // 设置渲染排序 const move = mon.get(MonMoveComp); if (move) { move.spawnOrder = this.globalSpawnOrder; } // 计算最终属性 const model = mon.get(HeroAttrsComp); const base = HeroInfo[uuid]; if (!model || !base) return; const stage = this.getCurrentStage(); const grow = this.resolveGrowPair(upType, isBoss); const bias = Math.max(0.1, this.getSpawnPowerBias()); model.ap = Math.max(1, Math.floor((base.ap + stage * grow[0]) * bias)); model.hp_max = Math.max(1, Math.floor((base.hp + stage * grow[1]) * bias)); model.hp = model.hp_max; } /** ECS 组件移除时触发(当前不销毁节点) */ reset() { // this.node.destroy(); } }