/** * @file MissionMonComp.ts * @description 怪物(Monster)波次刷新管理组件(逻辑层) * * 职责: * 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 分配怪物到固定槽位。 * 2. 管理 5 个固定刷怪槽位的占用状态,支持 Boss 占 2 格。 * 3. 处理特殊插队刷怪请求(MonQueue),优先于常规刷新。 * 4. 自动推进波次:当前波所有怪物被清除后自动进入下一波。 * * 关键设计: * - 全场固定 5 个槽位(索引 0-4),每个槽位占固定 X 坐标。 * - Boss 默认占 3 个连续槽位,只要有连续三格空闲即可。 * - slotOccupiedEids 记录每个槽位占用的怪物 ECS 实体 ID。 * - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。 * - refreshSlotOccupancy() 定期检查槽位占用的实体是否仍存活,清除已死亡的占用。 * - tryAdvanceWave() 在所有怪物死亡后自动推进波次。 * * 怪物属性计算公式: * 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 / MoveComp —— 怪物属性和移动组件 * - 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 } from "../common/config/GameSet"; import { MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType, WaveSlotConfig, DefaultWaveSlot, IWaveSlot } from "./RogueConfig"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { MoveComp } from "../hero/MoveComp"; const { ccclass, property } = _decorator; /** * MissionMonCompComp —— 怪物波次刷新管理器 * * 每波开始时根据 WaveSlotConfig 配置分配怪物到固定槽位, * 战斗中监控槽位状态,所有怪物消灭后自动推进到下一波。 */ @ccclass('MissionMonCompComp') @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { // ======================== 常量 ======================== /** Boss 的渲染优先级偏移(确保 Boss 始终渲染在最前) */ private static readonly BOSS_RENDER_PRIORITY = 1000000; /** 第一个槽位的 X 坐标起点 */ private static readonly MON_SLOT_START_X = 50; /** 槽位间的 X 间距 */ private static readonly MON_SLOT_X_INTERVAL = 65; /** 怪物出生掉落高度 */ private static readonly MON_DROP_HEIGHT = 280; /** 最大槽位数 */ private static readonly MAX_SLOTS = 5; // ======================== 编辑器属性 ======================== @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; // ======================== 插队刷怪队列 ======================== /** * 刷怪队列(优先于常规配置处理): * 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。 */ private MonQueue: Array<{ /** 怪物 UUID */ uuid: number, /** 怪物等级 */ level: number, }> = []; // ======================== 运行时状态 ======================== /** 槽位占用状态:记录每个槽位当前占用的怪物 ECS 实体 ID,null 表示空闲 */ private slotOccupiedEids: Array = Array(5).fill(null); /** 全局生成顺序计数器(用于渲染层级排序) */ 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.resetSlotSpawnData(1) } /** * 帧更新: * 1. 检查游戏是否运行中。 * 2. 刷新槽位占用状态(清除已死亡怪物的占用)。 * 3. 尝试推进波次(所有怪物清除后自动进入下一波)。 * 4. 处理插队刷怪队列。 */ 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.refreshSlotOccupancy(); this.tryAdvanceWave(); if(!smc.mission.in_fight) return; if(smc.mission.stop_spawn_mon) return; this.updateSpecialQueue(dt); } // ======================== 事件处理 ======================== /** * 接收特殊刷怪事件并入队。 * @param event 事件名 * @param args { uuid: number, level: 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, }); // 加速队列消费 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 = 0 this.waveTargetCount = 0 this.waveSpawnedCount = 0 this.MonQueue = [] this.startNextWave() mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System"); } // ======================== 插队刷怪 ======================== /** * 处理插队刷怪队列(每 0.15 秒尝试消费一个): * 1. 判断怪物是否为 Boss(决定占用 1 格还是 2 格)。 * 2. 在空闲槽位中查找合适位置。 * 3. 找到后从队列中移除并生成怪物。 */ private updateSpecialQueue(dt: number) { if (this.MonQueue.length <= 0) return; this.queueTimer += dt; if (this.queueTimer < 0.15) return; const item = this.MonQueue[0]; const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || MonList[MonType.LongBoss].includes(item.uuid); const isLongBoss = MonList[MonType.LongBoss].includes(item.uuid); const slotsPerMon = isBoss ? 3 : 1; // 查找空闲槽位 let slotIndex = -1; if (slotsPerMon === 3) { // 构造可用索引列表 let allowedIndices = []; for (let i = 0; i < MissionMonCompComp.MAX_SLOTS - 2; i++) { allowedIndices.push(i); } // 远程 Boss 插队时优先尝试从后往前找 if (isLongBoss) { allowedIndices.reverse(); } for (const idx of allowedIndices) { if (!this.slotOccupiedEids[idx] && !this.slotOccupiedEids[idx + 1] && !this.slotOccupiedEids[idx + 2]) { slotIndex = idx; break; } } } else { // 普通怪找第一个空闲格 for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { if (!this.slotOccupiedEids[i]) { slotIndex = i; break; } } } if (slotIndex !== -1) { this.MonQueue.shift(); this.queueTimer = 0; const upType = this.getRandomUpType(); this.addMonsterBySlot(slotIndex, item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1)), slotsPerMon); } } // ======================== 波次管理 ======================== /** * 开始下一波: * 1. 波数 +1 并更新全局数据。 * 2. 重置槽位并根据配置生成本波所有怪物。 * 3. 分发 NewWave 事件。 */ private startNextWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; this.resetSlotSpawnData(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) { hasBoss = true; } } oops.message.dispatchEvent(GameEvent.NewWave, { wave: this.currentWave, total: this.waveTargetCount, bossWave: hasBoss, }); } /** * 尝试推进波次: * 条件:队列为空 + 所有槽位无活怪 + 全局怪物数为 0。 */ private tryAdvanceWave() { if (this.MonQueue.length > 0) return; if (this.hasActiveSlotMonster()) return; if (smc.vmdata.mission_data.mon_num > 0) return; this.startNextWave(); } /** 获取当前阶段(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. 读取波次配置(WaveSlotConfig 或 DefaultWaveSlot)。 * 2. 将 Boss 和普通怪分类。 * 3. Boss 优先分配到 0, 2, 4 号位(占 2 格)。 * 4. 普通怪填充剩余空闲格。 * 5. 立即实例化所有怪物。 * * @param wave 当前波数 */ private resetSlotSpawnData(wave: number = 1) { const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot; this.slotOccupiedEids = Array(MissionMonCompComp.MAX_SLOTS).fill(null); let allMons: any[] = []; // 解析配置 for (const slot of config) { const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss; const slotsPerMon = slot.slotsPerMon || (isBoss ? 3 : 1); for (let i = 0; i < slot.count; i++) { const uuid = this.getRandomUuidByType(slot.type); const upType = this.getRandomUpType(); const req = { uuid, isBoss, upType, monLv: wave, slotsPerMon }; allMons.push(req); } } this.waveTargetCount = allMons.length; this.waveSpawnedCount = 0; let assignedSlots = new Array(MissionMonCompComp.MAX_SLOTS).fill(null); // 统一按顺序分配(根据所需格数找连续空位) for (const mon of allMons) { let placed = false; if (mon.slotsPerMon === 3) { // 需要 3 格,占满连续的 3 格 for (let idx = 0; idx < MissionMonCompComp.MAX_SLOTS - 2; idx++) { if (!assignedSlots[idx] && !assignedSlots[idx + 1] && !assignedSlots[idx + 2]) { assignedSlots[idx] = mon; assignedSlots[idx + 1] = "occupied"; // 占位标记 assignedSlots[idx + 2] = "occupied"; // 占位标记 placed = true; break; } } } else { // 只需要 1 格 for (let idx = 0; idx < MissionMonCompComp.MAX_SLOTS; idx++) { if (!assignedSlots[idx]) { assignedSlots[idx] = mon; placed = true; break; } } } if (!placed) { mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] No slot for monster! uuid:", mon.uuid); } } // 立即生成本波所有怪物 for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { const req = assignedSlots[i]; if (req && req !== "occupied") { this.addMonsterBySlot(i, req.uuid, req.isBoss, req.upType, req.monLv, req.slotsPerMon); } } } /** 检查是否有任何槽位仍被活着的怪物占用 */ private hasActiveSlotMonster() { for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { if (this.slotOccupiedEids[i]) return true; } return false; } /** * 刷新所有槽位的占用状态: * 检查每个占用的 ECS 实体是否仍存在且 HP > 0, * 已失效的清除占用标记。 */ private refreshSlotOccupancy() { for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { const eid = this.slotOccupiedEids[i]; if (!eid) continue; const entity = ecs.getEntityByEid(eid); if (!entity) { this.slotOccupiedEids[i] = null; continue; } const attrs = entity.get(HeroAttrsComp); if (!attrs || attrs.hp <= 0) { this.slotOccupiedEids[i] = null; } } } // ======================== 怪物生成 ======================== /** * 在指定槽位生成一个怪物: * 1. 计算出生坐标(多格时居中)。 * 2. 创建 Monster ECS 实体。 * 3. 标记槽位占用。 * 4. 设置渲染排序(Boss 优先级更高)。 * 5. 根据阶段和成长类型计算最终 AP / HP。 * * @param slotIndex 槽位索引(0-5) * @param uuid 怪物 UUID * @param isBoss 是否为 Boss * @param upType 属性成长类型 * @param monLv 怪物等级 * @param slotsPerMon 占用格数 */ private addMonsterBySlot( slotIndex: number, uuid: number = 1001, isBoss: boolean = false, upType: UpType = UpType.AP1_HP1, monLv: number = 1, slotsPerMon: number = 1, ) { let mon = ecs.getEntity(Monster); let scale = -1; // 多格占用时居中出生点 const centerXOffset = (slotsPerMon - 1) * MissionMonCompComp.MON_SLOT_X_INTERVAL / 2; const spawnX = MissionMonCompComp.MON_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL + centerXOffset; const landingY = BoxSet.GAME_LINE + (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); // 标记槽位占用 for (let j = 0; j < slotsPerMon; j++) { if (slotIndex + j < MissionMonCompComp.MAX_SLOTS) { this.slotOccupiedEids[slotIndex + j] = mon.eid; } } // 设置渲染排序 const move = mon.get(MoveComp); if (move) { move.spawnOrder = isBoss ? MissionMonCompComp.BOSS_RENDER_PRIORITY + this.globalSpawnOrder : 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(); } }