1. 新增整局跨回合刷怪统计与重置能力,替换原小节统计 2. 修改奖励配置为整局5回合累计结算逻辑 3. 调整失败奖励判定为整局维度统计 4. 重构胜利/失败奖励计算逻辑,适配多回合关卡
276 lines
11 KiB
TypeScript
276 lines
11 KiB
TypeScript
/**
|
||
* @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 (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 landingX = this.resolveSpawnX(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;
|
||
}
|
||
|
||
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() { }
|
||
}
|