- 在 IWaveSlot 配置中增加 slotsPerMon 字段,用于指定每个怪物占用的槽位数量 - 修改 pickAssignSlotIndex 方法以寻找连续且类型匹配的空闲槽位 - 调整 enqueueMonsterRequest 和 addMonsterBySlot 方法以处理多槽位怪物 - 更新波次配置,为 MeleeBoss 和 LongBoss 设置 slotsPerMon: 2 - 大型怪物生成时会居中放置在占用的多个槽位上
446 lines
17 KiB
TypeScript
446 lines
17 KiB
TypeScript
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;
|
||
@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,
|
||
slotsPerMon: number,
|
||
}>> = [];
|
||
private slotOccupiedEids: Array<number | null> = [];
|
||
private slotRangeTypes: Array<number> = [];
|
||
private slotSizes: Array<number> = []; // 记录每个槽位原本被配置为多大尺寸的怪,用于后续校验
|
||
/** 全局生成顺序计数器,用于层级管理(预留) */
|
||
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(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.waveSpawnTimer = 0
|
||
this.currentWave = 0
|
||
this.waveTargetCount = 0
|
||
this.waveSpawnedCount = 0
|
||
this.bossSpawnedInWave = false
|
||
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);
|
||
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();
|
||
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) {
|
||
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, 1, 2);
|
||
this.bossSpawnedInWave = true;
|
||
}
|
||
return;
|
||
}
|
||
const uuid = this.getRandomNormalMonsterUuid();
|
||
const upType = this.getRandomUpType();
|
||
this.enqueueMonsterRequest(uuid, false, upType, 1, 1);
|
||
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.resetSlotSpawnData(this.currentWave);
|
||
this.primeWaveInitialBurst();
|
||
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.Melee];
|
||
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(this.slotSpawnQueues.length, remain);
|
||
for (let i = 0; i < burstCount; i++) {
|
||
const uuid = this.getRandomNormalMonsterUuid();
|
||
const upType = this.getRandomUpType();
|
||
this.enqueueMonsterRequest(uuid, false, upType, 1, 1);
|
||
}
|
||
this.waveSpawnedCount += burstCount;
|
||
}
|
||
|
||
private resetSlotSpawnData(wave: number = 1) {
|
||
const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot;
|
||
|
||
let totalSlots = 0;
|
||
for (const slot of config) {
|
||
const slotsPerMon = slot.slotsPerMon || 1;
|
||
totalSlots += slot.count * slotsPerMon;
|
||
}
|
||
|
||
this.slotSpawnQueues = Array.from(
|
||
{ length: totalSlots },
|
||
() => []
|
||
);
|
||
this.slotOccupiedEids = Array.from(
|
||
{ length: totalSlots },
|
||
() => null
|
||
);
|
||
this.confirmWaveSlotTypes(config);
|
||
}
|
||
|
||
private confirmWaveSlotTypes(config: IWaveSlot[]) {
|
||
this.slotRangeTypes = [];
|
||
this.slotSizes = [];
|
||
for (const slot of config) {
|
||
const slotsPerMon = slot.slotsPerMon || 1;
|
||
for (let i = 0; i < slot.count; i++) {
|
||
for (let s = 0; s < slotsPerMon; s++) {
|
||
this.slotRangeTypes.push(slot.type);
|
||
this.slotSizes.push(slotsPerMon);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|