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 } from "../common/config/heroSet"; import { smc } from "../common/SingletonModuleComp"; import { GameEvent } from "../common/config/GameEvent"; import {BoxSet } from "../common/config/GameSet"; import { BossList, MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType } from "./RogueConfig"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { MoveComp } from "../hero/MoveComp"; const { ccclass, property } = _decorator; /** 视图层对象 */ @ccclass('MissionMonCompComp') @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { private static readonly BOSS_RENDER_PRIORITY = 1000000; private static readonly MON_SLOT_COUNT = 6; private static readonly MON_SLOT_START_X = 30; private static readonly MON_SLOT_X_INTERVAL = 60; private static readonly MON_DROP_HEIGHT = 280; private static readonly BATTLE_COUNTDOWN_SECONDS = 3; @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; @property({ tooltip: "每波基础普通怪数量" }) private baseMonstersPerWave: number = 5; @property({ tooltip: "每波额外增加普通怪数量" }) private waveMonsterGrowth: number = 0; @property({ tooltip: "多少波刷新一次 Boss" }) private bossWaveInterval: number = 5; @property({ tooltip: "同一波内刷怪间隔(秒)" }) private waveSpawnCd: number = 0.35; // 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪) // 约定:队列里的怪会优先于常规刷新处理 private MonQueue: Array<{ uuid: number, level: number, }> = []; private slotSpawnQueues: Array> = []; private slotOccupiedEids: Array = []; private nextAssignSlotIndex: number = 0; /** 全局生成顺序计数器,用于层级管理(预留) */ private globalSpawnOrder: number = 0; /** 插队刷怪处理计时器 */ private queueTimer: number = 0; private waveSpawnTimer: number = 0; private currentWave: number = 0; private waveTargetCount: number = 0; private waveSpawnedCount: number = 0; private bossSpawnedInWave: boolean = false; onLoad(){ this.on(GameEvent.FightReady,this.fight_ready,this) this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this); this.resetSlotSpawnData() } /** * 接收特殊刷怪事件并入队 * 事件数据最小结构:{ uuid, level } */ 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.waveSpawnTimer = 0 this.currentWave = 0 this.waveTargetCount = 0 this.waveSpawnedCount = 0 this.bossSpawnedInWave = false this.MonQueue = [] this.resetSlotSpawnData() this.unschedule(this.finishBattleCountdown) this.startNextWave() mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System"); } 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.trySpawnFromSlotQueues(); this.tryAdvanceWave(); if(!smc.mission.in_fight) return; if(smc.mission.stop_spawn_mon) return; this.updateSpecialQueue(dt); this.updateWaveSpawn(dt); } private updateSpecialQueue(dt: number) { if (this.MonQueue.length <= 0) return; this.queueTimer += dt; // 轻微节流,避免同帧内突发大量插队导致瞬间堆怪 if (this.queueTimer < 0.15) return; this.queueTimer = 0; const item = this.MonQueue.shift(); if (!item) return; const upType = this.getRandomUpType(); this.enqueueMonsterRequest(item.uuid, BossList.includes(item.uuid), upType, Math.max(1, Number(item.level ?? 1)), true); } private updateWaveSpawn(dt: number) { this.waveSpawnTimer += dt; if (this.waveSpawnTimer < this.waveSpawnCd) return; this.waveSpawnTimer = 0; if (this.waveSpawnedCount >= this.waveTargetCount) { if (this.isBossWave() && !this.bossSpawnedInWave) { const bossUuid = this.getRandomBossUuid(); const bossUpType = this.getRandomUpType(); this.enqueueMonsterRequest(bossUuid, true, bossUpType); this.bossSpawnedInWave = true; } return; } const uuid = this.getRandomNormalMonsterUuid(); const upType = this.getRandomUpType(); this.enqueueMonsterRequest(uuid, false, upType); this.waveSpawnedCount += 1; } private startNextWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; this.waveTargetCount = Math.max(1, this.baseMonstersPerWave + (this.currentWave - 1) * this.waveMonsterGrowth); this.waveSpawnedCount = 0; this.bossSpawnedInWave = false; this.waveSpawnTimer = this.waveSpawnCd; this.nextAssignSlotIndex = 0; this.primeWaveInitialBurst(); this.startBattleCountdownIfNeeded(); oops.message.dispatchEvent(GameEvent.NewWave, { wave: this.currentWave, total: this.waveTargetCount, bossWave: this.isBossWave(), }); } private tryAdvanceWave() { if (this.waveSpawnedCount < this.waveTargetCount) return; if (this.isBossWave() && !this.bossSpawnedInWave) return; if (this.hasPendingSlotQueue()) return; if (this.hasActiveSlotMonster()) return; if (smc.vmdata.mission_data.mon_num > 0) return; this.startNextWave(); } private isBossWave() { const interval = Math.max(1, Math.floor(this.bossWaveInterval)); return this.currentWave > 0 && this.currentWave % interval === 0; } private getCurrentStage(): number { return Math.max(0, this.currentWave - 1); } private getRandomUpType(): UpType { // 从 StageGrow 的 key 中采样,保证新增配置无需改逻辑 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; } private getRandomNormalMonsterUuid(): number { // MonType 是常量对象,这里通过值采样拿到怪物类型 id const typeKeys = Object.keys(MonType).map(k => (MonType as any)[k]).filter(v => typeof v === "number"); const randomType = typeKeys[Math.floor(Math.random() * typeKeys.length)] as number; // 如果某类型配置被清空,回退到 AP 类型,避免空池异常 const pool = MonList[randomType] || MonList[MonType.AP]; const index = Math.floor(Math.random() * pool.length); return pool[index]; } private getRandomBossUuid(): number { // 目前 Boss 池可扩展为多个,先走随机抽取 const index = Math.floor(Math.random() * BossList.length); return BossList[index]; } private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] { // 普通怪基础成长:StageGrow const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1]; if (!isBoss) return [grow[0], grow[1]]; // Boss 额外成长:StageBossGrow(在普通成长上叠加) const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1]; return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]]; } private getSpawnPowerBias(): number { return SpawnPowerBias; } private primeWaveInitialBurst() { const remain = this.waveTargetCount - this.waveSpawnedCount; if (remain <= 0) return; const burstCount = Math.min(MissionMonCompComp.MON_SLOT_COUNT, remain); for (let i = 0; i < burstCount; i++) { const uuid = this.getRandomNormalMonsterUuid(); const upType = this.getRandomUpType(); this.enqueueMonsterRequest(uuid, false, upType); } this.waveSpawnedCount += burstCount; } private startBattleCountdownIfNeeded() { if (this.currentWave !== 1) return; smc.mission.pause = true; smc.mission.stop_mon_action = true; const dropDuration = Math.max(0.18, Math.min(0.38, MissionMonCompComp.MON_DROP_HEIGHT / 1200)); const lockDuration = dropDuration + MissionMonCompComp.BATTLE_COUNTDOWN_SECONDS; this.unschedule(this.finishBattleCountdown); this.scheduleOnce(this.finishBattleCountdown, lockDuration); } private finishBattleCountdown = () => { if (!smc.mission.play) return; if (!smc.mission.in_fight) return; smc.mission.pause = false; smc.mission.stop_mon_action = false; } private resetSlotSpawnData() { this.slotSpawnQueues = Array.from( { length: MissionMonCompComp.MON_SLOT_COUNT }, () => [] ); this.slotOccupiedEids = Array.from( { length: MissionMonCompComp.MON_SLOT_COUNT }, () => null ); this.nextAssignSlotIndex = 0; } private hasPendingSlotQueue() { for (let i = 0; i < this.slotSpawnQueues.length; i++) { if (this.slotSpawnQueues[i].length > 0) return true; } return false; } private hasActiveSlotMonster() { for (let i = 0; i < this.slotOccupiedEids.length; i++) { if (this.slotOccupiedEids[i]) return true; } return false; } private refreshSlotOccupancy() { for (let i = 0; i < this.slotOccupiedEids.length; 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) { this.slotOccupiedEids[i] = null; } } } private getSlotQueueLoad(slotIndex: number): number { const occupied = this.slotOccupiedEids[slotIndex] ? 1 : 0; return occupied + this.slotSpawnQueues[slotIndex].length; } private pickAssignSlotIndex(): number { let bestLoad = Number.MAX_SAFE_INTEGER; let bestIndex = this.nextAssignSlotIndex % MissionMonCompComp.MON_SLOT_COUNT; for (let step = 0; step < MissionMonCompComp.MON_SLOT_COUNT; step++) { const index = (this.nextAssignSlotIndex + step) % MissionMonCompComp.MON_SLOT_COUNT; const load = this.getSlotQueueLoad(index); if (load < bestLoad) { bestLoad = load; bestIndex = index; } } this.nextAssignSlotIndex = (bestIndex + 1) % MissionMonCompComp.MON_SLOT_COUNT; return bestIndex; } private enqueueMonsterRequest( uuid: number, isBoss: boolean, upType: UpType, monLv: number = 1, priority: boolean = false, ) { const slotIndex = this.pickAssignSlotIndex(); const request = { uuid, isBoss, upType, monLv }; if (priority) { this.slotSpawnQueues[slotIndex].unshift(request); return; } this.slotSpawnQueues[slotIndex].push(request); } private trySpawnFromSlotQueues() { if (smc.mission.stop_spawn_mon) return; for (let i = 0; i < this.slotSpawnQueues.length; i++) { if (this.slotOccupiedEids[i]) continue; const request = this.slotSpawnQueues[i].shift(); if (!request) continue; this.addMonsterBySlot(i, request.uuid, request.isBoss, request.upType, request.monLv); } } private addMonsterBySlot( slotIndex: 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_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL; 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); this.slotOccupiedEids[slotIndex] = 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.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ reset() { // this.node.destroy(); } }