feat(map): 重构怪物生成系统为槽位队列机制

- 引入槽位队列系统替代顺序生成,提升怪物分布均匀性
- 增加战斗开始倒计时和首波爆发机制,改善游戏体验
- 实现槽位占用检测和负载均衡分配算法
- 添加怪物下落动画和槽位位置配置常量
This commit is contained in:
walkpan
2026-03-31 22:31:09 +08:00
parent 5889423db0
commit c7cbcc701f

View File

@@ -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<Array<{
uuid: number,
isBoss: boolean,
upType: UpType,
monLv: number,
}>> = [];
private slotOccupiedEids: Array<number | null> = [];
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>(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;
}