Files
pixelheros/assets/script/game/map/MissionMonComp.ts
walkpan c7cbcc701f feat(map): 重构怪物生成系统为槽位队列机制
- 引入槽位队列系统替代顺序生成,提升怪物分布均匀性
- 增加战斗开始倒计时和首波爆发机制,改善游戏体验
- 实现槽位占用检测和负载均衡分配算法
- 添加怪物下落动画和槽位位置配置常量
2026-03-31 22:31:09 +08:00

373 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { _decorator, v3, Vec3 } from "cc";
import { mLogger } from "../common/Logger";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { Monster } from "../hero/Mon";
import { HeroInfo } from "../common/config/heroSet";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import {BoxSet } from "../common/config/GameSet";
import { BossList, MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MoveComp } from "../hero/MoveComp";
const { ccclass, property } = _decorator;
/** 视图层对象 */
@ccclass('MissionMonCompComp')
@ecs.register('MissionMonComp', false)
export class MissionMonCompComp extends CCComp {
private static readonly BOSS_RENDER_PRIORITY = 1000000;
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: "每波基础普通怪数量" })
private baseMonstersPerWave: number = 5;
@property({ tooltip: "每波额外增加普通怪数量" })
private waveMonsterGrowth: number = 0;
@property({ tooltip: "多少波刷新一次 Boss" })
private bossWaveInterval: number = 5;
@property({ tooltip: "同一波内刷怪间隔(秒)" })
private waveSpawnCd: number = 0.35;
// 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪)
// 约定:队列里的怪会优先于常规刷新处理
private MonQueue: Array<{
uuid: number,
level: number,
}> = [];
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;
/** 插队刷怪处理计时器 */
private queueTimer: number = 0;
private waveSpawnTimer: number = 0;
private currentWave: number = 0;
private waveTargetCount: number = 0;
private waveSpawnedCount: number = 0;
private bossSpawnedInWave: boolean = false;
onLoad(){
this.on(GameEvent.FightReady,this.fight_ready,this)
this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this);
this.resetSlotSpawnData()
}
/**
* 接收特殊刷怪事件并入队
* 事件数据最小结构:{ uuid, level }
*/
private onSpawnSpecialMonster(event: string, args: any) {
if (!args) return;
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 收到特殊刷怪指令:`, args);
this.MonQueue.push({
uuid: args.uuid,
level: args.level,
});
// 让队列在下一帧附近尽快消费,提升事件响应感
this.queueTimer = 1.0;
}
start() {
}
fight_ready(){
smc.vmdata.mission_data.mon_num=0
smc.mission.stop_spawn_mon = false
this.globalSpawnOrder = 0
this.queueTimer = 0
this.waveSpawnTimer = 0
this.currentWave = 0
this.waveTargetCount = 0
this.waveSpawnedCount = 0
this.bossSpawnedInWave = false
this.MonQueue = []
this.resetSlotSpawnData()
this.unschedule(this.finishBattleCountdown)
this.startNextWave()
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
}
protected update(dt: number): void {
if(!smc.mission.play) return
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;
this.updateSpecialQueue(dt);
this.updateWaveSpawn(dt);
}
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);
}
private updateWaveSpawn(dt: number) {
this.waveSpawnTimer += dt;
if (this.waveSpawnTimer < this.waveSpawnCd) return;
this.waveSpawnTimer = 0;
if (this.waveSpawnedCount >= this.waveTargetCount) {
if (this.isBossWave() && !this.bossSpawnedInWave) {
const bossUuid = this.getRandomBossUuid();
const bossUpType = this.getRandomUpType();
this.enqueueMonsterRequest(bossUuid, true, bossUpType);
this.bossSpawnedInWave = true;
}
return;
}
const uuid = this.getRandomNormalMonsterUuid();
const upType = this.getRandomUpType();
this.enqueueMonsterRequest(uuid, false, upType);
this.waveSpawnedCount += 1;
}
private startNextWave() {
this.currentWave += 1;
smc.vmdata.mission_data.level = this.currentWave;
this.waveTargetCount = Math.max(1, this.baseMonstersPerWave + (this.currentWave - 1) * this.waveMonsterGrowth);
this.waveSpawnedCount = 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,
bossWave: this.isBossWave(),
});
}
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();
}
private isBossWave() {
const interval = Math.max(1, Math.floor(this.bossWaveInterval));
return this.currentWave > 0 && this.currentWave % interval === 0;
}
private getCurrentStage(): number {
return Math.max(0, this.currentWave - 1);
}
private getRandomUpType(): UpType {
// 从 StageGrow 的 key 中采样,保证新增配置无需改逻辑
const keys = Object.keys(StageGrow).map(v => Number(v) as UpType);
const index = Math.floor(Math.random() * keys.length);
return keys[index] ?? UpType.AP1_HP1;
}
private getRandomNormalMonsterUuid(): number {
// MonType 是常量对象,这里通过值采样拿到怪物类型 id
const typeKeys = Object.keys(MonType).map(k => (MonType as any)[k]).filter(v => typeof v === "number");
const randomType = typeKeys[Math.floor(Math.random() * typeKeys.length)] as number;
// 如果某类型配置被清空,回退到 AP 类型,避免空池异常
const pool = MonList[randomType] || MonList[MonType.AP];
const index = Math.floor(Math.random() * pool.length);
return pool[index];
}
private getRandomBossUuid(): number {
// 目前 Boss 池可扩展为多个,先走随机抽取
const index = Math.floor(Math.random() * BossList.length);
return BossList[index];
}
private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] {
// 普通怪基础成长StageGrow
const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1];
if (!isBoss) return [grow[0], grow[1]];
// Boss 额外成长StageBossGrow在普通成长上叠加
const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1];
return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]];
}
private getSpawnPowerBias(): number {
return SpawnPowerBias;
}
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,
) {
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
const spawnX = MissionMonCompComp.MON_SLOT_START_X + slotIndex * MissionMonCompComp.MON_SLOT_X_INTERVAL;
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;
const move = mon.get(MoveComp);
if (move) {
move.spawnOrder = isBoss
? MissionMonCompComp.BOSS_RENDER_PRIORITY + this.globalSpawnOrder
: this.globalSpawnOrder;
}
const model = mon.get(HeroAttrsComp);
const base = HeroInfo[uuid];
if (!model || !base) return;
const stage = this.getCurrentStage();
const grow = this.resolveGrowPair(upType, isBoss);
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));
model.hp = model.hp_max;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
// this.node.destroy();
}
}