Files
pixelheros/assets/script/game/map/MissionMonComp.ts
panFD 1754e0d408 fix(map,hero): 完善怪物入场排队逻辑
1. 给怪物刷出生成方法添加rank参数,用于计算出生位置
2. 新增怪物入场归位逻辑,替换原有的闲置状态处理
3. 更新注释描述,修正准备阶段怪物行为说明
2026-09-13 11:50:05 +08:00

280 lines
11 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. 按小节配置刷怪:监听 SectionStart 事件,从 StageSpawningEngine 获取怪物列表。
* 2. 普通节:3 只小怪立即刷出;精英节:3 只精英同刷;Boss 节:2 小怪 + 1 Boss(延迟 1s)。
* 3. 怪物统一从右侧屏幕外跑入(贴地向左推进),不再高空掉落。
* 4. 维护小节刷怪计数,供 MissionComp 检测小节结束。
*
* 关键设计:
* - 怪物按类型落排(近战/Boss→前排、远程/刺客→中排、辅助/召唤→后排),与英雄三排镜像对应。
* - 出生 X 取目标排 X + 右侧偏移(屏幕外),dropToY=GAME_LINE(无下落),
* MoveComp.targetX 指向目标排 X,落地即 moving=true 自然从右跑入。
* - 槽位索引仅用于 monGrid 寻路与 SCastSystem 索敌定位。
*/
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 { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import { BoxSet, FacSet, FightSet } from "../common/config/GameSet";
import { MonType } from "../common/config/heroSet";
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { MoveComp } from "../hero/MoveComp";
import { NORMAL_FIRST_SPAWN, NORMAL_SPAWN_INTERVAL } from "../common/config/StageSet";
const { ccclass, property } = _decorator;
@ccclass('MissionMonCompComp')
@ecs.register('MissionMonComp', false)
export class MissionMonCompComp extends CCComp {
// ======================== 常量 ========================
/** 怪物出生右侧偏移(码):目标排 X 再加该偏移,使出生点位于屏幕右侧外,贴地从右跑入 */
private static readonly MON_SPAWN_OFFSET_X = 380;
/**
* 怪物类型 → 编队优先级(与英雄 HRole 枚举值升序一致):
* 近战/重型/近战Boss → 0(最优先,占前排),
* 远程/刺客/远程Boss → 1(占中排),
* 辅助/召唤师 → 2(占后排)。
*/
private static readonly MON_TYPE_PRIORITY: Record<number, number> = {
[MonType.Melee]: 0,
[MonType.Heavy]: 0,
[MonType.MeleeBoss]: 0,
[MonType.Long]: 1,
[MonType.Assassin]: 1,
[MonType.LongBoss]: 1,
[MonType.Support]: 2,
[MonType.Summoner]: 2,
};
/** 槽位基准 Y(3行×4列结构仅供 monGrid 寻路与 SCastSystem 索敌定位) */
public static readonly MON_POSITIONS: Vec3[] = [
v3(0, BoxSet.GAME_LINE, 0), // index 0: Col1-Top
v3(0, BoxSet.GAME_LINE, 0), // index 1: Col1-Mid
v3(0, BoxSet.GAME_LINE, 0), // index 2: Col1-Bot
v3(0, BoxSet.GAME_LINE, 0), // index 3: Col2-Top
v3(0, BoxSet.GAME_LINE, 0), // index 4: Col2-Mid
v3(0, BoxSet.GAME_LINE, 0), // index 5: Col2-Bot
v3(0, BoxSet.GAME_LINE, 0), // index 6: Col3-Top
v3(0, BoxSet.GAME_LINE, 0), // index 7: Col3-Mid
v3(0, BoxSet.GAME_LINE, 0), // index 8: Col3-Bot
v3(0, BoxSet.GAME_LINE, 0), // index 9: Col4-Top
v3(0, BoxSet.GAME_LINE, 0), // index 10: Col4-Mid
v3(0, BoxSet.GAME_LINE, 0), // index 11: Col4-Bot
];
// ======================== 编辑器属性 ========================
@property({ tooltip: "是否启用调试日志" })
private debugMode: boolean = false;
// ======================== 运行时状态 ========================
/** 全局生成顺序计数器(用于渲染层级排序) */
private globalSpawnOrder: number = 0;
/** 当前小节目标怪物总数 */
private sectionTargetCount: number = 0;
/** 当前小节已刷出怪物数量 */
private sectionSpawnedCount: number = 0;
/** 整局(整关 5 回合)累计刷出怪物总数:跨回合累计,仅在整关重置(由 MissionComp.data_init 触发) */
private missionSpawnedCount: number = 0;
/** 待刷出队列(batch=1 的怪物) */
private pendingQueue: GeneratedMonster[] = [];
/** 刷怪定时器已累积时间 */
private spawnTimer: number = 0;
/** 是否正在刷怪中 */
private isSpawning: boolean = false;
// ======================== 生命周期 ========================
onLoad() {
this.on(GameEvent.SectionStart, this.onSectionStart, this);
}
protected update(dt: number): void {
// 待登场统计(战斗结束检测与 HUD 进度消费)
smc.vmdata.mission_data.pending_mon_num = this.pendingQueue.length;
// 定时刷怪:仅战斗阶段且非暂停时推进
if (!this.isSpawning || !smc.mission.in_fight || smc.mission.pause) return;
if (this.pendingQueue.length === 0) {
this.isSpawning = false;
return;
}
this.spawnTimer += dt;
if (this.spawnTimer >= NORMAL_SPAWN_INTERVAL) {
this.spawnTimer = 0;
const mon = this.pendingQueue.shift();
if (mon) {
this.spawnMonster(mon);
}
}
}
start() { }
/**
* 小节开始事件回调:生成怪物列表并按节奏刷出
* @param event 事件名
* @param data { chapter, stage, section }
*/
private onSectionStart(event: string, data: any) {
const chapter = Number(data?.chapter ?? 1);
const stage = Number(data?.stage ?? 1);
const section = Number(data?.section ?? 1);
// 重置小节状态
this.globalSpawnOrder = 0;
this.sectionSpawnedCount = 0;
this.pendingQueue = [];
this.spawnTimer = 0;
this.isSpawning = false;
smc.vmdata.mission_data.mon_num = 0;
smc.vmdata.mission_data.pending_mon_num = 0;
// 从生成引擎获取本小节怪物列表
const monsters = spawningEngine.generateSectionMonsters(chapter, stage, section);
this.sectionTargetCount = monsters.length;
if (TestModeConfig.enable) {
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启");
}
mLogger.log(this.debugMode, 'MissionMonComp',
`[MissionMonComp] 小节 ${chapter}-${stage}-${section} 生成完成,怪物总数: ${this.sectionTargetCount}`);
// 按批次拆分:batch=0 立即刷,batch=1 进入待刷队列
const immediate: GeneratedMonster[] = [];
const delayed: GeneratedMonster[] = [];
for (const mon of monsters) {
if (mon.batch === 0) {
immediate.push(mon);
} else {
delayed.push(mon);
}
}
// 按编队优先级排序立即刷出的怪物
immediate.sort((a, b) => {
const pa = MissionMonCompComp.MON_TYPE_PRIORITY[a.type] ?? 1;
const pb = MissionMonCompComp.MON_TYPE_PRIORITY[b.type] ?? 1;
return pa - pb;
});
// 立即刷出 batch=0 的怪物(处理 delay 字段)
for (let i = 0; i < immediate.length; i++) {
const mon = immediate[i];
if (mon.delay && mon.delay > 0) {
// Boss 延迟刷出
this.scheduleOnce(() => {
this.spawnMonster(mon, i);
}, mon.delay);
} else {
this.spawnMonster(mon, i);
}
}
// 剩余怪物进入待刷队列,启动定时刷怪
this.pendingQueue = delayed;
if (this.pendingQueue.length > 0) {
this.isSpawning = true;
}
// 广播小节开始事件(供 HUD 显示进度)
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: section,
total: this.sectionTargetCount,
bossWave: monsters.some(m => m.isBoss),
});
}
// ======================== 怪物生成 ========================
/**
* 刷出单个怪物(统一入口)
* @param monData 怪物生成数据
* @param rank 编队优先级排序后的名次(0=前排,1=中排,≥2=后排);缺省回退到 spawnIndex
*/
private spawnMonster(monData: GeneratedMonster, rank?: number) {
const posIndex = monData.spawnIndex;
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
const landingX = this.resolveSpawnX(rank ?? posIndex);
const landingY = basePos.y;
// 出生点位于目标排右侧屏幕外(dropToY=landingY 无下落),落地即 moving=true 从右跑入
const spawnPos: Vec3 = v3(landingX + MissionMonCompComp.MON_SPAWN_OFFSET_X, landingY, 0);
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
if (monData.skills) {
(mon as any)._testSkills = monData.skills;
}
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monData.lv, posIndex);
// oops.message 全局事件总线:Boss 登场(震屏/音效由表现层消费)
if (monData.isBoss) {
oops.message.dispatchEvent(GameEvent.BossSpawn, { pos: spawnPos.clone() });
}
const move = mon.get(MoveComp);
if (move) {
move.spawnOrder = this.globalSpawnOrder;
// 推进目标指向目标排 X,而非出生点(出生点在屏幕外)
move.targetX = landingX;
}
// 应用引擎计算好的最终属性
const model = mon.get(HeroAttrsComp);
if (model) {
model.ap = monData.ap;
model.hp_max = monData.hp;
model.hp = model.hp_max;
// 精英标记:对象池复用实体需显式重置(Boss 与精英互斥,Boss 走 is_boss)
model.is_elite = !monData.isBoss && !!monData.isElite;
}
this.sectionSpawnedCount++;
this.missionSpawnedCount++;
}
/** 整局累计已刷出怪物总数(跨回合,整关重置):供 MissionComp 整局击杀统计 */
public getTotalSpawnedCount(): number {
return this.missionSpawnedCount;
}
/** 整关重置刷怪统计(由 MissionComp.data_init 在整关开始/重开时调用,回合间不重置) */
public resetMissionStats(): void {
this.missionSpawnedCount = 0;
}
/**
* 解析怪物出生 X:按编队优先级名次映射三排锚点(与英雄编队排队逻辑一致)。
* 名次映射:rank0→前排(MON_ROW_FRONT_X)、rank1→中排(MON_ROW_MID_X)、rank≥2→后排(MON_ROW_BACK_X)。
* 效果:高优先级类型永远占据更靠前的排;当更高优先级类型插入时,低优先级类型自动向后让位。
*
* @param index 当前怪物在排序后列表中的索引(名次)
* @returns 出生 X 坐标
*/
private resolveSpawnX(index: number): number {
if (index === 0) return FightSet.MON_ROW_FRONT_X;
if (index === 1) return FightSet.MON_ROW_MID_X;
return FightSet.MON_ROW_BACK_X;
}
/** ECS 组件移除时触发 */
reset() { }
}