1. 新增CritRes、FreezeRes、KnockbackRes三种词缀类型 2. 配置对应词缀的名称、消耗、最低等级等属性 3. 为怪物组件添加词缀抗性属性的赋值逻辑 4. 将新词条加入优先级列表
360 lines
13 KiB
TypeScript
360 lines
13 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 { 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 || [];
|
||
|
||
// 解析特定的抗性词缀
|
||
if (monData.affixes) {
|
||
if (monData.affixes.includes(AffixType.CritRes)) {
|
||
model.critical_res = 50;
|
||
}
|
||
if (monData.affixes.includes(AffixType.FreezeRes)) {
|
||
model.freeze_res = 50;
|
||
}
|
||
if (monData.affixes.includes(AffixType.KnockbackRes)) {
|
||
model.knockback_res = 50;
|
||
}
|
||
}
|
||
}
|
||
|
||
/** ECS 组件移除时触发(当前不销毁节点) */
|
||
reset() {
|
||
// this.node.destroy();
|
||
}
|
||
}
|