Files
pixelheros/assets/script/game/map/MissionMonComp.ts
panw 08b0ad128d feat(刷怪): 支持大型怪物占用多个槽位
- 在 IWaveSlot 配置中增加 slotsPerMon 字段,用于指定每个怪物占用的槽位数量
- 修改 pickAssignSlotIndex 方法以寻找连续且类型匹配的空闲槽位
- 调整 enqueueMonsterRequest 和 addMonsterBySlot 方法以处理多槽位怪物
- 更新波次配置,为 MeleeBoss 和 LongBoss 设置 slotsPerMon: 2
- 大型怪物生成时会居中放置在占用的多个槽位上
2026-04-03 16:37:57 +08:00

446 lines
17 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;
@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();
}
}