refactor(game): 重构怪物波次生成逻辑,移除排队机制

- 移除 IWaveSlot 中的 monCount 字段,改为固定 6 个槽位
- Boss 现在固定占用 2 个槽位且只能放置在 1、3、5 号位
- 每波开始时立即生成所有怪物,不再支持死亡后排队刷怪
- 简化 MissionMonComp 中的槽位管理逻辑,移除 slotSpawnQueues 等复杂结构
This commit is contained in:
panw
2026-04-07 16:36:49 +08:00
parent c7e46fc591
commit 9a1d517aa9
2 changed files with 110 additions and 200 deletions

View File

@@ -31,16 +31,9 @@ export class MissionMonCompComp extends CCComp {
level: number,
}> = [];
private slotSpawnQueues: Array<Array<{
uuid: number,
isBoss: boolean,
upType: UpType,
monLv: number,
slotsPerMon: number,
}>> = [];
private slotOccupiedEids: Array<number | null> = [];
private slotRangeTypes: Array<number> = [];
private slotSizes: Array<number> = []; // 记录每个槽位原本被配置为多大尺寸的怪,用于后续校验
private static readonly MAX_SLOTS = 6;
private slotOccupiedEids: Array<number | null> = Array(6).fill(null);
/** 全局生成顺序计数器,用于层级管理(预留) */
private globalSpawnOrder: number = 0;
/** 插队刷怪处理计时器 */
@@ -91,7 +84,6 @@ export class MissionMonCompComp extends CCComp {
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;
@@ -102,14 +94,35 @@ export class MissionMonCompComp extends CCComp {
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();
const item = this.MonQueue[0];
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || MonList[MonType.LongBoss].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);
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() {
@@ -133,7 +146,7 @@ export class MissionMonCompComp extends CCComp {
}
private tryAdvanceWave() {
if (this.hasPendingSlotQueue()) return;
if (this.MonQueue.length > 0) return;
if (this.hasActiveSlotMonster()) return;
if (smc.vmdata.mission_data.mon_num > 0) return;
this.startNextWave();
@@ -171,73 +184,80 @@ export class MissionMonCompComp extends CCComp {
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[] = [];
let totalSlots = 0;
let totalMonsters = 0;
for (const slot of config) {
const slotsPerMon = slot.slotsPerMon || 1;
const monCount = slot.monCount || 1;
totalSlots += slot.count * slotsPerMon;
totalMonsters += slot.count * monCount;
}
this.waveTargetCount = totalMonsters;
this.waveSpawnedCount = 0;
this.slotSpawnQueues = Array.from(
{ length: totalSlots },
() => []
);
this.slotOccupiedEids = Array.from(
{ length: totalSlots },
() => null
);
this.slotRangeTypes = [];
this.slotSizes = [];
let slotIndex = 0;
for (const slot of config) {
const slotsPerMon = slot.slotsPerMon || 1;
const monCount = slot.monCount || 1;
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss;
for (let i = 0; i < slot.count; i++) {
const currentSlotIndex = slotIndex;
// 设置槽位类型和大小
for (let s = 0; s < slotsPerMon; s++) {
this.slotRangeTypes.push(slot.type);
this.slotSizes.push(slotsPerMon);
slotIndex++;
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);
}
// 根据配置数量,直接在波次开始时把该坑位要刷的所有怪排入队列
for (let m = 0; m < monCount; m++) {
const uuid = this.getRandomUuidByType(slot.type);
const upType = this.getRandomUpType();
const request = { uuid, isBoss, upType, monLv: wave, slotsPerMon };
this.slotSpawnQueues[currentSlotIndex].push(request);
}
}
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 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++) {
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
if (this.slotOccupiedEids[i]) return true;
}
return false;
}
private refreshSlotOccupancy() {
for (let i = 0; i < this.slotOccupiedEids.length; i++) {
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
const eid = this.slotOccupiedEids[i];
if (!eid) continue;
const entity = ecs.getEntityByEid(eid);
@@ -246,118 +266,12 @@ export class MissionMonCompComp extends CCComp {
continue;
}
const attrs = entity.get(HeroAttrsComp);
if (!attrs) {
if (!attrs || attrs.hp <= 0) {
this.slotOccupiedEids[i] = null;
}
}
}
private getSlotQueueLoad(slotIndex: number): number {
const occupied = this.slotOccupiedEids[slotIndex] ? 1 : 0;
return occupied + this.slotSpawnQueues[slotIndex].length;
}
private resolveMonType(uuid: number): number {
for (const key in MonList) {
const list = MonList[key as unknown as number] as number[];
if (list && list.includes(uuid)) {
return Number(key);
}
}
return MonType.Melee;
}
private pickAssignSlotIndex(uuid: number, slotsPerMon: number): number {
const expectedType = this.resolveMonType(uuid);
let bestLoad = Number.MAX_SAFE_INTEGER;
let bestIndex = -1;
// 尝试找到一个连续的、且类型匹配并且 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;
// 降级寻找任意能容纳大小的槽位组合
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;
}
}
return Math.max(0, bestIndex);
}
private enqueueMonsterRequest(
uuid: number,
isBoss: boolean,
upType: UpType,
monLv: number = 1,
slotsPerMon: number = 1,
priority: boolean = false,
) {
const slotIndex = this.pickAssignSlotIndex(uuid, slotsPerMon);
const request = { uuid, isBoss, upType, monLv, slotsPerMon };
// 如果怪占用多个槽位,它应该存在于它占据的所有槽位的队列中(这样别的怪才会认为这里很挤)
// 但为了避免在 trySpawnFromSlotQueues 中被多次生成,我们只把实际的 request 放进它的起始槽位
// 其他被占用的槽位可以放一个占位符,或者通过其它方式处理
// 为了简便,我们只将它推入首个槽位,但排队检查的时候只要其中一个满了就算占用
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++) {
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);
}
}
private addMonsterBySlot(
slotIndex: number,
uuid: number = 1001,
@@ -379,7 +293,7 @@ export class MissionMonCompComp extends CCComp {
// 将它占用的所有格子都标记为这个 eid
for (let j = 0; j < slotsPerMon; j++) {
if (slotIndex + j < this.slotOccupiedEids.length) {
if (slotIndex + j < MissionMonCompComp.MAX_SLOTS) {
this.slotOccupiedEids[slotIndex + j] = mon.eid;
}
}