feat(刷怪): 支持大型怪物占用多个槽位

- 在 IWaveSlot 配置中增加 slotsPerMon 字段,用于指定每个怪物占用的槽位数量
- 修改 pickAssignSlotIndex 方法以寻找连续且类型匹配的空闲槽位
- 调整 enqueueMonsterRequest 和 addMonsterBySlot 方法以处理多槽位怪物
- 更新波次配置,为 MeleeBoss 和 LongBoss 设置 slotsPerMon: 2
- 大型怪物生成时会居中放置在占用的多个槽位上
This commit is contained in:
panw
2026-04-03 16:37:57 +08:00
parent eb106c1b60
commit 08b0ad128d
2 changed files with 83 additions and 24 deletions

View File

@@ -44,9 +44,11 @@ export class MissionMonCompComp extends CCComp {
isBoss: boolean,
upType: UpType,
monLv: number,
slotsPerMon: number,
}>> = [];
private slotOccupiedEids: Array<number | null> = [];
private slotRangeTypes: Array<number> = [];
private slotSizes: Array<number> = []; // 记录每个槽位原本被配置为多大尺寸的怪,用于后续校验
/** 全局生成顺序计数器,用于层级管理(预留) */
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>(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

View File

@@ -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 }
],
}