feat(map): 重构怪物生成系统为槽位队列机制
- 引入槽位队列系统替代顺序生成,提升怪物分布均匀性 - 增加战斗开始倒计时和首波爆发机制,改善游戏体验 - 实现槽位占用检测和负载均衡分配算法 - 添加怪物下落动画和槽位位置配置常量
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user