Files
pixelheros/assets/script/game/map/MissionMonComp.ts
pan 0560999ce5 fix(hero): 修正英雄和怪物初始默认动画为待机
调整HeroSpine组件的start方法初始调用为idle动画,修改怪物非下落场景下的默认状态从移动改为待机
2026-06-11 10:51:01 +08:00

342 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排布。
* - 6 条刷怪线:在三路 Y 轴范围内随机偏移,实现 6 路进军。
* - 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 {
// ======================== 常量 ========================
/** 怪物最多 12 个 */
private static readonly MAX_MONSTERS = 12;
/** 怪物出生点起点 X */
private static readonly MON_SPAWN_START_X = 60;
/** 怪物出生的 X 间距 (列距) */
private static readonly MON_SPAWN_GAP_X = 80;
/** 怪物出生掉落高度 */
private static readonly MON_DROP_HEIGHT = 0;
/** 3行高度偏移 (行距) */
private static readonly ROW_Y_OFFSETS = [90, 0, -90];
// ======================== 编辑器属性 ========================
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false;
// ======================== 插队刷怪队列 ========================
/**
* 刷怪队列(优先于常规配置处理):
* 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。
*/
private MonQueue: Array<{
/** 怪物 UUID */
uuid: number,
/** 怪物等级 */
level: number,
/** 飞行层 */
flyLane: number,
}> = [];
// ======================== 运行时状态 ========================
/** 全局生成顺序计数器(用于渲染层级排序) */
private globalSpawnOrder: number = 0;
/** 插队刷怪处理计时器 */
private queueTimer: number = 0;
/** 当前波数 */
private currentWave: number = 0;
/** 当前波的目标怪物总数 */
private waveTargetCount: number = 0;
/** 当前波已生成的怪物数量 */
private waveSpawnedCount: number = 0;
/** 等待生成的怪物队列(由新肉鸽引擎提供) */
private pendingMonsters: GeneratedMonster[] = [];
// ======================== 生命周期 ========================
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 {
smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length;
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);
}
// ======================== 事件处理 ========================
/**
* 接收特殊刷怪事件并入队。
* @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() {
}
private setupWaveData(monsters: GeneratedMonster[]) {
this.pendingMonsters = monsters.slice(0, MissionMonCompComp.MAX_MONSTERS);
smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length;
this.waveTargetCount = this.pendingMonsters.length;
let hasBoss = monsters.some(m => m.isBoss);
console.log(`[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`);
const uuids = monsters.map(m => m.uuid);
console.log(`[MissionMonComp] 波次 ${this.currentWave} 怪物 UUID 列表:`, uuids);
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: this.currentWave,
total: this.waveTargetCount,
bossWave: hasBoss,
});
}
/**
* 战斗准备:重置所有运行时状态并开始第一波。
*/
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 = []
// 预生成第一波数据以获取数量和 Boss 信息
const monsters = spawningEngine.generateWave(this.currentWave);
this.setupWaveData(monsters);
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System");
}
// ======================== 插队刷怪 ========================
/**
* 处理插队刷怪队列(每 0.15 秒尝试消费一个):
* 1. 找到后从队列中移除并生成怪物。
*/
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 spawnIndex = this.waveSpawnedCount++;
const row = spawnIndex % 3;
const col = Math.floor(spawnIndex / 3);
// 构造一个模拟的 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.addMonsterAtGrid(row, col, monData, item.level);
}
// ======================== 波次管理 ========================
/**
* 开始下一波:
* 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.setupWaveData(monsters);
}
private onPhasePrepareEnd() {
this.resetSlotSpawnData(this.currentWave);
// 准备结束阶段,立即刷出本波所有怪物
if (this.pendingMonsters.length > 0) {
let count = Math.min(this.pendingMonsters.length, MissionMonCompComp.MAX_MONSTERS);
for (let i = 0; i < count; i++) {
const monData = this.pendingMonsters.shift()!;
const row = this.waveSpawnedCount % 3;
const col = Math.floor(this.waveSpawnedCount / 3);
console.log(`[MissionMonComp] [PhasePrepareEnd] 准备生成怪物 UUID=${monData.uuid}, 当前已生成数量=${this.waveSpawnedCount}`);
this.addMonsterAtGrid(row, col, monData);
this.waveSpawnedCount++;
}
// 生成完毕后清空 pendingMonsters
this.pendingMonsters = [];
}
}
// ======================== 槽位管理 ========================
/**
* 重新分配本波所有怪物状态:
* 1. 清理上一波残留怪物。
* 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好。
*
* @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.waveSpawnedCount = 0;
}
// ======================== 怪物生成 ========================
/**
* 在指定层级、指定索引处生成一个怪物:
*
* @param row 行 (0, 1, 2)
* @param col 列 (0, 1, 2, 3)
* @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等)
* @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定)
*/
private addMonsterAtGrid(
row: number,
col: number,
monData: GeneratedMonster,
monLv: number = 1
) {
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
// 计算坐标
const spawnX = MissionMonCompComp.MON_SPAWN_START_X + col * MissionMonCompComp.MON_SPAWN_GAP_X;
const randomY = Math.random() * 20 - 10; // -10 到 10 的随机Y轴偏移
const landingY = BoxSet.GAME_LINE + MissionMonCompComp.ROW_Y_OFFSETS[row] + randomY + (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, row);
// 设置渲染排序
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();
}
}