Files
pixelheros/assets/script/game/map/MissionMonComp.ts
pan ab32b0e09d refactor(普通关刷怪): 调整刷怪间隔并新增怪物砸地击退效果
1. 将普通关后续刷怪间隔从1秒改为2秒,同步更新注释与配置常量
2. 为普通关怪物添加从400码高度掉落入场的效果
3. 新增怪物落地震退逻辑,击退落点100码范围内的英雄
2026-09-11 15:39:57 +08:00

260 lines
10 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 只立即刷,后续每 2s 刷 1 只(共 9 只),均从高处掉落入场。
* 3. 精英关:3 只精英同时刷出。
* 4. Boss 关:2 精英先刷,Boss 延迟 1s 刷出。
* 5. 维护小节刷怪计数,供 MissionComp 检测小节结束。
*
* 关键设计:
* - 怪物按类型落排(近战/Boss→前排、远程/刺客→中排、辅助/召唤→后排),与英雄三排镜像对应。
* - 实际推进由统一 MoveComp 在战斗中向敌人移动自然形成,不再使用固定网格点。
* - 槽位索引仅用于 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, 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 {
// ======================== 常量 ========================
/** 怪物出生掉落高度(码):从高处砸落入场,落地时震退范围内英雄 */
private static readonly MON_DROP_HEIGHT = 400;
/**
* 怪物类型 → 编队优先级(与英雄 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;
/** 待刷出队列(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 (const mon of immediate) {
if (mon.delay && mon.delay > 0) {
// Boss 延迟刷出
this.scheduleOnce(() => {
this.spawnMonster(mon);
}, mon.delay);
} else {
this.spawnMonster(mon);
}
}
// 剩余怪物进入待刷队列,启动定时刷怪
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 怪物生成数据
*/
private spawnMonster(monData: GeneratedMonster) {
const posIndex = monData.spawnIndex;
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length];
const spawnX = this.resolveSpawnX(posIndex);
const landingY = basePos.y;
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 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;
}
// 应用引擎计算好的最终属性
const model = mon.get(HeroAttrsComp);
if (model) {
model.ap = monData.ap;
model.hp_max = monData.hp;
model.hp = model.hp_max;
}
this.sectionSpawnedCount++;
}
/**
* 解析怪物出生 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() { }
}