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; /** 视图层对象 */ @ccclass('MissionMonCompComp') @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { private static readonly BOSS_RENDER_PRIORITY = 1000000; private static readonly MON_SLOT_START_X = 30; private static readonly MON_SLOT_X_INTERVAL = 60; private static readonly MON_DROP_HEIGHT = 280; @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; // 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪) // 约定:队列里的怪会优先于常规刷新处理 private MonQueue: Array<{ uuid: number, level: number, }> = []; private static readonly MAX_SLOTS = 6; private slotOccupiedEids: Array = Array(6).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) } /** * 接收特殊刷怪事件并入队 * 事件数据最小结构:{ 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.currentWave = 0 this.waveTargetCount = 0 this.waveSpawnedCount = 0 this.MonQueue = [] 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.tryAdvanceWave(); if(!smc.mission.in_fight) return; if(smc.mission.stop_spawn_mon) return; this.updateSpecialQueue(dt); } 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 slotsPerMon = isBoss ? 2 : 1; let slotIndex = -1; if (slotsPerMon === 2) { // Boss 只能放在 0, 2, 4 for (const idx of [0, 2, 4]) { if (!this.slotOccupiedEids[idx] && !this.slotOccupiedEids[idx + 1]) { 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); } } private startNextWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; this.resetSlotSpawnData(this.currentWave); 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, }); } private tryAdvanceWave() { if (this.MonQueue.length > 0) return; if (this.hasActiveSlotMonster()) return; if (smc.vmdata.mission_data.mon_num > 0) return; this.startNextWave(); } 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; } 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]; } 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 resetSlotSpawnData(wave: number = 1) { const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot; this.slotOccupiedEids = Array(MissionMonCompComp.MAX_SLOTS).fill(null); let bosses: any[] = []; let normals: any[] = []; for (const slot of config) { const slotsPerMon = slot.slotsPerMon || 1; const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss; 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 }; if (isBoss || slotsPerMon === 2) { bosses.push(req); } else { normals.push(req); } } } this.waveTargetCount = bosses.length + normals.length; this.waveSpawnedCount = 0; // Boss 只能放在 0, 2, 4 (即 1, 3, 5 号位) let bossAllowedIndices = [0, 2, 4]; let assignedSlots = new Array(MissionMonCompComp.MAX_SLOTS).fill(null); for (const boss of bosses) { let placed = false; for (const idx of bossAllowedIndices) { if (!assignedSlots[idx] && !assignedSlots[idx + 1]) { assignedSlots[idx] = boss; assignedSlots[idx + 1] = "occupied"; // 占位 placed = true; break; } } if (!placed) { mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] No slot for boss!"); } } for (const normal of normals) { let placed = false; for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) { if (!assignedSlots[i]) { assignedSlots[i] = normal; placed = true; break; } } if (!placed) { mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] No slot for normal monster!"); } } // 立即生成本波所有怪物 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; } 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; } } } 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); // 将它占用的所有格子都标记为这个 eid 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.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ reset() { // this.node.destroy(); } }