Files
pixelheros/assets/script/game/map/MissionMonComp.ts
walkpan 8d61c67c1d refactor(map): 重构关卡刷怪与计时逻辑,适配30波新流程
1. 替换原固定战斗倒计时为正向计时clearTime
2. 移除旧波次配置,改用spawningEngine生成自适应怪物
3. 将波次上限从20调整为30,更新对应判断逻辑
4. 实现增量分批刷怪和自适应难度调整
5. 重置战斗状态时重置新引擎实例
2026-05-15 13:46:27 +08:00

347 lines
13 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 { spawningEngine, GeneratedMonster, AffixType, MonType, MonList } 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;
/** 等待生成的怪物队列(由新肉鸽引擎提供) */
private pendingMonsters: GeneratedMonster[] = [];
/** 增量刷怪计时器 */
private spawnTimer: 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. 处理插队刷怪队列。
* 3. 逐步从 pendingMonsters 队列中生成怪物(受 stop_spawn_mon 限制)。
*/
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.updateSpecialQueue(dt);
if(smc.mission.stop_spawn_mon) return;
// 逐步刷怪逻辑
if (this.pendingMonsters.length > 0) {
this.spawnTimer += dt;
// 控制刷怪速率:例如每 0.2 秒刷 1-2 只
if (this.spawnTimer > 0.2) {
this.spawnTimer = 0;
// 一次出 2 只,加快进度
for (let i = 0; i < 2; i++) {
if (this.pendingMonsters.length === 0) break;
const monData = this.pendingMonsters.shift()!;
const lane = this.pickBalancedLane();
this.addMonsterAt(lane, this.laneIndices[lane], monData);
this.laneIndices[lane]++;
this.waveSpawnedCount++;
}
}
}
}
// ======================== 事件处理 ========================
/**
* 接收特殊刷怪事件并入队。
* @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.pendingMonsters = []
this.spawnTimer = 0
this.laneIndices = [0, 0, 0];
// 预生成第一波数据以获取数量和 Boss 信息
const monsters = spawningEngine.generateWave(this.currentWave);
this.pendingMonsters = monsters;
this.waveTargetCount = monsters.length;
let hasBoss = monsters.some(m => m.isBoss);
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);
const lane = item.flyLane !== undefined && item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : this.pickBalancedLane();
// 构造一个模拟的 GeneratedMonster 数据传递给 addMonsterAt
const base = HeroInfo[item.uuid];
const monData: GeneratedMonster = {
uuid: item.uuid,
type: MonType.Melee, // 简化的兜底,真实逻辑依赖 heroSet 配置
hp: base ? base.hp : 100,
ap: base ? base.ap : 10,
affixes: [],
isBoss: isBoss,
spawnIndex: 0
};
this.addMonsterAt(lane, this.laneIndices[lane], monData, item.level);
this.laneIndices[lane]++;
}
// ======================== 波次管理 ========================
/**
* 开始下一波:
* 1. 波数 +1 并更新全局数据。
* 2. 分发 NewWave 事件(实际的生成在 resetSlotSpawnData 中触发)。
*/
private onTimeUpAdvanceWave() {
this.currentWave += 1;
smc.vmdata.mission_data.level = this.currentWave;
// 预生成新一波数据以获取数量和 Boss 信息
const monsters = spawningEngine.generateWave(this.currentWave);
this.pendingMonsters = monsters;
this.waveTargetCount = monsters.length;
let hasBoss = monsters.some(m => m.isBoss);
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: this.currentWave,
total: this.waveTargetCount,
bossWave: hasBoss,
});
}
private onPhasePrepareEnd() {
this.resetSlotSpawnData(this.currentWave);
}
// ======================== 槽位管理 ========================
/**
* 重新分配本波所有怪物状态:
* 1. 清理上一波残留怪物。
* 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好,只需重置 laneIndices 即可。
*
* @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. 重置排号索引
this.laneIndices = [0, 0, 0];
this.waveSpawnedCount = 0;
}
// ======================== 怪物生成 ========================
/**
* 在指定层级、指定索引处生成一个怪物:
*
* @param laneIndex 三路索引 (0 上, 1 中, 2 下)
* @param monIndex 该路级的第几个怪 (0, 1, 2...)
* @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等)
* @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定)
*/
private addMonsterAt(
laneIndex: number,
monIndex: number,
monData: GeneratedMonster,
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] + (monData.isBoss ? 6 : 0);
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, laneIndex);
// 设置渲染排序
const move = mon.get(MonMoveComp);
if (move) {
move.spawnOrder = this.globalSpawnOrder;
}
// 应用新引擎计算好的最终属性和词缀
const model = mon.get(HeroAttrsComp);
if (!model) return;
model.ap = monData.ap;
model.hp_max = monData.hp;
model.hp = model.hp_max;
// 将词缀记录到属性组件上,供战斗层使用
(model as any).affixes = monData.affixes || [];
}
/** ECS 组件移除时触发(当前不销毁节点) */
reset() {
// this.node.destroy();
}
}