抽离MonMoveComp拆分怪物移动逻辑,让MoveComp仅负责英雄移动 新增Fly和FlyBoss怪物类型,配置三层飞行轨道支持空中怪物 重写波次刷怪逻辑,移除固定5槽限制,按轨道自由排布怪物 将怪物生成上限与恢复阈值从5/3调整为50/30 优化渲染排序逻辑,为飞行怪添加持续浮动动画 移除跨波怪物属性继承,波次切换时自动清理残留怪物
387 lines
14 KiB
TypeScript
387 lines
14 KiB
TypeScript
/**
|
||
* @file MissionMonComp.ts
|
||
* @description 怪物(Monster)波次刷新管理组件(逻辑层)
|
||
*
|
||
* 职责:
|
||
* 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 生成怪物。
|
||
* 2. 处理特殊插队刷怪请求(MonQueue),优先于常规刷新。
|
||
* 3. 自动推进波次:当前波所有怪物被清除后自动进入下一波。
|
||
*
|
||
* 关键设计:
|
||
* - 突破 5 槽限制,怪物按刷怪顺序依次从 X=280 开始向右(每隔 50)排布。
|
||
* - 3 条刷怪线:地面、+120、+240。
|
||
* - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。
|
||
* - 去除跨波 HP 继承,上一波残留怪在波次结束/开始时销毁。
|
||
*
|
||
* 怪物属性计算公式:
|
||
* ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias)
|
||
* hp = floor((base_hp + stage × grow_hp) × SpawnPowerBias)
|
||
* 其中 stage = currentWave - 1
|
||
*
|
||
* 依赖:
|
||
* - RogueConfig —— 怪物类型、成长值、波次配置
|
||
* - Monster(hero/Mon.ts)—— 怪物 ECS 实体类
|
||
* - HeroInfo(heroSet)—— 怪物基础属性配置(与英雄共用配置)
|
||
* - HeroAttrsComp / MonMoveComp —— 怪物属性和移动组件
|
||
* - 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, FacSet } from "../common/config/GameSet";
|
||
import { MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType, WaveSlotConfig, DefaultWaveSlot, IWaveSlot } from "./RogueConfig";
|
||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||
import { MonMoveComp } from "../hero/MonMoveComp";
|
||
const { ccclass, property } = _decorator;
|
||
|
||
/**
|
||
* MissionMonCompComp —— 怪物波次刷新管理器
|
||
*
|
||
* 每波开始时根据 WaveSlotConfig 配置生成怪物,
|
||
* 战斗中监控数量,所有怪物消灭后自动推进到下一波。
|
||
*/
|
||
@ccclass('MissionMonCompComp')
|
||
@ecs.register('MissionMonComp', false)
|
||
export class MissionMonCompComp extends CCComp {
|
||
// ======================== 常量 ========================
|
||
|
||
/** 怪物出生点起点 X */
|
||
private static readonly MON_SPAWN_START_X = 280;
|
||
/** 怪物出生的 X 间距 */
|
||
private static readonly MON_SPAWN_GAP_X = 50;
|
||
/** 怪物出生掉落高度 */
|
||
private static readonly MON_DROP_HEIGHT = 280;
|
||
/** 飞行层高度偏移(地面, 空中1, 空中2) */
|
||
private static readonly FLY_LANE_Y_OFFSETS = [0, 120, 240];
|
||
|
||
// ======================== 编辑器属性 ========================
|
||
|
||
@property({ tooltip: "是否启用调试日志" })
|
||
private debugMode: boolean = false;
|
||
|
||
// ======================== 插队刷怪队列 ========================
|
||
|
||
/**
|
||
* 刷怪队列(优先于常规配置处理):
|
||
* 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。
|
||
*/
|
||
private MonQueue: Array<{
|
||
/** 怪物 UUID */
|
||
uuid: number,
|
||
/** 怪物等级 */
|
||
level: number,
|
||
/** 飞行层 */
|
||
flyLane: number,
|
||
}> = [];
|
||
|
||
// ======================== 运行时状态 ========================
|
||
|
||
/** 记录每条线当前排到的索引 */
|
||
private laneIndices: number[] = [0, 0, 0];
|
||
/** 全局生成顺序计数器(用于渲染层级排序) */
|
||
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.on("PhasePrepareEnd", this.onPhasePrepareEnd, this);
|
||
this.on("TimeUpAdvanceWave", this.onTimeUpAdvanceWave, this);
|
||
}
|
||
|
||
/**
|
||
* 帧更新:
|
||
* 1. 检查游戏是否运行中。
|
||
* 2. 处理插队刷怪队列。
|
||
*/
|
||
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;
|
||
if(smc.mission.stop_spawn_mon) return;
|
||
this.updateSpecialQueue(dt);
|
||
}
|
||
|
||
// ======================== 事件处理 ========================
|
||
|
||
/**
|
||
* 接收特殊刷怪事件并入队。
|
||
* @param event 事件名
|
||
* @param args { uuid: number, level: number, flyLane?: 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,
|
||
flyLane: args.flyLane || 0
|
||
});
|
||
// 加速队列消费
|
||
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 = 1
|
||
this.waveTargetCount = 0
|
||
this.waveSpawnedCount = 0
|
||
this.MonQueue = []
|
||
this.laneIndices = [0, 0, 0];
|
||
|
||
let hasBoss = false;
|
||
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
|
||
for (const slot of config) {
|
||
if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss) {
|
||
hasBoss = true;
|
||
}
|
||
}
|
||
oops.message.dispatchEvent(GameEvent.NewWave, {
|
||
wave: this.currentWave,
|
||
total: this.waveTargetCount,
|
||
bossWave: hasBoss,
|
||
});
|
||
|
||
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
|
||
}
|
||
|
||
// ======================== 插队刷怪 ========================
|
||
|
||
/**
|
||
* 处理插队刷怪队列(每 0.15 秒尝试消费一个):
|
||
* 1. 根据飞行层排号。
|
||
* 2. 找到后从队列中移除并生成怪物。
|
||
*/
|
||
private updateSpecialQueue(dt: number) {
|
||
if (this.MonQueue.length <= 0) return;
|
||
this.queueTimer += dt;
|
||
if (this.queueTimer < 0.15) return;
|
||
|
||
const item = this.MonQueue.shift()!;
|
||
this.queueTimer = 0;
|
||
|
||
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) ||
|
||
MonList[MonType.LongBoss].includes(item.uuid) ||
|
||
(MonList[MonType.FlyBoss] && MonList[MonType.FlyBoss].includes(item.uuid));
|
||
|
||
const upType = this.getRandomUpType();
|
||
const lane = item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : 0;
|
||
|
||
this.addMonsterAt(lane, this.laneIndices[lane], item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1)));
|
||
this.laneIndices[lane]++;
|
||
}
|
||
|
||
// ======================== 波次管理 ========================
|
||
|
||
/**
|
||
* 开始下一波:
|
||
* 1. 波数 +1 并更新全局数据。
|
||
* 2. 重置槽位并根据配置生成本波所有怪物。
|
||
* 3. 分发 NewWave 事件。
|
||
*/
|
||
private onTimeUpAdvanceWave() {
|
||
this.currentWave += 1;
|
||
smc.vmdata.mission_data.level = 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 || slot.type === MonType.FlyBoss) {
|
||
hasBoss = true;
|
||
}
|
||
}
|
||
|
||
oops.message.dispatchEvent(GameEvent.NewWave, {
|
||
wave: this.currentWave,
|
||
total: this.waveTargetCount, // 此时还是上一波的怪物数量,但可以不传或后续修正
|
||
bossWave: hasBoss,
|
||
});
|
||
}
|
||
|
||
private onPhasePrepareEnd() {
|
||
this.resetSlotSpawnData(this.currentWave);
|
||
}
|
||
|
||
/** 获取当前阶段(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. 清理上一波残留怪物。
|
||
* 2. 读取波次配置。
|
||
* 3. 依据配置和 flyLane 属性,为每只怪物分配自增索引。
|
||
* 4. 立即实例化所有怪物。
|
||
*
|
||
* @param wave 当前波数
|
||
*/
|
||
private resetSlotSpawnData(wave: number = 1) {
|
||
// 1. 清理上一波残留怪物
|
||
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
|
||
const attrs = e.get(HeroAttrsComp);
|
||
if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) {
|
||
e.destroy();
|
||
}
|
||
});
|
||
|
||
// 2. 读取波次配置
|
||
const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot;
|
||
|
||
// 3. 重置排号索引
|
||
this.laneIndices = [0, 0, 0];
|
||
|
||
let allMons: any[] = [];
|
||
|
||
// 解析配置
|
||
for (const slot of config) {
|
||
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss;
|
||
// 判断分配到的飞行层
|
||
let lane: number = slot.flyLane !== undefined ? slot.flyLane : 0;
|
||
if (slot.type === MonType.Fly || slot.type === MonType.FlyBoss) {
|
||
lane = slot.flyLane !== undefined ? slot.flyLane : 1; // 飞行怪默认在第一层
|
||
}
|
||
lane = Math.max(0, Math.min(2, lane)); // 约束在 0,1,2
|
||
|
||
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, lane };
|
||
allMons.push(req);
|
||
}
|
||
}
|
||
|
||
this.waveTargetCount = allMons.length;
|
||
this.waveSpawnedCount = 0;
|
||
|
||
// 4. 立即生成本波所有怪物
|
||
for (const req of allMons) {
|
||
this.addMonsterAt(req.lane, this.laneIndices[req.lane], req.uuid, req.isBoss, req.upType, req.monLv);
|
||
this.laneIndices[req.lane]++;
|
||
}
|
||
}
|
||
|
||
// ======================== 怪物生成 ========================
|
||
|
||
/**
|
||
* 在指定层级、指定索引处生成一个怪物:
|
||
*
|
||
* @param laneIndex 飞行层索引 (0, 1, 2)
|
||
* @param monIndex 该层级的第几个怪 (0, 1, 2...)
|
||
* @param uuid 怪物 UUID
|
||
* @param isBoss 是否为 Boss
|
||
* @param upType 属性成长类型
|
||
* @param monLv 怪物等级
|
||
*/
|
||
private addMonsterAt(
|
||
laneIndex: number,
|
||
monIndex: 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_SPAWN_START_X + monIndex * MissionMonCompComp.MON_SPAWN_GAP_X;
|
||
const landingY = BoxSet.GAME_LINE + MissionMonCompComp.FLY_LANE_Y_OFFSETS[laneIndex] + (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, laneIndex);
|
||
|
||
// 设置渲染排序
|
||
const move = mon.get(MonMoveComp);
|
||
if (move) {
|
||
move.spawnOrder = 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();
|
||
}
|
||
}
|