Files
pixelheros/assets/script/game/map/MissionMonComp.ts
panw 0129771435 refactor(怪物生成): 重构波次配置与生成逻辑
- 将波次配置从属性迁移至配置文件,增强可维护性
- 重构怪物生成逻辑,使用基于槽位的排队机制
- 移除旧的计时生成方式,改为配置驱动
2026-04-03 16:52:12 +08:00

408 lines
15 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, HType } 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, WaveSlotConfig, DefaultWaveSlot, IWaveSlot } 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_START_X = 30;
private static readonly MON_SLOT_X_INTERVAL = 60;
private static readonly MON_DROP_HEIGHT = 280;
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false;
// 刷怪队列(用于插队生成:比如运营活动怪、技能召唤怪、剧情强制怪)
// 约定:队列里的怪会优先于常规刷新处理
private MonQueue: Array<{
uuid: number,
level: number,
}> = [];
private slotSpawnQueues: Array<Array<{
uuid: number,
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;
/** 插队刷怪处理计时器 */
private queueTimer: number = 0;
private currentWave: number = 0;
private waveTargetCount: number = 0;
private waveSpawnedCount: number = 0;
onLoad(){
this.on(GameEvent.FightReady,this.fight_ready,this)
this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this);
this.resetSlotSpawnData(1)
}
/**
* 接收特殊刷怪事件并入队
* 事件数据最小结构:{ 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.currentWave = 0
this.waveTargetCount = 0
this.waveSpawnedCount = 0
this.MonQueue = []
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);
}
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();
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 startNextWave() {
this.currentWave += 1;
smc.vmdata.mission_data.level = this.currentWave;
this.resetSlotSpawnData(this.currentWave);
let hasBoss = false;
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
for (const slot of config) {
if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss) {
hasBoss = true;
}
}
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: this.currentWave,
total: this.waveTargetCount,
bossWave: hasBoss,
});
}
private tryAdvanceWave() {
if (this.hasPendingSlotQueue()) return;
if (this.hasActiveSlotMonster()) return;
if (smc.vmdata.mission_data.mon_num > 0) return;
this.startNextWave();
}
private getCurrentStage(): number {
return Math.max(0, this.currentWave - 1);
}
private getRandomUpType(): UpType {
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 getRandomUuidByType(monType: number): number {
const pool = (MonList as any)[monType] || MonList[MonType.Melee];
if (!pool || pool.length === 0) return 6001;
const index = Math.floor(Math.random() * pool.length);
return pool[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 resetSlotSpawnData(wave: number = 1) {
const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot;
let totalSlots = 0;
let totalMonsters = 0;
for (const slot of config) {
const slotsPerMon = slot.slotsPerMon || 1;
const monCount = slot.monCount || 1;
totalSlots += slot.count * slotsPerMon;
totalMonsters += slot.count * monCount;
}
this.waveTargetCount = totalMonsters;
this.waveSpawnedCount = 0;
this.slotSpawnQueues = Array.from(
{ length: totalSlots },
() => []
);
this.slotOccupiedEids = Array.from(
{ length: totalSlots },
() => null
);
this.slotRangeTypes = [];
this.slotSizes = [];
let slotIndex = 0;
for (const slot of config) {
const slotsPerMon = slot.slotsPerMon || 1;
const monCount = slot.monCount || 1;
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss;
for (let i = 0; i < slot.count; i++) {
const currentSlotIndex = slotIndex;
// 设置槽位类型和大小
for (let s = 0; s < slotsPerMon; s++) {
this.slotRangeTypes.push(slot.type);
this.slotSizes.push(slotsPerMon);
slotIndex++;
}
// 根据配置数量,直接在波次开始时把该坑位要刷的所有怪排入队列
for (let m = 0; m < monCount; m++) {
const uuid = this.getRandomUuidByType(slot.type);
const upType = this.getRandomUpType();
const request = { uuid, isBoss, upType, monLv: wave, slotsPerMon };
this.slotSpawnQueues[currentSlotIndex].push(request);
}
}
}
}
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 resolveMonType(uuid: number): number {
for (const key in MonList) {
const list = MonList[key as unknown as number] as number[];
if (list && list.includes(uuid)) {
return Number(key);
}
}
return MonType.Melee;
}
private pickAssignSlotIndex(uuid: number, slotsPerMon: number): number {
const expectedType = this.resolveMonType(uuid);
let bestLoad = Number.MAX_SAFE_INTEGER;
let bestIndex = -1;
// 尝试找到一个连续的、且类型匹配并且 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;
// 降级寻找任意能容纳大小的槽位组合
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;
}
}
return Math.max(0, bestIndex);
}
private enqueueMonsterRequest(
uuid: number,
isBoss: boolean,
upType: UpType,
monLv: number = 1,
slotsPerMon: number = 1,
priority: boolean = false,
) {
const slotIndex = this.pickAssignSlotIndex(uuid, slotsPerMon);
const request = { uuid, isBoss, upType, monLv, slotsPerMon };
// 如果怪占用多个槽位,它应该存在于它占据的所有槽位的队列中(这样别的怪才会认为这里很挤)
// 但为了避免在 trySpawnFromSlotQueues 中被多次生成,我们只把实际的 request 放进它的起始槽位
// 其他被占用的槽位可以放一个占位符,或者通过其它方式处理
// 为了简便,我们只将它推入首个槽位,但排队检查的时候只要其中一个满了就算占用
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++) {
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);
}
}
private addMonsterBySlot(
slotIndex: number,
uuid: number = 1001,
isBoss: boolean = false,
upType: UpType = UpType.AP1_HP1,
monLv: number = 1,
slotsPerMon: number = 1,
) {
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
// 如果占用了多个格子,出生坐标居中处理
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);
// 将它占用的所有格子都标记为这个 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
? 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();
}
}