feat(关卡系统): 实现章节-关-节三级关卡推进机制
本次提交重构了关卡系统,实现了完整的小节级关卡推进流程: 1. 新增关卡配置表StageSet,定义章节-关-节三级结构与怪物数值公式 2. 重构刷怪逻辑,按小节配置生成对应怪物波次 3. 实现关卡进度存档与恢复,支持断点续玩 4. 新增结算弹窗分支逻辑,区分小节胜利/关卡通关/失败状态 5. 优化胜利按钮文案与显示逻辑,将"再来一局"改为"下一关" 6. 新增一套完整的关卡事件系统,驱动各模块协同工作
This commit is contained in:
@@ -3,28 +3,25 @@
|
||||
* @description 任务(关卡)核心控制组件(UI + 逻辑层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理单局游戏的 **完整生命周期**:初始化 → 准备阶段 → 战斗阶段 → 结算。
|
||||
* 1. 管理单局游戏的 **完整生命周期**:初始化 → 小节循环(战斗→结算弹窗)→ 章节通关。
|
||||
* 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。
|
||||
* 3. 管理怪物数量阈值(暂停 / 恢复刷怪的上下限)。
|
||||
* 4. 处理新一回合事件(NewWave),进入准备阶段并发放金币奖励。
|
||||
* 3. 驱动小节推进:SectionStart → 战斗 → VictoryComp 结算 → 下一节/下一关。
|
||||
* 4. 处理关卡进度持久化(chapter / stage / section)。
|
||||
* 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。
|
||||
* 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。
|
||||
*
|
||||
* 关键设计:
|
||||
* - mission_start() 初始化所有游戏数据 → 进入准备阶段 → 显示 loading。
|
||||
* - 准备阶段(enterPreparePhase):停止刷怪,显示开始按钮。
|
||||
* - 战斗阶段(to_fight):开始刷怪,隐藏按钮,由 update 驱动。
|
||||
* - 怪物数量管理采用 max/resume 双阈值:
|
||||
* * 超过 max → 暂停刷怪(stop_spawn_mon=true)
|
||||
* * 降至 resume 以下 → 恢复刷怪
|
||||
* - mission_start() 初始化所有游戏数据 → 恢复存档进度 → 进入第一小节。
|
||||
* - 每节即一局:怪物全灭后弹 VictoryComp 结算。
|
||||
* - 第 1~9 节胜利点"下一节"继续;第 10 节胜利显示"本关通关",确认退出并存档进下一关。
|
||||
* - 每节完全独立:进入下一节时重派 MissionStart 触发三选一,英雄重新召唤。
|
||||
* - cleanComponents() 在任务开始/结束时销毁所有英雄和技能 ECS 实体。
|
||||
* - clearBattlePools() 回收对象池(Monster / Skill / Tooltip)。
|
||||
*
|
||||
* 依赖:
|
||||
* - smc.mission —— 全局任务运行状态(play / pause / in_fight / stop_spawn_mon 等)
|
||||
* - smc.vmdata.mission_data —— 局内数据(金币 / 回合数 / 怪物数量等)
|
||||
* - FightSet —— 战斗常量配置
|
||||
* - CardInitCoins —— 初始金币数
|
||||
* - smc.vmdata.mission_data —— 局内数据(金币 / 回合数 / 怪物数量 / 关卡进度等)
|
||||
* - StageSet —— 关卡配置与数值公式
|
||||
* - UIID.Victory —— 结算弹窗
|
||||
*/
|
||||
import { _decorator, Vec3, Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label, CCInteger, tween, v3, Tween, Widget, UIOpacity } from "cc";
|
||||
@@ -49,6 +46,9 @@ import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
||||
import { FieldSkillType } from "../common/config/SkillSet";
|
||||
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
||||
import { spawningEngine, WAVE_DURATION, BATTLE_TIMEOUT } from "./RogueConfig";
|
||||
import {
|
||||
getProgressText,
|
||||
} from "../common/config/StageSet";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 任务(关卡)生命周期阶段 */
|
||||
@@ -60,16 +60,19 @@ export enum MissionPhase {
|
||||
BattleStart = 4, // 战斗开始阶段 (2s)
|
||||
Battle = 5, // 战斗阶段 (刷怪、战斗中)
|
||||
BattleEnd = 6, // 战斗结束阶段 (2s)
|
||||
Settle = 7 // 结算阶段
|
||||
Settle = 7, // 结算阶段(VictoryComp 弹窗,等待玩家点击下一节/下一关/退出)
|
||||
}
|
||||
|
||||
/** 结算类型(驱动 VictoryComp 按钮分流与文案) */
|
||||
type SettleType = "defeat" | "section" | "final";
|
||||
|
||||
//@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理
|
||||
|
||||
/**
|
||||
* MissionComp —— 任务(关卡)核心控制器
|
||||
*
|
||||
* 驱动单局游戏的完整流程:准备 → 战斗 → 结算。
|
||||
* 管理战斗计时、怪物数量控制、英雄全灭检测和金币奖励发放。
|
||||
* 驱动单局游戏的完整流程:准备 → 战斗 → 小节结算 → 大关结算 → 章节结算。
|
||||
* 管理战斗计时、怪物数量控制、英雄全灭检测和关卡进度推进。
|
||||
*/
|
||||
@ccclass('MissionComp')
|
||||
@ecs.register('MissionComp', false)
|
||||
@@ -102,7 +105,7 @@ export class MissionComp extends CCComp {
|
||||
[MissionPhase.BattleStart]: "战斗开始",
|
||||
[MissionPhase.Battle]: "战斗中",
|
||||
[MissionPhase.BattleEnd]: "战斗结束",
|
||||
[MissionPhase.Settle]: "结算阶段"
|
||||
[MissionPhase.Settle]: "结算阶段",
|
||||
};
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
@@ -171,11 +174,24 @@ export class MissionComp extends CCComp {
|
||||
private isWaveCountdown: boolean = false;
|
||||
/** 本局是否胜利(open_Victory 时记录,供结算弹窗使用) */
|
||||
private _victory: boolean = false;
|
||||
/** 本次结算类型(小节胜利/末关通关/失败,供结算弹窗按钮分流) */
|
||||
private _settleType: SettleType = "defeat";
|
||||
/** 末关通关时结算页展示的标题("本关通关"/"本章通关"),其余结算为空 */
|
||||
private _settleTitle: string = "";
|
||||
/** 结算弹窗延迟(秒):等待最后一个英雄/怪物死亡动画播完再切 BattleEnd */
|
||||
private static readonly SETTLE_DELAY = 1.5;
|
||||
/** 结算是否已预约调度(防止 0.2s 同步周期内重复 scheduleOnce) */
|
||||
private _settleScheduled: boolean = false;
|
||||
|
||||
// ======================== 关卡进度状态 ========================
|
||||
|
||||
/** 当前章节号 */
|
||||
public currentChapter: number = 1;
|
||||
/** 当前关卡号(1~10) */
|
||||
public currentStage: number = 1;
|
||||
/** 当前小节号(1~10) */
|
||||
public currentSection: number = 1;
|
||||
|
||||
// ======================== ECS 查询匹配器(预缓存) ========================
|
||||
|
||||
/** 匹配拥有 HeroViewComp 的实体(英雄/怪物视图) */
|
||||
@@ -196,6 +212,8 @@ export class MissionComp extends CCComp {
|
||||
this.on(GameEvent.MissionEnd, this.mission_end, this)
|
||||
this.on(GameEvent.NewWave, this.onNewWave, this)
|
||||
this.on(GameEvent.DO_AD_BACK, this.do_ad, this)
|
||||
// 结算弹窗"下一节"按钮推进关卡
|
||||
this.on(GameEvent.NextSection, this.onNextSection, this)
|
||||
this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||||
// 开局免费三选一选完后自动进入战斗
|
||||
oops.message.on("OpeningPicksDone", this.onOpeningPicksDone, this);
|
||||
@@ -317,7 +335,7 @@ export class MissionComp extends CCComp {
|
||||
if (phaseNode) {
|
||||
const label = phaseNode.getComponent(Label);
|
||||
if (label) {
|
||||
label.string = `下一回合 ${remain}s`;
|
||||
label.string = `下一节 ${remain}s`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,15 +378,22 @@ export class MissionComp extends CCComp {
|
||||
* 1. 取消上一局延迟回调。
|
||||
* 2. 清理残留实体。
|
||||
* 3. 初始化全部局内数据。
|
||||
* 4. 分发 FightReady 事件。
|
||||
* 4. 恢复关卡进度存档。
|
||||
* 5. 进入准备阶段并显示 loading。
|
||||
*/
|
||||
async mission_start() {
|
||||
this.unscheduleAllCallbacks();
|
||||
this.cleanComponents();
|
||||
this.data_init()
|
||||
this.loadStageProgress();
|
||||
oops.message.dispatchEvent(GameEvent.FightReady)
|
||||
this.changePhase(MissionPhase.Prepare)
|
||||
this.changePhase(MissionPhase.Prepare);
|
||||
// 派发首个 SectionStart,MissionMonComp 据此生成开局怪物
|
||||
oops.message.dispatchEvent(GameEvent.SectionStart, {
|
||||
chapter: this.currentChapter,
|
||||
stage: this.currentStage,
|
||||
section: this.currentSection,
|
||||
});
|
||||
let loading = this.node.getChildByName("loading")
|
||||
if (loading) {
|
||||
loading.active = true
|
||||
@@ -468,8 +493,7 @@ export class MissionComp extends CCComp {
|
||||
if (phaseNode) {
|
||||
const label = phaseNode.getComponent(Label);
|
||||
if (label) {
|
||||
const wave = Math.max(1, this.currentWave || (smc.vmdata && smc.vmdata.mission_data ? smc.vmdata.mission_data.level : 1) || 1);
|
||||
label.string = `第 ${wave} 回合`;
|
||||
label.string = `第 ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)} 节`;
|
||||
}
|
||||
|
||||
// 阶段切换动感表现:只在进入战斗阶段跳动一下,让流程充满心流体验
|
||||
@@ -642,7 +666,10 @@ export class MissionComp extends CCComp {
|
||||
all_alive: this.checkAllHeroAlive(),
|
||||
rewards: this.rewards,
|
||||
game_data: this.game_data,
|
||||
can_revive: this.revive_times > 0
|
||||
can_revive: this.revive_times > 0,
|
||||
// 结算类型:小节胜利显示"下一节",末关通关显示"确认退出" + 通关标题
|
||||
settle_type: this._settleType,
|
||||
settle_title: this._settleTitle,
|
||||
});
|
||||
} else {
|
||||
// 如果 play 已经是 false,说明是通过 fight_end 进来的
|
||||
@@ -732,6 +759,12 @@ export class MissionComp extends CCComp {
|
||||
this.to_fight();
|
||||
}
|
||||
|
||||
/** 结算弹窗"下一节"事件:关闭弹窗并推进到下一小节 */
|
||||
private onNextSection() {
|
||||
oops.gui.remove(UIID.Victory);
|
||||
this.advanceToNextSection();
|
||||
}
|
||||
|
||||
/** 开局免费三选一选完,自动进入战斗(等效于点击开始按钮) */
|
||||
private onOpeningPicksDone() {
|
||||
if (this.currentPhase !== MissionPhase.Prepare) return;
|
||||
@@ -743,7 +776,8 @@ export class MissionComp extends CCComp {
|
||||
/**
|
||||
* 打开结算弹窗:
|
||||
* - 暂停游戏
|
||||
* - 打开 VictoryComp 弹窗
|
||||
* - 按当前进度判定结算类型(小节胜利 / 末关通关 / 失败)
|
||||
* - 切入 BattleEnd,2s 后由 autoNextPhase 自动弹出 VictoryComp
|
||||
*
|
||||
* @param e 事件对象(未使用)
|
||||
* @param is_hero_dead 是否因英雄全灭触发
|
||||
@@ -752,8 +786,35 @@ export class MissionComp extends CCComp {
|
||||
open_Victory(e: any, is_hero_dead: boolean = false, victory: boolean = false) {
|
||||
// 战斗失败或胜利,标记暂停状态以切断回合流转逻辑
|
||||
smc.mission.pause = true;
|
||||
// 记录胜利标记,供 BattleEnd 阶段结算弹窗使用
|
||||
// 记录胜利标记,供结算弹窗使用
|
||||
this._victory = victory;
|
||||
this._settleTitle = "";
|
||||
|
||||
if (!victory) {
|
||||
// 失败:进度不变,结算窗走复活/退出流程
|
||||
this._settleType = "defeat";
|
||||
} else if (this.currentSection < 10) {
|
||||
// 第 1~9 节胜利:还有下一节,结算窗显示"下一节"
|
||||
this._settleType = "section";
|
||||
} else {
|
||||
// 第 10 节胜利:本关最后一节,无"下一节"按钮,结算页显示通关标题、确认即退出
|
||||
this._settleType = "final";
|
||||
this._settleTitle = this.currentStage >= 10 ? "本章通关" : "本关通关";
|
||||
// 存档直接推进到下一关第 1 小节(仅写存档,内存进度保留用于本局结算显示);
|
||||
// 第 10 关通关即本章全通,存档停留等待后续章节扩展
|
||||
if (this.currentStage < 10) {
|
||||
smc.data.stage_progress = {
|
||||
chapter: this.currentChapter,
|
||||
stage: this.currentStage + 1,
|
||||
section: 1,
|
||||
};
|
||||
if ((smc as any).updateCloudData) {
|
||||
(smc as any).updateCloudData();
|
||||
}
|
||||
mLogger.log(this.debugMode, 'MissionComp',
|
||||
`[StageProgress] 本关通关,存档推进: ${this.currentChapter}-${this.currentStage + 1}-1`);
|
||||
}
|
||||
}
|
||||
// 直接切入 BattleEnd,2s 后由 autoNextPhase 自动流转弹窗结算
|
||||
this.changePhase(MissionPhase.BattleEnd);
|
||||
}
|
||||
@@ -835,6 +896,83 @@ export class MissionComp extends CCComp {
|
||||
smc.vmdata.scores.gold_earned += smc.vmdata.mission_data.coin;
|
||||
}
|
||||
|
||||
// ======================== 关卡进度管理 ========================
|
||||
|
||||
/**
|
||||
* 加载关卡进度存档
|
||||
* 从 smc.data.stage_progress 恢复,缺省从 1-1-1 开始
|
||||
*/
|
||||
private loadStageProgress() {
|
||||
const progress = smc.data.stage_progress;
|
||||
if (progress && progress.chapter >= 1 && progress.stage >= 1 && progress.section >= 1) {
|
||||
this.currentChapter = progress.chapter;
|
||||
this.currentStage = progress.stage;
|
||||
this.currentSection = progress.section;
|
||||
} else {
|
||||
this.currentChapter = 1;
|
||||
this.currentStage = 1;
|
||||
this.currentSection = 1;
|
||||
}
|
||||
mLogger.log(this.debugMode, 'MissionComp',
|
||||
`[StageProgress] 加载进度: ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存关卡进度存档
|
||||
* 写入 smc.data.stage_progress,并标记云端同步
|
||||
*/
|
||||
private saveStageProgress() {
|
||||
smc.data.stage_progress = {
|
||||
chapter: this.currentChapter,
|
||||
stage: this.currentStage,
|
||||
section: this.currentSection,
|
||||
};
|
||||
// 标记数据脏,触发云端/本地同步
|
||||
if ((smc as any).updateCloudData) {
|
||||
(smc as any).updateCloudData();
|
||||
}
|
||||
mLogger.log(this.debugMode, 'MissionComp',
|
||||
`[StageProgress] 保存进度: ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推进到下一小节
|
||||
* 结算弹窗保证:第 10 节胜利时不显示"下一节",不会触发本方法
|
||||
*/
|
||||
private advanceToNextSection() {
|
||||
this.currentSection++;
|
||||
this.startNextSectionBattle();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始下一小节战斗:
|
||||
* 1. 清理上一节战场(英雄/技能实体 + 对象池)。
|
||||
* 2. 重置局内数据(金币/计时/标志位)。
|
||||
* 3. 重派 MissionStart 触发开局免费三选一(MissionCardComp 监听)。
|
||||
* 4. 进入准备阶段并派发 SectionStart 生成新小节怪物。
|
||||
* 5. 保存进度存档(精确到待挑战小节)。
|
||||
*/
|
||||
private startNextSectionBattle() {
|
||||
this.cleanComponents();
|
||||
this.clearBattlePools();
|
||||
this.data_init();
|
||||
this.changePhase(MissionPhase.Prepare);
|
||||
|
||||
// 重新三选一:MissionCardComp 监听 MissionStart 重置开局免费抽卡
|
||||
oops.message.dispatchEvent(GameEvent.MissionStart, {});
|
||||
oops.message.dispatchEvent(GameEvent.FightReady);
|
||||
// 触发 SectionStart 事件,MissionMonComp 据此生成新小节怪物
|
||||
oops.message.dispatchEvent(GameEvent.SectionStart, {
|
||||
chapter: this.currentChapter,
|
||||
stage: this.currentStage,
|
||||
section: this.currentSection,
|
||||
});
|
||||
|
||||
this.saveStageProgress();
|
||||
mLogger.log(this.debugMode, 'MissionComp',
|
||||
`[StageProgress] 进入小节: ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)}`);
|
||||
}
|
||||
|
||||
// ======================== 回合管理 ========================
|
||||
|
||||
/**
|
||||
@@ -902,7 +1040,7 @@ export class MissionComp extends CCComp {
|
||||
});
|
||||
this.handleHeroWipe(heroCount);
|
||||
|
||||
// 怪物全灭检测:一场定胜负,全灭即整局通关,直接进入结算
|
||||
// 怪物全灭检测:本小节怪物全灭 + 待刷队列空 → 弹结算窗(小节胜利,按钮按进度显示下一节/下一关)
|
||||
const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0;
|
||||
if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
|
||||
// oops.message 全局事件总线:清屏庆祝(横幅/奖励由 BattleBannerComp 消费),需在结算前派发(clearTime 归零前取值)
|
||||
@@ -915,7 +1053,7 @@ export class MissionComp extends CCComp {
|
||||
allAlive,
|
||||
fastClear,
|
||||
});
|
||||
// 最后一个怪物死亡:延迟结算,等待死亡动画播放完再弹窗
|
||||
// 小节胜利:延迟弹结算窗,等待死亡动画播放完
|
||||
this.scheduleSettle(false, true);
|
||||
return;
|
||||
}
|
||||
@@ -961,6 +1099,7 @@ export class MissionComp extends CCComp {
|
||||
// 延迟期间局内状态可能已变化(如复活、任务结束),兜底校验
|
||||
if (!smc.mission.play || smc.mission.pause) return;
|
||||
if (this.currentPhase !== MissionPhase.Battle) return;
|
||||
// 胜利/失败统一走结算弹窗:open_Victory 内部按进度判定小节/大关/章节结算类型
|
||||
this.open_Victory(null, is_hero_dead, victory);
|
||||
}, MissionComp.SETTLE_DELAY);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user