Files
pixelheros/assets/script/game/map/MissionMonComp.ts
panw 20e9b1d484 refactor(monster&hero): 重构三路分层逻辑与渲染排序
1. 移除飞行怪特殊判定,统一按Y轴高度处理三路渲染
2. 重命名飞行层相关变量为更准确的路次命名
3. 新增英雄自动分路均衡分配逻辑
4. 调整渲染排序规则,按Y轴高度决定上下层显示顺序
5. 修复怪物入场动画与刷怪分路逻辑
2026-05-12 16:32:25 +08:00

403 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.
/**
* @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 —— 怪物类型、成长值、波次配置
* - Monsterhero/Mon.ts—— 怪物 ECS 实体类
* - HeroInfoheroSet—— 怪物基础属性配置(与英雄共用配置)
* - 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;
/** 三路高度偏移(上路, 中路, 下路) */
private static readonly LANE_Y_OFFSETS = [100, 0, -100];
// ======================== 编辑器属性 ========================
@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");
}
// ======================== 插队刷怪 ========================
/** 选取当前排数最少的路(均衡分配) */
private pickBalancedLane(): number {
let min = this.laneIndices[0];
let lane = 0;
for (let i = 1; i < 3; i++) {
if (this.laneIndices[i] < min) {
min = this.laneIndices[i];
lane = i;
}
}
return lane;
}
/**
* 处理插队刷怪队列(每 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 !== undefined && item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : this.pickBalancedLane();
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;
for (let i = 0; i < slot.count; i++) {
const uuid = this.getRandomUuidByType(slot.type);
const upType = this.getRandomUpType();
// 优先使用配置的 lane否则均衡分配
let lane = slot.flyLane !== undefined ? slot.flyLane : this.pickBalancedLane();
lane = Math.max(0, Math.min(2, lane));
const req = { uuid, isBoss, upType, monLv: wave, lane };
allMons.push(req);
// 提前累加 laneIndices以便本波内的均衡分配能正确计算
this.laneIndices[lane]++;
}
}
this.waveTargetCount = allMons.length;
this.waveSpawnedCount = 0;
// 由于上面循环中已经累加了 laneIndices这里需要重置以便下面真正生成时再累加或者直接利用 allMons 生成)
this.laneIndices = [0, 0, 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.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();
}
}