feat(关卡系统): 实现章节-关-节三级关卡推进机制

本次提交重构了关卡系统,实现了完整的小节级关卡推进流程:
1. 新增关卡配置表StageSet,定义章节-关-节三级结构与怪物数值公式
2. 重构刷怪逻辑,按小节配置生成对应怪物波次
3. 实现关卡进度存档与恢复,支持断点续玩
4. 新增结算弹窗分支逻辑,区分小节胜利/关卡通关/失败状态
5. 优化胜利按钮文案与显示逻辑,将"再来一局"改为"下一关"
6. 新增一套完整的关卡事件系统,驱动各模块协同工作
This commit is contained in:
pan
2026-09-10 17:30:37 +08:00
parent 270f734a6a
commit 7b9545e2a8
9 changed files with 804 additions and 308 deletions

View File

@@ -3,8 +3,11 @@
* @description 怪物(Monster)登场管理组件(逻辑层)
*
* 职责:
* 1. 预生成战斗怪物:FightReady 时通过 RogueSpawningEngine.generateBossCombo 随机一组 Boss 组合,并立即登场展示(供玩家侦查后再选英雄)。
* 2. 战斗开始:PhasePrepareEnd 时恢复怪物行动(in_fight 前怪物仅原地待命,不会推进/索敌)。
* 1. 按小节配置刷怪:监听 SectionStart 事件,从 StageSpawningEngine 获取怪物列表。
* 2. 普通关:前 3 只立即刷,后续每 5s 刷 1 只(共 9 只)。
* 3. 精英关:3 只精英同时刷出。
* 4. Boss 关:2 精英先刷,Boss 延迟 1s 刷出。
* 5. 维护小节刷怪计数,供 MissionComp 检测小节结束。
*
* 关键设计:
* - 怪物按类型落排(近战/Boss→前排、远程/刺客→中排、辅助/召唤→后排),与英雄三排镜像对应。
@@ -24,6 +27,7 @@ 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;
@@ -77,68 +81,116 @@ export class MissionMonCompComp extends CCComp {
/** 全局生成顺序计数器(用于渲染层级排序) */
private globalSpawnOrder: number = 0;
/** 战斗场次(本局恒为 1,保留供 HUD/事件协议) */
private currentWave: number = 1;
/** 本场战斗的怪物总数 */
private waveTargetCount: number = 0;
/** 已登场的怪物数量 */
private waveSpawnedCount: number = 0;
/** 待登场队列(FightReady 时整组进入,PhasePrepareEnd 时一次全量刷出) */
private spawnQueue: GeneratedMonster[] = [];
/** 当前小节目标怪物总数 */
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.FightReady, this.fight_ready, this);
this.on(GameEvent.SectionStart, this.onSectionStart, this);
}
protected update(dt: number): void {
// 待登场统计(战斗结束检测与 HUD 进度消费)
smc.vmdata.mission_data.pending_mon_num = this.spawnQueue.length;
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() { }
/**
* 战斗准备:重置运行时状态并预生成本场 Boss 组合。
* 小节开始事件回调:生成怪物列表并按节奏刷出
* @param event 事件名
* @param data { chapter, stage, section }
*/
fight_ready() {
smc.vmdata.mission_data.mon_num = 0;
smc.mission.stop_spawn_mon = false;
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.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 = [];
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;
for (let i = 0; i < squad.length; i++) {
this.addMonsterAtGrid(i, squad[i], this.currentWave);
this.waveSpawnedCount++;
}
// 从生成引擎获取本小节怪物列表
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] Boss 组合生成完成,怪物总数: ${this.waveTargetCount}`);
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: this.currentWave,
total: this.waveTargetCount,
wave: section,
total: this.sectionTargetCount,
bossWave: monsters.some(m => m.isBoss),
});
}
@@ -146,29 +198,11 @@ export class MissionMonCompComp extends CCComp {
// ======================== 怪物生成 ========================
/**
* 解析怪物出生 X:按编队优先级名次映射三排锚点(与英雄编队排队逻辑一致)。
* 名次映射:rank0→前排(MON_ROW_FRONT_X)、rank1→中排(MON_ROW_MID_X)、rank≥2→后排(MON_ROW_BACK_X)。
* 效果:高优先级类型永远占据更靠前的排;当更高优先级类型插入时,低优先级类型自动向后让位。
*
* @param index 当前怪物在排序后列表中的索引(名次)
* @returns 出生 X 坐标
* 刷出单个怪物(统一入口)
* @param monData 怪物生成数据
*/
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
) {
private spawnMonster(monData: GeneratedMonster) {
const posIndex = monData.spawnIndex;
let mon = ecs.getEntity<Monster>(Monster);
let scale = -1;
@@ -183,7 +217,7 @@ export class MissionMonCompComp extends CCComp {
(mon as any)._testSkills = monData.skills;
}
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex);
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monData.lv, posIndex);
// oops.message 全局事件总线:Boss 登场(震屏/音效由表现层消费)
if (monData.isBoss) {
@@ -202,6 +236,22 @@ export class MissionMonCompComp extends CCComp {
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 组件移除时触发 */