From 08b0ad128d5a3dbdc075b9b1ec57a3f341320026 Mon Sep 17 00:00:00 2001 From: panw Date: Fri, 3 Apr 2026 16:37:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=88=B7=E6=80=AA):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=A7=E5=9E=8B=E6=80=AA=E7=89=A9=E5=8D=A0=E7=94=A8=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E6=A7=BD=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 IWaveSlot 配置中增加 slotsPerMon 字段,用于指定每个怪物占用的槽位数量 - 修改 pickAssignSlotIndex 方法以寻找连续且类型匹配的空闲槽位 - 调整 enqueueMonsterRequest 和 addMonsterBySlot 方法以处理多槽位怪物 - 更新波次配置,为 MeleeBoss 和 LongBoss 设置 slotsPerMon: 2 - 大型怪物生成时会居中放置在占用的多个槽位上 --- assets/script/game/map/MissionMonComp.ts | 100 ++++++++++++++++++----- assets/script/game/map/RogueConfig.ts | 7 +- 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index 2e2b0b5b..586c2ecd 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -44,9 +44,11 @@ export class MissionMonCompComp extends CCComp { isBoss: boolean, upType: UpType, monLv: number, + slotsPerMon: number, }>> = []; private slotOccupiedEids: Array = []; private slotRangeTypes: Array = []; + private slotSizes: Array = []; // 记录每个槽位原本被配置为多大尺寸的怪,用于后续校验 /** 全局生成顺序计数器,用于层级管理(预留) */ private globalSpawnOrder: number = 0; /** 插队刷怪处理计时器 */ @@ -112,13 +114,15 @@ export class MissionMonCompComp extends CCComp { 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); + const isBoss = BossList.includes(item.uuid); + // 简单推断:如果是 boss 默认给 2 格(你也可以从配置里反查或者加专门的英雄表配置) + const slotsPerMon = isBoss ? 2 : 1; + this.enqueueMonsterRequest(item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1)), slotsPerMon, true); } private updateWaveSpawn(dt: number) { @@ -129,14 +133,14 @@ export class MissionMonCompComp extends CCComp { if (this.isBossWave() && !this.bossSpawnedInWave) { const bossUuid = this.getRandomBossUuid(); const bossUpType = this.getRandomUpType(); - this.enqueueMonsterRequest(bossUuid, true, bossUpType); + this.enqueueMonsterRequest(bossUuid, true, bossUpType, 1, 2); this.bossSpawnedInWave = true; } return; } const uuid = this.getRandomNormalMonsterUuid(); const upType = this.getRandomUpType(); - this.enqueueMonsterRequest(uuid, false, upType); + this.enqueueMonsterRequest(uuid, false, upType, 1, 1); this.waveSpawnedCount += 1; } @@ -217,7 +221,7 @@ export class MissionMonCompComp extends CCComp { for (let i = 0; i < burstCount; i++) { const uuid = this.getRandomNormalMonsterUuid(); const upType = this.getRandomUpType(); - this.enqueueMonsterRequest(uuid, false, upType); + this.enqueueMonsterRequest(uuid, false, upType, 1, 1); } this.waveSpawnedCount += burstCount; } @@ -227,7 +231,8 @@ export class MissionMonCompComp extends CCComp { let totalSlots = 0; for (const slot of config) { - totalSlots += slot.count; + const slotsPerMon = slot.slotsPerMon || 1; + totalSlots += slot.count * slotsPerMon; } this.slotSpawnQueues = Array.from( @@ -243,9 +248,14 @@ export class MissionMonCompComp extends CCComp { private confirmWaveSlotTypes(config: IWaveSlot[]) { this.slotRangeTypes = []; + this.slotSizes = []; for (const slot of config) { + const slotsPerMon = slot.slotsPerMon || 1; for (let i = 0; i < slot.count; i++) { - this.slotRangeTypes.push(slot.type); + for (let s = 0; s < slotsPerMon; s++) { + this.slotRangeTypes.push(slot.type); + this.slotSizes.push(slotsPerMon); + } } } } @@ -295,24 +305,41 @@ export class MissionMonCompComp extends CCComp { return MonType.Melee; } - private pickAssignSlotIndex(uuid: number): number { + private pickAssignSlotIndex(uuid: number, slotsPerMon: number): number { const expectedType = this.resolveMonType(uuid); let bestLoad = Number.MAX_SAFE_INTEGER; let bestIndex = -1; - for (let i = 0; i < this.slotRangeTypes.length; i++) { - if (this.slotRangeTypes[i] !== expectedType) continue; - const load = this.getSlotQueueLoad(i); + // 尝试找到一个连续的、且类型匹配并且 slotSizes 符合的空位 + for (let i = 0; i <= this.slotRangeTypes.length - slotsPerMon; i++) { + let valid = true; + let load = 0; + for (let j = 0; j < slotsPerMon; j++) { + if (this.slotRangeTypes[i + j] !== expectedType || this.slotSizes[i + j] !== slotsPerMon) { + valid = false; + break; + } + load += this.getSlotQueueLoad(i + j); + } + if (!valid) continue; + if (load < bestLoad) { bestLoad = load; bestIndex = i; } + // 步进跨过这个怪的槽位,避免重叠判断 + i += slotsPerMon - 1; } if (bestIndex >= 0) return bestIndex; - for (let i = 0; i < this.slotRangeTypes.length; i++) { - const load = this.getSlotQueueLoad(i); + // 降级寻找任意能容纳大小的槽位组合 + bestLoad = Number.MAX_SAFE_INTEGER; + for (let i = 0; i <= this.slotRangeTypes.length - slotsPerMon; i++) { + let load = 0; + for (let j = 0; j < slotsPerMon; j++) { + load += this.getSlotQueueLoad(i + j); + } if (load < bestLoad) { bestLoad = load; bestIndex = i; @@ -326,10 +353,16 @@ export class MissionMonCompComp extends CCComp { isBoss: boolean, upType: UpType, monLv: number = 1, + slotsPerMon: number = 1, priority: boolean = false, ) { - const slotIndex = this.pickAssignSlotIndex(uuid); - const request = { uuid, isBoss, upType, monLv }; + const slotIndex = this.pickAssignSlotIndex(uuid, slotsPerMon); + const request = { uuid, isBoss, upType, monLv, slotsPerMon }; + + // 如果怪占用多个槽位,它应该存在于它占据的所有槽位的队列中(这样别的怪才会认为这里很挤) + // 但为了避免在 trySpawnFromSlotQueues 中被多次生成,我们只把实际的 request 放进它的起始槽位 + // 其他被占用的槽位可以放一个占位符,或者通过其它方式处理 + // 为了简便,我们只将它推入首个槽位,但排队检查的时候只要其中一个满了就算占用 if (priority) { this.slotSpawnQueues[slotIndex].unshift(request); return; @@ -340,10 +373,26 @@ export class MissionMonCompComp extends CCComp { 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); + const queue = this.slotSpawnQueues[i]; + if (queue.length === 0) continue; + + const request = queue[0]; + const slotsPerMon = request.slotsPerMon; + + // 检查这个怪需要的所有的槽位是否都空闲 + let canSpawn = true; + for (let j = 0; j < slotsPerMon; j++) { + if (i + j >= this.slotOccupiedEids.length || this.slotOccupiedEids[i + j]) { + canSpawn = false; + break; + } + } + + if (!canSpawn) continue; + + // 可以生成了,弹出请求 + queue.shift(); + this.addMonsterBySlot(i, request.uuid, request.isBoss, request.upType, request.monLv, slotsPerMon); } } @@ -353,16 +402,25 @@ export class MissionMonCompComp extends CCComp { isBoss: boolean = false, upType: UpType = UpType.AP1_HP1, monLv: number = 1, + slotsPerMon: number = 1, ) { let mon = ecs.getEntity(Monster); let scale = -1; - const spawnX = MissionMonCompComp.MON_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL; + // 如果占用了多个格子,出生坐标居中处理 + 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); - this.slotOccupiedEids[slotIndex] = mon.eid; + + // 将它占用的所有格子都标记为这个 eid + for (let j = 0; j < slotsPerMon; j++) { + if (slotIndex + j < this.slotOccupiedEids.length) { + this.slotOccupiedEids[slotIndex + j] = mon.eid; + } + } const move = mon.get(MoveComp); if (move) { move.spawnOrder = isBoss diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index 22af7b41..eb4c4d4e 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -37,7 +37,8 @@ export const SpawnPowerBias = 1 export interface IWaveSlot { type: number; // 对应 MonType - count: number; + count: number; // 怪物数量 + slotsPerMon?: number; // 每个怪占用几个位置,默认 1 } // 每波怪物占位数量配置:数组顺序即为占位从左到右的排列顺序 @@ -53,14 +54,14 @@ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = { ], 3: [ { type: MonType.Melee, count: 3 }, - { type: MonType.MeleeBoss, count: 1 }, + { type: MonType.MeleeBoss, count: 1, slotsPerMon: 2 }, { type: MonType.Long, count: 2 } ], 4: [ { type: MonType.Melee, count: 2 }, { type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }, - { type: MonType.LongBoss, count: 1 } + { type: MonType.LongBoss, count: 1, slotsPerMon: 2 } ], }