Files
pixelheros/assets/script/game/map/MissionMonComp.ts
panw e7b0d55e36 feat(rogue): 调整Boss占用槽位为3格并重构波次配置
- 将Boss默认占用槽位从2格改为3格,允许在任意连续三格空闲位置放置
- 简化波次配置,第1波改为2近战+1近战Boss,第2波改为2近战+1远程Boss
- 更新怪物池配置,调整近战Boss池包含6006和6105
- 重构怪物分配逻辑,统一处理所有类型怪物的槽位分配
- 优化远程Boss的放置策略,优先从后往前寻找空闲槽位
2026-04-08 10:31:20 +08:00

477 lines
18 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.
/**
* @file MissionMonComp.ts
* @description 怪物Monster波次刷新管理组件逻辑层
*
* 职责:
* 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 分配怪物到固定槽位。
* 2. 管理 5 个固定刷怪槽位的占用状态,支持 Boss 占 2 格。
* 3. 处理特殊插队刷怪请求MonQueue优先于常规刷新。
* 4. 自动推进波次:当前波所有怪物被清除后自动进入下一波。
*
* 关键设计:
* - 全场固定 5 个槽位(索引 0-4每个槽位占固定 X 坐标。
* - Boss 默认占 3 个连续槽位,只要有连续三格空闲即可。
* - slotOccupiedEids 记录每个槽位占用的怪物 ECS 实体 ID。
* - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。
* - refreshSlotOccupancy() 定期检查槽位占用的实体是否仍存活,清除已死亡的占用。
* - tryAdvanceWave() 在所有怪物死亡后自动推进波次。
*
* 怪物属性计算公式:
* ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias)
* hp = floor((base_hp + stage × grow_hp) × SpawnPowerBias)
* 其中 stage = currentWave - 1
*
* 依赖:
* - RogueConfig —— 怪物类型、成长值、波次配置
* - Monsterhero/Mon.ts—— 怪物 ECS 实体类
* - HeroInfoheroSet—— 怪物基础属性配置(与英雄共用配置)
* - HeroAttrsComp / MoveComp —— 怪物属性和移动组件
* - BoxSet.GAME_LINE —— 地面基准 Y 坐标
*/
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 { 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;
/**
* MissionMonCompComp —— 怪物波次刷新管理器
*
* 每波开始时根据 WaveSlotConfig 配置分配怪物到固定槽位,
* 战斗中监控槽位状态,所有怪物消灭后自动推进到下一波。
*/
@ccclass('MissionMonCompComp')
@ecs.register('MissionMonComp', false)
export class MissionMonCompComp extends CCComp {
// ======================== 常量 ========================
/** Boss 的渲染优先级偏移(确保 Boss 始终渲染在最前) */
private static readonly BOSS_RENDER_PRIORITY = 1000000;
/** 第一个槽位的 X 坐标起点 */
private static readonly MON_SLOT_START_X = 50;
/** 槽位间的 X 间距 */
private static readonly MON_SLOT_X_INTERVAL = 60;
/** 怪物出生掉落高度 */
private static readonly MON_DROP_HEIGHT = 280;
/** 最大槽位数 */
private static readonly MAX_SLOTS = 5;
// ======================== 编辑器属性 ========================
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false;
// ======================== 插队刷怪队列 ========================
/**
* 刷怪队列(优先于常规配置处理):
* 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。
*/
private MonQueue: Array<{
/** 怪物 UUID */
uuid: number,
/** 怪物等级 */
level: number,
}> = [];
// ======================== 运行时状态 ========================
/** 槽位占用状态:记录每个槽位当前占用的怪物 ECS 实体 IDnull 表示空闲 */
private slotOccupiedEids: Array<number | null> = Array(5).fill(null);
/** 全局生成顺序计数器(用于渲染层级排序) */
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)
}
/**
* 帧更新:
* 1. 检查游戏是否运行中。
* 2. 刷新槽位占用状态(清除已死亡怪物的占用)。
* 3. 尝试推进波次(所有怪物清除后自动进入下一波)。
* 4. 处理插队刷怪队列。
*/
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.tryAdvanceWave();
if(!smc.mission.in_fight) return;
if(smc.mission.stop_spawn_mon) return;
this.updateSpecialQueue(dt);
}
// ======================== 事件处理 ========================
/**
* 接收特殊刷怪事件并入队。
* @param event 事件名
* @param args { uuid: number, level: number }
*/
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");
}
// ======================== 插队刷怪 ========================
/**
* 处理插队刷怪队列(每 0.15 秒尝试消费一个):
* 1. 判断怪物是否为 Boss决定占用 1 格还是 2 格)。
* 2. 在空闲槽位中查找合适位置。
* 3. 找到后从队列中移除并生成怪物。
*/
private updateSpecialQueue(dt: number) {
if (this.MonQueue.length <= 0) return;
this.queueTimer += dt;
if (this.queueTimer < 0.15) return;
const item = this.MonQueue[0];
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || MonList[MonType.LongBoss].includes(item.uuid);
const isLongBoss = MonList[MonType.LongBoss].includes(item.uuid);
const slotsPerMon = isBoss ? 3 : 1;
// 查找空闲槽位
let slotIndex = -1;
if (slotsPerMon === 3) {
// 构造可用索引列表
let allowedIndices = [];
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS - 2; i++) {
allowedIndices.push(i);
}
// 远程 Boss 插队时优先尝试从后往前找
if (isLongBoss) {
allowedIndices.reverse();
}
for (const idx of allowedIndices) {
if (!this.slotOccupiedEids[idx] && !this.slotOccupiedEids[idx + 1] && !this.slotOccupiedEids[idx + 2]) {
slotIndex = idx;
break;
}
}
} else {
// 普通怪找第一个空闲格
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
if (!this.slotOccupiedEids[i]) {
slotIndex = i;
break;
}
}
}
if (slotIndex !== -1) {
this.MonQueue.shift();
this.queueTimer = 0;
const upType = this.getRandomUpType();
this.addMonsterBySlot(slotIndex, item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1)), slotsPerMon);
}
}
// ======================== 波次管理 ========================
/**
* 开始下一波:
* 1. 波数 +1 并更新全局数据。
* 2. 重置槽位并根据配置生成本波所有怪物。
* 3. 分发 NewWave 事件。
*/
private startNextWave() {
this.currentWave += 1;
smc.vmdata.mission_data.level = this.currentWave;
this.resetSlotSpawnData(this.currentWave);
// 检查本波是否有 Boss
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,
});
}
/**
* 尝试推进波次:
* 条件:队列为空 + 所有槽位无活怪 + 全局怪物数为 0。
*/
private tryAdvanceWave() {
if (this.MonQueue.length > 0) return;
if (this.hasActiveSlotMonster()) return;
if (smc.vmdata.mission_data.mon_num > 0) return;
this.startNextWave();
}
/** 获取当前阶段stage = wave - 1用于属性成长计算 */
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;
}
/**
* 根据怪物类型从对应池中随机选取 UUID。
* @param monType 怪物类型MonType 枚举值)
* @returns 怪物 UUID
*/
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];
}
/**
* 计算怪物属性成长值对。
* Boss 在普通成长基础上叠加 StageBossGrow。
*
* @param upType 成长类型
* @param isBoss 是否为 Boss
* @returns [AP 成长值, HP 成长值]
*/
private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] {
const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1];
if (!isBoss) return [grow[0], grow[1]];
const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1];
return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]];
}
/** 获取全局刷怪强度系数 */
private getSpawnPowerBias(): number {
return SpawnPowerBias;
}
// ======================== 槽位管理 ========================
/**
* 重置槽位并生成本波所有怪物:
* 1. 读取波次配置WaveSlotConfig 或 DefaultWaveSlot
* 2. 将 Boss 和普通怪分类。
* 3. Boss 优先分配到 0, 2, 4 号位(占 2 格)。
* 4. 普通怪填充剩余空闲格。
* 5. 立即实例化所有怪物。
*
* @param wave 当前波数
*/
private resetSlotSpawnData(wave: number = 1) {
const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot;
this.slotOccupiedEids = Array(MissionMonCompComp.MAX_SLOTS).fill(null);
let allMons: any[] = [];
// 解析配置
for (const slot of config) {
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss;
const slotsPerMon = slot.slotsPerMon || (isBoss ? 3 : 1);
for (let i = 0; i < slot.count; i++) {
const uuid = this.getRandomUuidByType(slot.type);
const upType = this.getRandomUpType();
const req = { uuid, isBoss, upType, monLv: wave, slotsPerMon };
allMons.push(req);
}
}
this.waveTargetCount = allMons.length;
this.waveSpawnedCount = 0;
let assignedSlots = new Array(MissionMonCompComp.MAX_SLOTS).fill(null);
// 统一按顺序分配(根据所需格数找连续空位)
for (const mon of allMons) {
let placed = false;
if (mon.slotsPerMon === 3) {
// 需要 3 格,占满连续的 3 格
for (let idx = 0; idx < MissionMonCompComp.MAX_SLOTS - 2; idx++) {
if (!assignedSlots[idx] && !assignedSlots[idx + 1] && !assignedSlots[idx + 2]) {
assignedSlots[idx] = mon;
assignedSlots[idx + 1] = "occupied"; // 占位标记
assignedSlots[idx + 2] = "occupied"; // 占位标记
placed = true;
break;
}
}
} else {
// 只需要 1 格
for (let idx = 0; idx < MissionMonCompComp.MAX_SLOTS; idx++) {
if (!assignedSlots[idx]) {
assignedSlots[idx] = mon;
placed = true;
break;
}
}
}
if (!placed) {
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] No slot for monster! uuid:", mon.uuid);
}
}
// 立即生成本波所有怪物
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
const req = assignedSlots[i];
if (req && req !== "occupied") {
this.addMonsterBySlot(i, req.uuid, req.isBoss, req.upType, req.monLv, req.slotsPerMon);
}
}
}
/** 检查是否有任何槽位仍被活着的怪物占用 */
private hasActiveSlotMonster() {
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; i++) {
if (this.slotOccupiedEids[i]) return true;
}
return false;
}
/**
* 刷新所有槽位的占用状态:
* 检查每个占用的 ECS 实体是否仍存在且 HP > 0
* 已失效的清除占用标记。
*/
private refreshSlotOccupancy() {
for (let i = 0; i < MissionMonCompComp.MAX_SLOTS; 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 || attrs.hp <= 0) {
this.slotOccupiedEids[i] = null;
}
}
}
// ======================== 怪物生成 ========================
/**
* 在指定槽位生成一个怪物:
* 1. 计算出生坐标(多格时居中)。
* 2. 创建 Monster ECS 实体。
* 3. 标记槽位占用。
* 4. 设置渲染排序Boss 优先级更高)。
* 5. 根据阶段和成长类型计算最终 AP / HP。
*
* @param slotIndex 槽位索引0-5
* @param uuid 怪物 UUID
* @param isBoss 是否为 Boss
* @param upType 属性成长类型
* @param monLv 怪物等级
* @param 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);
// 标记槽位占用
for (let j = 0; j < slotsPerMon; j++) {
if (slotIndex + j < MissionMonCompComp.MAX_SLOTS) {
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 组件移除时触发(当前不销毁节点) */
reset() {
// this.node.destroy();
}
}