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, isBoss: boolean,
upType: UpType, upType: UpType,
monLv: number, monLv: number,
slotsPerMon: number,
}>> = []; }>> = [];
private slotOccupiedEids: Array<number | null> = []; private slotOccupiedEids: Array<number | null> = [];
private slotRangeTypes: Array<number> = []; private slotRangeTypes: Array<number> = [];
private slotSizes: Array<number> = []; // 记录每个槽位原本被配置为多大尺寸的怪,用于后续校验
/** 全局生成顺序计数器,用于层级管理(预留) */ /** 全局生成顺序计数器,用于层级管理(预留) */
private globalSpawnOrder: number = 0; private globalSpawnOrder: number = 0;
/** 插队刷怪处理计时器 */ /** 插队刷怪处理计时器 */
@@ -112,13 +114,15 @@ export class MissionMonCompComp extends CCComp {
private updateSpecialQueue(dt: number) { private updateSpecialQueue(dt: number) {
if (this.MonQueue.length <= 0) return; if (this.MonQueue.length <= 0) return;
this.queueTimer += dt; this.queueTimer += dt;
// 轻微节流,避免同帧内突发大量插队导致瞬间堆怪
if (this.queueTimer < 0.15) return; if (this.queueTimer < 0.15) return;
this.queueTimer = 0; this.queueTimer = 0;
const item = this.MonQueue.shift(); const item = this.MonQueue.shift();
if (!item) return; if (!item) return;
const upType = this.getRandomUpType(); 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) { private updateWaveSpawn(dt: number) {
@@ -129,14 +133,14 @@ export class MissionMonCompComp extends CCComp {
if (this.isBossWave() && !this.bossSpawnedInWave) { if (this.isBossWave() && !this.bossSpawnedInWave) {
const bossUuid = this.getRandomBossUuid(); const bossUuid = this.getRandomBossUuid();
const bossUpType = this.getRandomUpType(); const bossUpType = this.getRandomUpType();
this.enqueueMonsterRequest(bossUuid, true, bossUpType); this.enqueueMonsterRequest(bossUuid, true, bossUpType, 1, 2);
this.bossSpawnedInWave = true; this.bossSpawnedInWave = true;
} }
return; return;
} }
const uuid = this.getRandomNormalMonsterUuid(); const uuid = this.getRandomNormalMonsterUuid();
const upType = this.getRandomUpType(); const upType = this.getRandomUpType();
this.enqueueMonsterRequest(uuid, false, upType); this.enqueueMonsterRequest(uuid, false, upType, 1, 1);
this.waveSpawnedCount += 1; this.waveSpawnedCount += 1;
} }
@@ -217,7 +221,7 @@ export class MissionMonCompComp extends CCComp {
for (let i = 0; i < burstCount; i++) { for (let i = 0; i < burstCount; i++) {
const uuid = this.getRandomNormalMonsterUuid(); const uuid = this.getRandomNormalMonsterUuid();
const upType = this.getRandomUpType(); const upType = this.getRandomUpType();
this.enqueueMonsterRequest(uuid, false, upType); this.enqueueMonsterRequest(uuid, false, upType, 1, 1);
} }
this.waveSpawnedCount += burstCount; this.waveSpawnedCount += burstCount;
} }
@@ -227,7 +231,8 @@ export class MissionMonCompComp extends CCComp {
let totalSlots = 0; let totalSlots = 0;
for (const slot of config) { for (const slot of config) {
totalSlots += slot.count; const slotsPerMon = slot.slotsPerMon || 1;
totalSlots += slot.count * slotsPerMon;
} }
this.slotSpawnQueues = Array.from( this.slotSpawnQueues = Array.from(
@@ -243,9 +248,14 @@ export class MissionMonCompComp extends CCComp {
private confirmWaveSlotTypes(config: IWaveSlot[]) { private confirmWaveSlotTypes(config: IWaveSlot[]) {
this.slotRangeTypes = []; this.slotRangeTypes = [];
this.slotSizes = [];
for (const slot of config) { for (const slot of config) {
const slotsPerMon = slot.slotsPerMon || 1;
for (let i = 0; i < slot.count; i++) { 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; return MonType.Melee;
} }
private pickAssignSlotIndex(uuid: number): number { private pickAssignSlotIndex(uuid: number, slotsPerMon: number): number {
const expectedType = this.resolveMonType(uuid); const expectedType = this.resolveMonType(uuid);
let bestLoad = Number.MAX_SAFE_INTEGER; let bestLoad = Number.MAX_SAFE_INTEGER;
let bestIndex = -1; let bestIndex = -1;
for (let i = 0; i < this.slotRangeTypes.length; i++) { // 尝试找到一个连续的、且类型匹配并且 slotSizes 符合的空位
if (this.slotRangeTypes[i] !== expectedType) continue; for (let i = 0; i <= this.slotRangeTypes.length - slotsPerMon; i++) {
const load = this.getSlotQueueLoad(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) { if (load < bestLoad) {
bestLoad = load; bestLoad = load;
bestIndex = i; bestIndex = i;
} }
// 步进跨过这个怪的槽位,避免重叠判断
i += slotsPerMon - 1;
} }
if (bestIndex >= 0) return bestIndex; 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) { if (load < bestLoad) {
bestLoad = load; bestLoad = load;
bestIndex = i; bestIndex = i;
@@ -326,10 +353,16 @@ export class MissionMonCompComp extends CCComp {
isBoss: boolean, isBoss: boolean,
upType: UpType, upType: UpType,
monLv: number = 1, monLv: number = 1,
slotsPerMon: number = 1,
priority: boolean = false, priority: boolean = false,
) { ) {
const slotIndex = this.pickAssignSlotIndex(uuid); const slotIndex = this.pickAssignSlotIndex(uuid, slotsPerMon);
const request = { uuid, isBoss, upType, monLv }; const request = { uuid, isBoss, upType, monLv, slotsPerMon };
// 如果怪占用多个槽位,它应该存在于它占据的所有槽位的队列中(这样别的怪才会认为这里很挤)
// 但为了避免在 trySpawnFromSlotQueues 中被多次生成,我们只把实际的 request 放进它的起始槽位
// 其他被占用的槽位可以放一个占位符,或者通过其它方式处理
// 为了简便,我们只将它推入首个槽位,但排队检查的时候只要其中一个满了就算占用
if (priority) { if (priority) {
this.slotSpawnQueues[slotIndex].unshift(request); this.slotSpawnQueues[slotIndex].unshift(request);
return; return;
@@ -340,10 +373,26 @@ export class MissionMonCompComp extends CCComp {
private trySpawnFromSlotQueues() { private trySpawnFromSlotQueues() {
if (smc.mission.stop_spawn_mon) return; if (smc.mission.stop_spawn_mon) return;
for (let i = 0; i < this.slotSpawnQueues.length; i++) { for (let i = 0; i < this.slotSpawnQueues.length; i++) {
if (this.slotOccupiedEids[i]) continue; const queue = this.slotSpawnQueues[i];
const request = this.slotSpawnQueues[i].shift(); if (queue.length === 0) continue;
if (!request) continue;
this.addMonsterBySlot(i, request.uuid, request.isBoss, request.upType, request.monLv); 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, isBoss: boolean = false,
upType: UpType = UpType.AP1_HP1, upType: UpType = UpType.AP1_HP1,
monLv: number = 1, monLv: number = 1,
slotsPerMon: number = 1,
) { ) {
let mon = ecs.getEntity<Monster>(Monster); let mon = ecs.getEntity<Monster>(Monster);
let scale = -1; 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 landingY = BoxSet.GAME_LINE + (isBoss ? 6 : 0);
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0); const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv); 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); const move = mon.get(MoveComp);
if (move) { if (move) {
move.spawnOrder = isBoss move.spawnOrder = isBoss

View File

@@ -37,7 +37,8 @@ export const SpawnPowerBias = 1
export interface IWaveSlot { export interface IWaveSlot {
type: number; // 对应 MonType type: number; // 对应 MonType
count: number; count: number; // 怪物数量
slotsPerMon?: number; // 每个怪占用几个位置,默认 1
} }
// 每波怪物占位数量配置:数组顺序即为占位从左到右的排列顺序 // 每波怪物占位数量配置:数组顺序即为占位从左到右的排列顺序
@@ -53,14 +54,14 @@ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = {
], ],
3: [ 3: [
{ type: MonType.Melee, count: 3 }, { type: MonType.Melee, count: 3 },
{ type: MonType.MeleeBoss, count: 1 }, { type: MonType.MeleeBoss, count: 1, slotsPerMon: 2 },
{ type: MonType.Long, count: 2 } { type: MonType.Long, count: 2 }
], ],
4: [ 4: [
{ type: MonType.Melee, count: 2 }, { type: MonType.Melee, count: 2 },
{ type: MonType.Long, count: 2 }, { type: MonType.Long, count: 2 },
{ type: MonType.Support, count: 1 }, { type: MonType.Support, count: 1 },
{ type: MonType.LongBoss, count: 1 } { type: MonType.LongBoss, count: 1, slotsPerMon: 2 }
], ],
} }