From c7cbcc701f97379ce2798b480414c03e94fb34a2 Mon Sep 17 00:00:00 2001 From: walkpan Date: Tue, 31 Mar 2026 22:31:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(map):=20=E9=87=8D=E6=9E=84=E6=80=AA?= =?UTF-8?q?=E7=89=A9=E7=94=9F=E6=88=90=E7=B3=BB=E7=BB=9F=E4=B8=BA=E6=A7=BD?= =?UTF-8?q?=E4=BD=8D=E9=98=9F=E5=88=97=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入槽位队列系统替代顺序生成,提升怪物分布均匀性 - 增加战斗开始倒计时和首波爆发机制,改善游戏体验 - 实现槽位占用检测和负载均衡分配算法 - 添加怪物下落动画和槽位位置配置常量 --- assets/script/game/map/MissionMonComp.ts | 171 ++++++++++++++++++++--- 1 file changed, 150 insertions(+), 21 deletions(-) diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index c27c834d..a679ee24 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -18,8 +18,11 @@ const { ccclass, property } = _decorator; @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { private static readonly BOSS_RENDER_PRIORITY = 1000000; - private static readonly WAVE_SPAWN_START_X = 400; - private static readonly WAVE_SPAWN_X_INTERVAL = 60; + 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: "每波基础普通怪数量" }) @@ -38,7 +41,14 @@ export class MissionMonCompComp extends CCComp { level: number, }> = []; - private waveSpawnOrder: number = 0; + private slotSpawnQueues: Array> = []; + private slotOccupiedEids: Array = []; + private nextAssignSlotIndex: number = 0; /** 全局生成顺序计数器,用于层级管理(预留) */ private globalSpawnOrder: number = 0; /** 插队刷怪处理计时器 */ @@ -51,6 +61,7 @@ export class MissionMonCompComp extends CCComp { onLoad(){ this.on(GameEvent.FightReady,this.fight_ready,this) this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this); + this.resetSlotSpawnData() } /** @@ -82,7 +93,8 @@ export class MissionMonCompComp extends CCComp { this.waveSpawnedCount = 0 this.bossSpawnedInWave = false this.MonQueue = [] - this.waveSpawnOrder = 0 + this.resetSlotSpawnData() + this.unschedule(this.finishBattleCountdown) this.startNextWave() mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System"); } @@ -92,6 +104,8 @@ export class MissionMonCompComp extends CCComp { 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; @@ -107,9 +121,8 @@ export class MissionMonCompComp extends CCComp { this.queueTimer = 0; const item = this.MonQueue.shift(); if (!item) return; - // 特殊怪同样走随机成长类型,保持局内随机性一致 const upType = this.getRandomUpType(); - this.addMonster(item.uuid, BossList.includes(item.uuid), upType); + this.enqueueMonsterRequest(item.uuid, BossList.includes(item.uuid), upType, Math.max(1, Number(item.level ?? 1)), true); } private updateWaveSpawn(dt: number) { @@ -120,14 +133,14 @@ export class MissionMonCompComp extends CCComp { if (this.isBossWave() && !this.bossSpawnedInWave) { const bossUuid = this.getRandomBossUuid(); const bossUpType = this.getRandomUpType(); - this.addMonster(bossUuid, true, bossUpType); + this.enqueueMonsterRequest(bossUuid, true, bossUpType); this.bossSpawnedInWave = true; } return; } const uuid = this.getRandomNormalMonsterUuid(); const upType = this.getRandomUpType(); - this.addMonster(uuid, false, upType); + this.enqueueMonsterRequest(uuid, false, upType); this.waveSpawnedCount += 1; } @@ -136,9 +149,11 @@ export class MissionMonCompComp extends CCComp { smc.vmdata.mission_data.level = this.currentWave; this.waveTargetCount = Math.max(1, this.baseMonstersPerWave + (this.currentWave - 1) * this.waveMonsterGrowth); this.waveSpawnedCount = 0; - this.waveSpawnOrder = 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, @@ -149,6 +164,8 @@ export class MissionMonCompComp extends CCComp { 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(); } @@ -195,27 +212,142 @@ export class MissionMonCompComp extends CCComp { } private getSpawnPowerBias(): number { - // 动态难度偏差入口:当前固定读取配置,后续可切到玩家表现驱动 return SpawnPowerBias; } - - private addMonster( + 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, ) { - // 创建 ECS 怪物实体 let mon = ecs.getEntity(Monster); let scale = -1; - const spawnX = MissionMonCompComp.WAVE_SPAWN_START_X + this.waveSpawnOrder * MissionMonCompComp.WAVE_SPAWN_X_INTERVAL; + const spawnX = MissionMonCompComp.MON_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL; const landingY = BoxSet.GAME_LINE + (isBoss ? 6 : 0); - let pos: Vec3 = v3(spawnX, landingY, 0); - this.waveSpawnOrder += 1; - // 递增全局生成顺序,做溢出保护 + const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0); this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; - mon.load(pos, scale, uuid, isBoss, landingY); + mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv); + this.slotOccupiedEids[slotIndex] = mon.eid; const move = mon.get(MoveComp); if (move) { move.spawnOrder = isBoss @@ -227,12 +359,9 @@ export class MissionMonCompComp extends CCComp { if (!model || !base) return; const stage = this.getCurrentStage(); const grow = this.resolveGrowPair(upType, isBoss); - // 偏差值用于整体系数缩放:1=不变,>1增强,<1减弱 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)); - // 满血登场,保证 hp/hp_max 一致 model.hp = model.hp_max; }