Files
pixelheros/assets/script/game/map/MissionMonComp.ts
pan 9a24f0a9a5 refactor(missionMon): 调整怪物登场排布逻辑
将怪物从按X轴均匀分布改为按类型分三排登场,匹配英雄编队镜像布局,高优先级怪物自动占据前排
2026-08-20 17:30:36 +08:00

210 lines
8.7 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. 预生成战斗怪物FightReady 时通过 RogueSpawningEngine.generateBossCombo 随机一组 Boss 组合,并立即登场展示(供玩家侦查后再选英雄)。
* 2. 战斗开始PhasePrepareEnd 时恢复怪物行动in_fight 前怪物仅原地待命,不会推进/索敌)。
*
* 关键设计:
* - 怪物按类型落排(近战/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";
const { ccclass, property } = _decorator;
@ccclass('MissionMonCompComp')
@ecs.register('MissionMonComp', false)
export class MissionMonCompComp extends CCComp {
// ======================== 常量 ========================
/** 怪物出生掉落高度 */
private static readonly MON_DROP_HEIGHT = 0;
/**
* 怪物类型 → 编队优先级(与英雄 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,
};
/** 槽位基准 Y3行×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;
/** 战斗场次(本局恒为 1保留供 HUD/事件协议) */
private currentWave: number = 1;
/** 本场战斗的怪物总数 */
private waveTargetCount: number = 0;
/** 已登场的怪物数量 */
private waveSpawnedCount: number = 0;
/** 待登场队列FightReady 时整组进入PhasePrepareEnd 时一次全量刷出) */
private spawnQueue: GeneratedMonster[] = [];
// ======================== 生命周期 ========================
onLoad() {
this.on(GameEvent.FightReady, this.fight_ready, this);
}
protected update(dt: number): void {
// 待登场统计(战斗结束检测与 HUD 进度消费)
smc.vmdata.mission_data.pending_mon_num = this.spawnQueue.length;
}
start() { }
/**
* 战斗准备:重置运行时状态并预生成本场 Boss 组合。
*/
fight_ready() {
smc.vmdata.mission_data.mon_num = 0;
smc.mission.stop_spawn_mon = false;
this.globalSpawnOrder = 0;
this.currentWave = 1;
this.waveTargetCount = 0;
this.waveSpawnedCount = 0;
this.spawnQueue = [];
// 随机一组 Boss 组合(最终 Boss 战强度)
const monsters = spawningEngine.generateBossCombo();
this.spawnQueue = monsters;
this.waveTargetCount = monsters.length;
smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount;
// 怪物先行登场:玩家侦查 Boss 组合后再做英雄三选一in_fight 前怪物原地待命,不推进不索敌)
// 按编队优先级排序后依次登场:近战/Boss(0) → 远程/刺客(1) → 辅助/召唤(2)
const squad = [...this.spawnQueue].sort((a, b) => {
const pa = MissionMonCompComp.MON_TYPE_PRIORITY[a.type] ?? 1;
const pb = MissionMonCompComp.MON_TYPE_PRIORITY[b.type] ?? 1;
return pa - pb;
});
this.spawnQueue = [];
smc.vmdata.mission_data.pending_mon_num = 0;
for (let i = 0; i < squad.length; i++) {
this.addMonsterAtGrid(i, squad[i], this.currentWave);
this.waveSpawnedCount++;
}
if (TestModeConfig.enable) {
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启");
}
mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] Boss 组合生成完成,怪物总数: ${this.waveTargetCount}`);
oops.message.dispatchEvent(GameEvent.NewWave, {
wave: this.currentWave,
total: this.waveTargetCount,
bossWave: monsters.some(m => m.isBoss),
});
}
// ======================== 怪物生成 ========================
/**
* 解析怪物出生 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;
}
/**
* 在指定位置索引处生成一个怪物
* 怪物按编队优先级名次落排rank0→前排、rank1→中排、rank≥2→后排
* 与英雄编队排队逻辑一致:高优先级类型永远占更靠前的排,低优先级自动让位。
*/
private addMonsterAtGrid(
posIndex: number,
monData: GeneratedMonster,
monLv: number = 1
) {
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, monLv, 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;
}
}
/** ECS 组件移除时触发 */
reset() { }
}