/** * @file MissionMonComp.ts * @description 怪物(Monster)登场管理组件(逻辑层) * * 职责: * 1. 按小节配置刷怪:监听 SectionStart 事件,从 StageSpawningEngine 获取怪物列表。 * 2. 普通节:3 只小怪立即刷出;精英节:3 只精英同刷;Boss 节:2 小怪 + 1 Boss(延迟 1s)。 * 3. 怪物统一从右侧屏幕外跑入(贴地向左推进),不再高空掉落。 * 4. 维护小节刷怪计数,供 MissionComp 检测小节结束。 * * 关键设计: * - 怪物按类型落排(近战/Boss→前排、远程/刺客→中排、辅助/召唤→后排),与英雄三排镜像对应。 * - 出生 X 取目标排 X + 右侧偏移(屏幕外),dropToY=GAME_LINE(无下落), * MoveComp.targetX 指向目标排 X,落地即 moving=true 自然从右跑入。 * - 槽位索引仅用于 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, FightSet } from "../common/config/GameSet"; import { MonType } from "../common/config/heroSet"; import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { MoveComp } from "../hero/MoveComp"; import { NORMAL_FIRST_SPAWN, NORMAL_SPAWN_INTERVAL } from "../common/config/StageSet"; const { ccclass, property } = _decorator; @ccclass('MissionMonCompComp') @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { // ======================== 常量 ======================== /** 怪物出生右侧偏移(码):目标排 X 再加该偏移,使出生点位于屏幕右侧外,贴地从右跑入 */ private static readonly MON_SPAWN_OFFSET_X = 380; /** * 怪物类型 → 编队优先级(与英雄 HRole 枚举值升序一致): * 近战/重型/近战Boss → 0(最优先,占前排), * 远程/刺客/远程Boss → 1(占中排), * 辅助/召唤师 → 2(占后排)。 */ private static readonly MON_TYPE_PRIORITY: Record = { [MonType.Melee]: 0, [MonType.Heavy]: 0, [MonType.MeleeBoss]: 0, [MonType.Long]: 1, [MonType.Assassin]: 1, [MonType.LongBoss]: 1, [MonType.Support]: 2, [MonType.Summoner]: 2, }; /** 槽位基准 Y(3行×4列结构仅供 monGrid 寻路与 SCastSystem 索敌定位) */ public static readonly MON_POSITIONS: Vec3[] = [ v3(0, BoxSet.GAME_LINE, 0), // index 0: Col1-Top v3(0, BoxSet.GAME_LINE, 0), // index 1: Col1-Mid v3(0, BoxSet.GAME_LINE, 0), // index 2: Col1-Bot v3(0, BoxSet.GAME_LINE, 0), // index 3: Col2-Top v3(0, BoxSet.GAME_LINE, 0), // index 4: Col2-Mid v3(0, BoxSet.GAME_LINE, 0), // index 5: Col2-Bot v3(0, BoxSet.GAME_LINE, 0), // index 6: Col3-Top v3(0, BoxSet.GAME_LINE, 0), // index 7: Col3-Mid v3(0, BoxSet.GAME_LINE, 0), // index 8: Col3-Bot v3(0, BoxSet.GAME_LINE, 0), // index 9: Col4-Top v3(0, BoxSet.GAME_LINE, 0), // index 10: Col4-Mid v3(0, BoxSet.GAME_LINE, 0), // index 11: Col4-Bot ]; // ======================== 编辑器属性 ======================== @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; // ======================== 运行时状态 ======================== /** 全局生成顺序计数器(用于渲染层级排序) */ private globalSpawnOrder: number = 0; /** 当前小节目标怪物总数 */ private sectionTargetCount: number = 0; /** 当前小节已刷出怪物数量 */ private sectionSpawnedCount: number = 0; /** 整局(整关 5 回合)累计刷出怪物总数:跨回合累计,仅在整关重置(由 MissionComp.data_init 触发) */ private missionSpawnedCount: number = 0; /** 待刷出队列(batch=1 的怪物) */ private pendingQueue: GeneratedMonster[] = []; /** 刷怪定时器已累积时间 */ private spawnTimer: number = 0; /** 是否正在刷怪中 */ private isSpawning: boolean = false; // ======================== 生命周期 ======================== onLoad() { this.on(GameEvent.SectionStart, this.onSectionStart, this); } protected update(dt: number): void { // 待登场统计(战斗结束检测与 HUD 进度消费) smc.vmdata.mission_data.pending_mon_num = this.pendingQueue.length; // 定时刷怪:仅战斗阶段且非暂停时推进 if (!this.isSpawning || !smc.mission.in_fight || smc.mission.pause) return; if (this.pendingQueue.length === 0) { this.isSpawning = false; return; } this.spawnTimer += dt; if (this.spawnTimer >= NORMAL_SPAWN_INTERVAL) { this.spawnTimer = 0; const mon = this.pendingQueue.shift(); if (mon) { this.spawnMonster(mon); } } } start() { } /** * 小节开始事件回调:生成怪物列表并按节奏刷出 * @param event 事件名 * @param data { chapter, stage, section } */ private onSectionStart(event: string, data: any) { const chapter = Number(data?.chapter ?? 1); const stage = Number(data?.stage ?? 1); const section = Number(data?.section ?? 1); // 重置小节状态 this.globalSpawnOrder = 0; this.sectionSpawnedCount = 0; this.pendingQueue = []; this.spawnTimer = 0; this.isSpawning = false; smc.vmdata.mission_data.mon_num = 0; smc.vmdata.mission_data.pending_mon_num = 0; // 从生成引擎获取本小节怪物列表 const monsters = spawningEngine.generateSectionMonsters(chapter, stage, section); this.sectionTargetCount = monsters.length; if (TestModeConfig.enable) { mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启"); } mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 小节 ${chapter}-${stage}-${section} 生成完成,怪物总数: ${this.sectionTargetCount}`); // 按批次拆分:batch=0 立即刷,batch=1 进入待刷队列 const immediate: GeneratedMonster[] = []; const delayed: GeneratedMonster[] = []; for (const mon of monsters) { if (mon.batch === 0) { immediate.push(mon); } else { delayed.push(mon); } } // 按编队优先级排序立即刷出的怪物 immediate.sort((a, b) => { const pa = MissionMonCompComp.MON_TYPE_PRIORITY[a.type] ?? 1; const pb = MissionMonCompComp.MON_TYPE_PRIORITY[b.type] ?? 1; return pa - pb; }); // 立即刷出 batch=0 的怪物(处理 delay 字段) for (const mon of immediate) { if (mon.delay && mon.delay > 0) { // Boss 延迟刷出 this.scheduleOnce(() => { this.spawnMonster(mon); }, mon.delay); } else { this.spawnMonster(mon); } } // 剩余怪物进入待刷队列,启动定时刷怪 this.pendingQueue = delayed; if (this.pendingQueue.length > 0) { this.isSpawning = true; } // 广播小节开始事件(供 HUD 显示进度) oops.message.dispatchEvent(GameEvent.NewWave, { wave: section, total: this.sectionTargetCount, bossWave: monsters.some(m => m.isBoss), }); } // ======================== 怪物生成 ======================== /** * 刷出单个怪物(统一入口) * @param monData 怪物生成数据 */ private spawnMonster(monData: GeneratedMonster) { const posIndex = monData.spawnIndex; let mon = ecs.getEntity(Monster); let scale = -1; const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length]; const landingX = this.resolveSpawnX(posIndex); const landingY = basePos.y; // 出生点位于目标排右侧屏幕外(dropToY=landingY 无下落),落地即 moving=true 从右跑入 const spawnPos: Vec3 = v3(landingX + MissionMonCompComp.MON_SPAWN_OFFSET_X, landingY, 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, monData.lv, posIndex); // oops.message 全局事件总线:Boss 登场(震屏/音效由表现层消费) if (monData.isBoss) { oops.message.dispatchEvent(GameEvent.BossSpawn, { pos: spawnPos.clone() }); } const move = mon.get(MoveComp); if (move) { move.spawnOrder = this.globalSpawnOrder; // 推进目标指向目标排 X,而非出生点(出生点在屏幕外) move.targetX = landingX; } // 应用引擎计算好的最终属性 const model = mon.get(HeroAttrsComp); if (model) { model.ap = monData.ap; model.hp_max = monData.hp; model.hp = model.hp_max; // 精英标记:对象池复用实体需显式重置(Boss 与精英互斥,Boss 走 is_boss) model.is_elite = !monData.isBoss && !!monData.isElite; } this.sectionSpawnedCount++; this.missionSpawnedCount++; } /** 整局累计已刷出怪物总数(跨回合,整关重置):供 MissionComp 整局击杀统计 */ public getTotalSpawnedCount(): number { return this.missionSpawnedCount; } /** 整关重置刷怪统计(由 MissionComp.data_init 在整关开始/重开时调用,回合间不重置) */ public resetMissionStats(): void { this.missionSpawnedCount = 0; } /** * 解析怪物出生 X:按编队优先级名次映射三排锚点(与英雄编队排队逻辑一致)。 * 名次映射:rank0→前排(MON_ROW_FRONT_X)、rank1→中排(MON_ROW_MID_X)、rank≥2→后排(MON_ROW_BACK_X)。 * 效果:高优先级类型永远占据更靠前的排;当更高优先级类型插入时,低优先级类型自动向后让位。 * * @param index 当前怪物在排序后列表中的索引(名次) * @returns 出生 X 坐标 */ private resolveSpawnX(index: number): number { if (index === 0) return FightSet.MON_ROW_FRONT_X; if (index === 1) return FightSet.MON_ROW_MID_X; return FightSet.MON_ROW_BACK_X; } /** ECS 组件移除时触发 */ reset() { } }