From 1b2545deec5db6c6e01bd6d6ec47dd8a3d9455d7 Mon Sep 17 00:00:00 2001 From: panFD Date: Sat, 12 Sep 2026 19:03:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor(rogue):=20=E9=87=8D=E6=9E=84=E5=85=B3?= =?UTF-8?q?=E5=8D=A1=E5=88=B7=E6=80=AA=E4=B8=8E=E6=B5=81=E7=A8=8B=E4=B8=BA?= =?UTF-8?q?=E4=B8=80=E5=85=B3=E4=B8=80=E5=B1=80=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 将原10小节/关改为5小节/关,固定节类型为1-3普通节、4精英节、5Boss节 2. 调整刷怪逻辑:普通节3只小怪立即全刷,移除定时分批刷怪 3. 统一怪物入场方式为从右侧屏幕外贴地跑入,移除高空掉落动画 4. 移除小节间结算弹窗,改为自动连战,节间仅播放背景滚动过渡 5. 简化结算逻辑,仅保留通关/失败两种结算类型,移除小节胜利分支 6. 重构关卡进度存储,仅保存章节关卡,小节恒从1开始 --- assets/script/game/common/config/StageSet.ts | 70 +++-- assets/script/game/map/MissionComp.ts | 292 ++++++++----------- assets/script/game/map/MissionMonComp.ts | 21 +- assets/script/game/map/RogueConfig.ts | 16 +- assets/script/game/map/VictoryComp.ts | 57 ++-- 5 files changed, 196 insertions(+), 260 deletions(-) diff --git a/assets/script/game/common/config/StageSet.ts b/assets/script/game/common/config/StageSet.ts index 858ae08c..dcf0459d 100644 --- a/assets/script/game/common/config/StageSet.ts +++ b/assets/script/game/common/config/StageSet.ts @@ -3,8 +3,8 @@ * @description 关卡系统配置(章节-关-节三级结构) * * 设计要点: - * - 1 章 = 10 关,每关 = 10 小节,共 100 个小节 - * - 第 5 关为精英关,第 10 关为 Boss 关,其余为普通关 + * - 1 章 = 10 关,每关 = 5 小节(一关一局,节间自动推进),共 50 个小节 + * - 每关节点固定:1~3 节普通(3 小怪)、第 4 节精英(3 精英)、第 5 节 Boss(1 Boss + 2 小怪) * - 怪物等级 = 关卡号(1-1 的怪为 Lv1,1-5 的怪为 Lv5) * - 怪物属性 = 基础 × 开局系数 × (1 + 成长斜率 × (lv-1)) × 类型倍率 * @@ -26,26 +26,32 @@ export const MON_GROW_SLOPE = 0.05; export const ELITE_MUL = 1.5; /** Boss 属性倍率:同级普通怪的 3.0 倍(精英 × 2) */ export const BOSS_MUL = 3.0; -/** 普通关小怪数量 */ -export const NORMAL_MON_COUNT = 9; -/** 普通关开局立即刷出数量 */ +/** 每关小节总数(一关一局,节间自动推进) */ +export const SECTIONS_PER_STAGE = 5; +/** 精英节号(每关第 4 节为精英节) */ +export const ELITE_SECTION = 4; +/** Boss 节号(每关第 5 节为 Boss 节,即末节) */ +export const BOSS_SECTION = 5; +/** 普通节小怪数量(一关一局压缩后单节 3 只) */ +export const NORMAL_MON_COUNT = 3; +/** 普通节开局立即刷出数量(3 只全部立即刷出) */ export const NORMAL_FIRST_SPAWN = 3; -/** 普通关后续刷怪间隔(秒):前 3 只立即刷,后续每 2s 刷 1 只 */ +/** 普通节后续刷怪间隔(秒):保留配置位(3 只全立即刷,当前无后续批次) */ export const NORMAL_SPAWN_INTERVAL = 2; -/** 精英关精英数量 */ +/** 精英节精英数量 */ export const ELITE_COUNT = 3; -/** Boss 关精英数量 */ +/** Boss 节护卫小怪数量 */ export const BOSS_ELITE_COUNT = 2; -/** Boss 关 Boss 延迟刷出时间(秒) */ +/** Boss 节 Boss 延迟刷出时间(秒) */ export const BOSS_DELAY = 1; // ======================== 类型定义 ======================== /** 小节类型 */ export enum SectionType { - Normal = 0, // 普通关:9 小怪 - Elite = 1, // 精英关:3 精英 - Boss = 2, // Boss 关:1 Boss + 2 精英 + Normal = 0, // 普通节:3 小怪 + Elite = 1, // 精英节:3 精英 + Boss = 2, // Boss 节:1 Boss + 2 小怪 } /** 小节配置 */ @@ -62,13 +68,13 @@ export interface SectionConfig { monTypes?: MonType[]; } -/** 大关配置(1-1 ~ 1-10) */ +/** 大关配置(1-1 ~ 1-10),每关 SECTIONS_PER_STAGE 个小节 */ export interface StageConfig { /** 章节号 */ chapter: number; /** 关卡号 1~10 */ stage: number; - /** 10 个小节 */ + /** 小节列表(长度 = SECTIONS_PER_STAGE) */ sections: SectionConfig[]; } @@ -122,40 +128,40 @@ export function getMonsterLv(stage: number): number { } /** - * 判断是否为精英关 - * @param stage 关卡号(1~10) + * 判断是否为精英节(每关第 ELITE_SECTION 节) + * @param section 小节号(1~SECTIONS_PER_STAGE) */ -export function isEliteStage(stage: number): boolean { - return stage === 5; +export function isEliteSection(section: number): boolean { + return section === ELITE_SECTION; } /** - * 判断是否为 Boss 关 - * @param stage 关卡号(1~10) + * 判断是否为 Boss 节(每关第 BOSS_SECTION 节,即末节) + * @param section 小节号(1~SECTIONS_PER_STAGE) */ -export function isBossStage(stage: number): boolean { - return stage === 10; +export function isBossSection(section: number): boolean { + return section === BOSS_SECTION; } /** - * 获取小节类型 - * @param stage 关卡号(1~10) + * 获取小节类型(按节号区分:1~3 普通 / 4 精英 / 5 Boss) + * @param section 小节号(1~SECTIONS_PER_STAGE) */ -export function getSectionType(stage: number): SectionType { - if (isBossStage(stage)) return SectionType.Boss; - if (isEliteStage(stage)) return SectionType.Elite; +export function getSectionType(section: number): SectionType { + if (isBossSection(section)) return SectionType.Boss; + if (isEliteSection(section)) return SectionType.Elite; return SectionType.Normal; } // ======================== 第 1 章配置 ======================== -/** 生成单个大关的 10 个小节配置 */ +/** 生成单个大关的 SECTIONS_PER_STAGE 个小节配置(1~3 普通 / 4 精英 / 5 Boss) */ function makeStageSections(stage: number): SectionConfig[] { - const type = getSectionType(stage); const monLv = getMonsterLv(stage); const sections: SectionConfig[] = []; - for (let i = 1; i <= 10; i++) { + for (let i = 1; i <= SECTIONS_PER_STAGE; i++) { + const type = getSectionType(i); const section: SectionConfig = { type, monLv }; if (type === SectionType.Elite) { @@ -164,7 +170,7 @@ function makeStageSections(stage: number): SectionConfig[] { section.eliteCount = BOSS_ELITE_COUNT; // bossUuid 缺省随机,由生成引擎从 Boss 池抽取 } else { - // 普通关:近战为主,穿插远程/重型 + // 普通节:近战为主,穿插远程/重型 section.monTypes = [MonType.Melee, MonType.Melee, MonType.Melee, MonType.Heavy, MonType.Long]; } @@ -200,7 +206,7 @@ export const ChapterSet: Record = { * 获取小节配置 * @param chapter 章节号 * @param stage 关卡号 - * @param section 小节号(1~10) + * @param section 小节号(1~SECTIONS_PER_STAGE) * @returns 小节配置,不存在时返回 null */ export function getSectionConfig(chapter: number, stage: number, section: number): SectionConfig | null { diff --git a/assets/script/game/map/MissionComp.ts b/assets/script/game/map/MissionComp.ts index 3307ed75..04fa8646 100644 --- a/assets/script/game/map/MissionComp.ts +++ b/assets/script/game/map/MissionComp.ts @@ -3,26 +3,19 @@ * @description 任务(关卡)核心控制组件(UI + 逻辑层) * * 职责: - * 1. 管理单局游戏的 **完整生命周期**:初始化 → 小节循环(战斗→结算弹窗)→ 章节通关。 - * 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。 - * 3. 驱动小节推进:SectionStart → 战斗 → VictoryComp 结算 → 下一节/下一关。 - * 4. 处理关卡进度持久化(chapter / stage / section)。 - * 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。 - * 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。 + * 1. 管理单关(一关一局)的 **完整生命周期**:初始化 → 5 节自动连战 → 本关结算。 + * 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭 / 怪物全灭。 + * 3. 驱动节间自动推进:节间不弹结算,背景滚动过渡 + 英雄归位 + 怪物从右侧跑入,自动开下一节。 + * 4. 处理关卡进度持久化(只记到关,节恒从 1 开始)。 + * 5. 第 5 节(Boss 节)胜利后弹结算(VictoryComp);英雄全灭判负弹结算。 * * 关键设计: - * - 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 —— 局内数据(金币 / 回合数 / 怪物数量 / 关卡进度等) - * - StageSet —— 关卡配置与数值公式 - * - UIID.Victory —— 结算弹窗 + * - mission_start() 初始化所有游戏数据 → 恢复存档进度(大关)→ 进入第 1 节。 + * - 每节不清场、不重新三选一:英雄保留并自动归位到左侧编队,节间仅做一次过场推进。 + * - 节间过场:BattleEnd 判定非末节 → 进入 PrepareStart(in_fight=false)触发英雄编队归位, + * 同时播放背景滚动,倒计时结束自动进入下一节。 + * - 第 5 节胜利 → open_Victory(final) 弹结算,确认退出并存档进下一关。 + * - cleanComponents() / clearBattlePools() 仅在整关开始/结束时执行,节间不调用。 */ import { _decorator, Vec3, Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label, CCInteger, tween, v3, Tween, Widget, UIOpacity } from "cc"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; @@ -50,6 +43,8 @@ import { MissionMonCompComp } from "./MissionMonComp"; import { getProgressText, getMonsterLv, + BOSS_SECTION, + SECTIONS_PER_STAGE, } from "../common/config/StageSet"; const { ccclass, property } = _decorator; @@ -65,8 +60,8 @@ export enum MissionPhase { Settle = 7, // 结算阶段(VictoryComp 弹窗,等待玩家点击下一节/下一关/退出) } -/** 结算类型(驱动 VictoryComp 按钮分流与文案) */ -type SettleType = "defeat" | "section" | "final"; +/** 结算类型(驱动 VictoryComp 按钮分流与文案):一关一局后仅 失败 / 通关 两种 */ +type SettleType = "defeat" | "final"; //@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理 @@ -126,14 +121,10 @@ export class MissionComp extends CCComp { } /**秒计时 */ PhaseTime: Timer = new Timer(1) - /** 回合间倒计时(秒) */ + /** 节间过场倒计时(秒):播放背景滚动 + 英雄归位,倒计时结束自动开下一节 */ private waveCountdown: number = 0; - /** 倒计时档位:快清场(压缩垃圾时间,奖励强 build) */ - private static readonly COUNTDOWN_FAST = 2.5; - /** 倒计时档位:普通 */ - private static readonly COUNTDOWN_NORMAL = 4.0; - /** 倒计时档位:有英雄死亡(保留满运营窗口:调整阵型/复活/买卡) */ - private static readonly COUNTDOWN_FULL = 5.0; + /** 节间过场倒计时档位:统一时长(一关一局节间自动推进,节奏紧凑) */ + private static readonly COUNTDOWN_TRANSITION = 2.5; /** 上一次显示的时间字符串(避免重复设置) */ private lastTimeStr: string = ""; /** 上一次显示的秒数(避免重复计算) */ @@ -160,15 +151,13 @@ export class MissionComp extends CCComp { private heapTrendBaseMB: number = -1; /** 怪物数量同步计时器(降低同步频率) */ private monsterCountSyncTimer: number = 0; - /** 当前回合数 */ + /** 当前回合数(= 当前小节号,驱动 HUD 回合/进度显示) */ private currentWave: number = 0; - /** 是否为Boss回合 */ + /** 当前节是否包含 Boss(驱动 Boss 预警) */ private isBossWave: boolean = false; /** 超时回合已在 onBattleTimeout 预扣留存分,BattleEnd 需跳过重复累加 */ private skipRemainScoreOnBattleEnd: boolean = false; - /** 上回合英雄死亡数快照(BattleEnd 写入,供下一回合倒计时档位判定) */ - private lastWaveDeathCount: number = 0; - /** 上回合计时口径清场时间快照(含清场加速还原,供下一回合倒计时档位判定) */ + /** 上节清场时间快照(含清场加速还原,供评分统计) */ private lastWaveClearTime: number = 0; /** 当前任务阶段 */ public currentPhase: MissionPhase = MissionPhase.None; @@ -176,9 +165,9 @@ 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; @@ -191,7 +180,7 @@ export class MissionComp extends CCComp { public currentChapter: number = 1; /** 当前关卡号(1~10) */ public currentStage: number = 1; - /** 当前小节号(1~10) */ + /** 当前小节号(1~SECTIONS_PER_STAGE) */ public currentSection: number = 1; // ======================== ECS 查询匹配器(预缓存) ======================== @@ -214,8 +203,6 @@ 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.on(GameEvent.RestartMission, this.onRestartMission, this) this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this) @@ -231,13 +218,13 @@ export class MissionComp extends CCComp { oops.message.dispatchEvent(GameEvent.MissionStart, {}); this.mission_start(); }, 0); + // 整关战斗期间停掉地图背景动画;节间过场时短暂恢复滚动(见节间推进逻辑) smc.map.MapView.scene.mapLayer.stopAnimations(); - // smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = true; } onDestroy() { + // 退出关卡时恢复地图背景动画 smc.map.MapView.scene.mapLayer.playAnimations() - // smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = false; super.onDestroy(); if (this.start_btn && this.start_btn.isValid) { this.start_btn.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this); @@ -309,20 +296,15 @@ export class MissionComp extends CCComp { // ======================== 回合倒计时 ======================== - /** 进入回合间倒计时:按上回合战况分档(快清场压缩垃圾时间,有死亡保留运营窗口) */ + /** 进入节间过场:恢复背景滚动 + 英雄归位(in_fight=false 触发编队排队),倒计时结束自动开下一节 */ private startWaveCountdown() { - this.waveCountdown = this.computeCountdown(); + this.waveCountdown = MissionComp.COUNTDOWN_TRANSITION; this.isWaveCountdown = true; + // 节间过场:恢复地图背景滚动动画,表现玩家推进 + smc.map?.MapView?.scene?.mapLayer?.playAnimations(); this.updateCountdownUI(true); } - /** 计算下一回合倒计时档位:读 BattleEnd 写入的快照(onNewWave 会清零 clearTime,不能现读) */ - private computeCountdown(): number { - if (this.lastWaveDeathCount > 0) return MissionComp.COUNTDOWN_FULL; - if (this.lastWaveClearTime > 0 && this.lastWaveClearTime < WAVE_DURATION * 0.65) return MissionComp.COUNTDOWN_FAST; - return MissionComp.COUNTDOWN_NORMAL; - } - /** 更新倒计时 UI(显示剩余秒数) */ private updateCountdownUI(force: boolean = false) { if (!this.isWaveCountdown) return; @@ -331,10 +313,12 @@ export class MissionComp extends CCComp { this.lastTimeSecond = remain; } - /** 结束回合间倒计时 */ + /** 结束节间过场:停背景滚动,进入战斗 */ private stopWaveCountdown() { this.isWaveCountdown = false; this.lastTimeSecond = -1; + // 过场结束,停掉背景滚动(战斗中保持静止) + smc.map?.MapView?.scene?.mapLayer?.stopAnimations(); } // ======================== 奖励与广告 ======================== @@ -542,16 +526,15 @@ export class MissionComp extends CCComp { smc.mission.stop_spawn_mon = true; if (smc.mission.play && !smc.mission.pause) { - // 【评分系统 - 战绩分】每回合胜利加分 + // 【评分系统 - 战绩分】每节胜利加分 smc.vmdata.scores.wave_win_count++; - // 【评分系统 - 战绩分】记录每回合结束时场上留存的敌人数量(扣分项) + // 【评分系统 - 战绩分】记录每节结束时场上留存的敌人数量(扣分项) // 超时回合已在 onBattleTimeout 预扣,跳过防重 if (!this.skipRemainScoreOnBattleEnd) { smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num; } this.skipRemainScoreOnBattleEnd = false; - let allAlive = true; let hasHero = false; ecs.query(this.heroAttrsMatcher).forEach(entity => { const attrs = entity.get(HeroAttrsComp); @@ -559,19 +542,17 @@ export class MissionComp extends CCComp { hasHero = true; } }); - // 英雄为真实死亡(实体已销毁),本回合死亡数取死亡记录长度 + // 英雄为真实死亡(实体已销毁),本节死亡数取死亡记录长度 const heroDeathCount = smc.mission.dead_heroes.length; - if (heroDeathCount > 0) allAlive = false; - // 快照供倒计时档位判定(onNewWave 会清零 clearTime,必须提前快照) - this.lastWaveDeathCount = heroDeathCount; + // 快照清场时间供评分统计(onNewWave 会清零 clearTime,必须提前快照) this.lastWaveClearTime = this.clearTime; - // 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分) - if (hasHero && allAlive) { + // 【评分系统 - 战绩分】记录全员存活的胜利节数(额外加分) + if (hasHero && heroDeathCount === 0) { smc.vmdata.scores.wave_all_alive_count++; } - // 【评分系统 - 战绩分】单场 Boss 战定胜负,通过即视为通关 + // 通关判定:Boss 节(末节)胜利即视为本关通关 smc.vmdata.scores.passed_wave_20 = true; } @@ -606,8 +587,9 @@ export class MissionComp extends CCComp { private autoNextPhase() { switch (this.currentPhase) { case MissionPhase.PrepareStart: - // 回合间倒计时结束,停止倒计时状态,直接进入 PrepareEnd(不再等待玩家点击) + // 节间过场倒计时结束:推进到下一节、停背景滚动、派发 SectionStart 刷怪,进入 PrepareEnd 自动开战 this.stopWaveCountdown(); + this.advanceToNextSection(); this.changePhase(MissionPhase.PrepareEnd); break; case MissionPhase.PrepareEnd: @@ -617,38 +599,36 @@ export class MissionComp extends CCComp { this.changePhase(MissionPhase.Battle); break; case MissionPhase.BattleEnd: - // BattleEnd 计时结束后,如果是因为全灭或手动调用的 fight_end,进入 Settle - // 需要注意的是,open_Victory / fight_end 现在只需切换到 BattleEnd 即可,Settle 由这里自动接管 - // 如果游戏正在运行(回合更迭),则自动进入 PrepareStart 阶段 + // BattleEnd 计时结束后分流: + // - 局仍在进行(节间清场,非末节)→ 进入 PrepareStart 过节间过场,自动连战下一节 + // - 已暂停(open_Victory 结算:末节通关/失败)→ 进入 Settle 弹结算 if (smc.mission.play && !smc.mission.pause) { this.changePhase(MissionPhase.PrepareStart); } else { this.changePhase(MissionPhase.Settle); - // 此时已经经过了 2s,可以真正执行结算弹窗或清理逻辑 if (smc.mission.play) { - // 如果游戏还在运行中,说明是通过 open_Victory 进来的 + // 通过 open_Victory 进来:暂停并弹结算(通关/失败) smc.mission.pause = true; - mLogger.log(this.debugMode, 'MissionComp', " autoNextPhase -> open_Victory logic", this.revive_times); oops.gui.open(UIID.Victory, { victory: this._victory, all_alive: this.checkAllHeroAlive(), rewards: this.rewards, game_data: this.game_data, can_revive: this.revive_times > 0, - // 结算类型:小节胜利显示"下一节",末关通关显示"确认退出" + 通关标题 + // 结算类型:final=通关(确认退出),defeat=失败(再来一次/退出) settle_type: this._settleType, settle_title: this._settleTitle, // 关卡难度度量(= 大关号,驱动胜利奖励随进度增长) mon_lv: getMonsterLv(this.currentStage), - // 失败奖励判定数据:本节坚持时间 + 击杀统计(kills=已刷出-存活) + // 失败奖励判定数据:本关坚持时间 + 击杀统计(kills=已刷出-存活) defeat_stats: { clear_time: this.clearTime, ...this.querySectionKillStats(), }, }); } else { - // 如果 play 已经是 false,说明是通过 fight_end 进来的 + // play 已为 false(fight_end 强制结束)→ 直接清理 this.cleanComponents(); this.clearBattlePools(); } @@ -669,16 +649,6 @@ export class MissionComp extends CCComp { this.changePhase(MissionPhase.PrepareEnd); } - /** - * 进入准备阶段: - * - 标记非战斗 - * - 暂停刷怪 - * - 显示开始按钮 - */ - private enterPreparePhase() { - this.changePhase(MissionPhase.PrepareStart); - } - /** * 触发英雄的战斗开始技能 */ @@ -698,8 +668,8 @@ export class MissionComp extends CCComp { }); } - /** - * 战斗结束阶段治疗所有英雄(包括墓地英雄),按 FightSet.WAVE_HEAL_RATE 恢复最大生命值比例 + /** + * 战斗结束阶段治疗所有英雄(节间回血),按 FightSet.WAVE_HEAL_RATE 恢复最大生命值比例 */ private healAllHeroes() { const healRateBoost = FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.WaveHeal); @@ -735,12 +705,6 @@ export class MissionComp extends CCComp { this.to_fight(); } - /** 结算弹窗"下一节"事件:关闭弹窗并推进到下一小节 */ - private onNextSection() { - oops.gui.remove(UIID.Victory); - this.advanceToNextSection(); - } - /** 结算弹窗"再来一次"事件:关闭弹窗并重开本关 */ private onRestartMission() { oops.gui.remove(UIID.Victory); @@ -756,9 +720,9 @@ export class MissionComp extends CCComp { /** - * 打开结算弹窗: - * - 暂停游戏 - * - 按当前进度判定结算类型(小节胜利 / 末关通关 / 失败) + * 打开结算弹窗(一关一局:仅整关结束时触发——Boss 节通关 / 英雄全灭判负 / 超时): + * - 暂停游戏、切断节间流转 + * - 胜利即 final(通关),存档推进到下一关;失败即 defeat(进度不变,可复活/再来一次) * - 切入 BattleEnd,2s 后由 autoNextPhase 自动弹出 VictoryComp * * @param e 事件对象(未使用) @@ -766,23 +730,19 @@ export class MissionComp extends CCComp { * @param victory 是否胜利(默认 false) */ open_Victory(e: any, is_hero_dead: boolean = false, victory: boolean = false) { - // 战斗失败或胜利,标记暂停状态以切断回合流转逻辑 + // 战斗结束(通关/失败),标记暂停状态以切断节间流转逻辑 smc.mission.pause = true; - // 记录胜利标记,供结算弹窗使用 this._victory = victory; this._settleTitle = ""; if (!victory) { - // 失败:进度不变,结算窗走复活/退出流程 + // 失败:进度不变,结算窗走复活/再来一次/退出流程 this._settleType = "defeat"; - } else if (this.currentSection < 10) { - // 第 1~9 节胜利:还有下一节,结算窗显示"下一节" - this._settleType = "section"; } else { - // 第 10 节胜利:本关最后一节,无"下一节"按钮,结算页显示通关标题、确认即退出 + // 通关(Boss 节胜利):结算窗显示通关标题,确认退出并存档进下一关 this._settleType = "final"; this._settleTitle = this.currentStage >= 10 ? "本章通关" : "本关通关"; - // 存档直接推进到下一关第 1 小节(仅写存档,内存进度保留用于本局结算显示); + // 存档推进到下一关第 1 节(仅写存档,内存进度保留用于本局结算显示); // 第 10 关通关即本章全通,存档停留等待后续章节扩展 if (this.currentStage < 10) { smc.updateStageProgress({ @@ -850,7 +810,6 @@ export class MissionComp extends CCComp { this.currentWave = 1; this.isBossWave = false; this.skipRemainScoreOnBattleEnd = false; - this.lastWaveDeathCount = 0; this.lastWaveClearTime = 0; this.rewards = [] this.revive_times = 1; @@ -881,20 +840,20 @@ export class MissionComp extends CCComp { // ======================== 关卡进度管理 ======================== /** - * 加载关卡进度存档 - * 从 smc.data.stage_progress 恢复,缺省从 1-1-1 开始 + * 加载关卡进度存档(一关一局:只恢复大关,节恒从 1 开始) + * 从 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) { + if (progress && progress.chapter >= 1 && progress.stage >= 1) { this.currentChapter = progress.chapter; this.currentStage = progress.stage; - this.currentSection = progress.section; } else { this.currentChapter = 1; this.currentStage = 1; - this.currentSection = 1; } + // 一关一局:进入关卡始终从第 1 节开打 + this.currentSection = 1; // 加载完成后将权威进度镜像到 vmdata,驱动 HUD 实时显示 smc.syncStageProgressToView(); mLogger.log(this.debugMode, 'MissionComp', @@ -902,64 +861,36 @@ export class MissionComp extends CCComp { } /** - * 保存关卡进度存档 - * 写入 smc.data.stage_progress 并镜像到 vmdata,同时标记云端同步 - */ - private saveStageProgress() { - smc.updateStageProgress({ - 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 节胜利时不显示"下一节",不会触发本方法 + * 推进到下一小节(节间自动推进): + * 1. 节号 +1,派发 SectionStart 触发 MissionMonComp 生成下一节怪物(从右侧跑入)。 + * 2. 不清理英雄/技能实体、不重置局内数据、不重新三选一——英雄保留并自动归位(编队排队)。 + * 3. 进度实时镜像到 HUD,但不写持久存档(存档只在整关通关时推进到下一关)。 + * + * 调用时机:PrepareStart 过场倒计时结束(autoNextPhase)。 + * 保证:仅 1~4 节清场后触发,末节(BOSS_SECTION)已走 open_Victory 结算,不会推进。 */ 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 据此生成新小节怪物 + // 节号实时同步到 HUD(内存进度,不持久化;整关通关时才统一写大关存档)。 + // 直接写 vmdata 视图层而非走 syncStageProgressToView(后者从 data 读,而 data 仍停留在大关起点) + const view = smc.vmdata.stage_progress; + view.chapter = this.currentChapter; + view.stage = this.currentStage; + view.section = this.currentSection; + oops.message.dispatchEvent(GameEvent.STAGE_PROGRESS_UPDATE); + // 触发 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)}`); } /** - * 重开本关:清理战场并重置数据,从当前小节重新开始 - * 与 advanceToNextSection 的区别:不推进小节进度,保持当前 section 不变 + * 重开本关:清理战场并重置数据,从本关第 1 节重新开始 + * Why: 一关一局,失败"再来一次"即整关重打(节恒从 1 开始) */ private restartCurrentMission() { // 清理战场与局内数据 @@ -970,12 +901,14 @@ export class MissionComp extends CCComp { this._victory = false; this._settleType = "defeat"; this._settleTitle = ""; + // 一关一局:重开从第 1 节开始 + this.currentSection = 1; // 进入准备阶段 this.changePhase(MissionPhase.Prepare); // 触发 MissionStart 重置开局免费抽卡 oops.message.dispatchEvent(GameEvent.MissionStart, {}); oops.message.dispatchEvent(GameEvent.FightReady); - // 触发 SectionStart 重新生成当前小节怪物 + // 触发 SectionStart 重新生成本关第 1 节怪物 oops.message.dispatchEvent(GameEvent.SectionStart, { chapter: this.currentChapter, stage: this.currentStage, @@ -988,33 +921,28 @@ export class MissionComp extends CCComp { // ======================== 回合管理 ======================== /** - * 新一回合事件回调: - * 1. 进入准备阶段。 - * 2. 更新当前回合数。 - * 3. 刷新时间显示。 + * 新一节事件回调(SectionStart 时由 MissionMonComp 派发,wave = 小节号): + * 1. 更新当前节号与 HUD 进度。 + * 2. 按节配置判定是否含 Boss(驱动 Boss 预警)。 + * 3. 首回合延迟播放回合提示。 * - * 注意:金币不再按回合固定发放,改为怪物死亡时掉落(见 HeroAtkSystem)。 + * 注意:节间推进在 autoNextPhase(PrepareStart) 完成,此处仅同步显示与 Boss 预警。 * * @param event 事件名 - * @param data { wave: number } + * @param data { wave: number, bossWave?: boolean } */ private onNewWave(event: string, data: any) { const wave = Number(data?.wave ?? 0); if (wave <= 0) return; - this.isBossWave = true; // 单场 Boss 战,恒为 Boss 战(超时阈值/预警走 Boss 档) + // Boss 预警:本节怪物列表含 Boss 时置位(Boss 节 Boss 延迟登场,BattleStart 时发预警) + this.isBossWave = !!data?.bossWave; - if (wave > 1 && this.currentPhase === MissionPhase.Battle) { - // 战斗中回合更迭:先进入 BattleEnd,2秒后自动进入下一回合的准备阶段。 - // Why: NewWave 现在仅在 SectionStart(Prepare 阶段,等待三选一)时派发, - // 不加相位守卫会把"下一节/再来一次"从 Prepare 强切 BattleEnd, - // 导致玩家还没选英雄就自动开战,场上无英雄直接判负 - this.changePhase(MissionPhase.BattleEnd); - } else { - // 第1回合不需要结束上一回合,延迟一点播放提示,避免被开始游戏的loading遮挡 + if (this.currentPhase === MissionPhase.Prepare) { + // 首回合:延迟一点播放提示,避免被开始游戏的 loading 遮挡 this.scheduleOnce(() => { if (this.currentPhase === MissionPhase.Prepare) { - this.playTooltipAnim(`第 ${wave} 回合`); + this.playTooltipAnim(`第 ${wave} 节`); } }, 0.5); } @@ -1055,10 +983,10 @@ 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 归零前取值) + // oops.message 全局事件总线:清屏庆祝(横幅/奖励由 BattleBannerComp 消费),需在结算/过场前派发(clearTime 归零前取值) const allAlive = this.checkAllHeroAlive(); const fastClear = this.clearTime < WAVE_DURATION * 0.5 ? 2 : this.clearTime < WAVE_DURATION * 0.75 ? 1 : 0; @@ -1068,8 +996,14 @@ export class MissionComp extends CCComp { allAlive, fastClear, }); - // 小节胜利:延迟弹结算窗,等待死亡动画播放完 - this.scheduleSettle(false, true); + + if (this.currentSection >= BOSS_SECTION) { + // Boss 节(末节)清场 → 本关通关,延迟弹结算(等死亡动画播完) + this.scheduleSettle(false, true); + } else { + // 非末节清场 → 不切暂停,延迟切 BattleEnd,由 autoNextPhase 进入节间过场自动推进下一节 + this.scheduleSectionTransition(); + } return; } @@ -1125,11 +1059,27 @@ export class MissionComp extends CCComp { // 延迟期间局内状态可能已变化(如复活、任务结束),兜底校验 if (!smc.mission.play || smc.mission.pause) return; if (this.currentPhase !== MissionPhase.Battle) return; - // 胜利/失败统一走结算弹窗:open_Victory 内部按进度判定小节/大关/章节结算类型 + // 胜利/失败统一走结算弹窗:open_Victory 判定通关/失败 this.open_Victory(null, is_hero_dead, victory); }, MissionComp.SETTLE_DELAY); } + /** + * 预约节间过场(非末节清场):延迟 SETTLE_DELAY 秒等死亡动画播完,再切 BattleEnd。 + * 与 scheduleSettle 的区别:不置 pause、不弹结算,由 autoNextPhase 进入 PrepareStart 过场自动连战下一节。 + */ + private scheduleSectionTransition() { + if (this._settleScheduled) return; + this._settleScheduled = true; + this.scheduleOnce(() => { + this._settleScheduled = false; + // 延迟期间局内状态可能已变化(如英雄刚好全灭、任务结束),兜底校验 + if (!smc.mission.play || smc.mission.pause) return; + if (this.currentPhase !== MissionPhase.Battle) return; + this.changePhase(MissionPhase.BattleEnd); + }, MissionComp.SETTLE_DELAY); + } + /** * 战斗超时强制结束(僵局兜底): * 1. 预扣留存分(残留怪销毁后 mon_num 会被下一次同步清零,BattleEnd 的累加会漏扣,故在此显式预扣并置防重标志)。 diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index bbaa22d6..2fb59a8d 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -4,14 +4,14 @@ * * 职责: * 1. 按小节配置刷怪:监听 SectionStart 事件,从 StageSpawningEngine 获取怪物列表。 - * 2. 普通关:前 3 只立即刷,后续每 2s 刷 1 只(共 9 只),均从高处掉落入场。 - * 3. 精英关:3 只精英同时刷出。 - * 4. Boss 关:2 精英先刷,Boss 延迟 1s 刷出。 - * 5. 维护小节刷怪计数,供 MissionComp 检测小节结束。 + * 2. 普通节:3 只小怪立即刷出;精英节:3 只精英同刷;Boss 节:2 小怪 + 1 Boss(延迟 1s)。 + * 3. 怪物统一从右侧屏幕外跑入(贴地向左推进),不再高空掉落。 + * 4. 维护小节刷怪计数,供 MissionComp 检测小节结束。 * * 关键设计: * - 怪物按类型落排(近战/Boss→前排、远程/刺客→中排、辅助/召唤→后排),与英雄三排镜像对应。 - * - 实际推进由统一 MoveComp 在战斗中向敌人移动自然形成,不再使用固定网格点。 + * - 出生 X 取目标排 X + 右侧偏移(屏幕外),dropToY=GAME_LINE(无下落), + * MoveComp.targetX 指向目标排 X,落地即 moving=true 自然从右跑入。 * - 槽位索引仅用于 monGrid 寻路与 SCastSystem 索敌定位。 */ import { _decorator, v3, Vec3 } from "cc"; @@ -36,8 +36,8 @@ const { ccclass, property } = _decorator; export class MissionMonCompComp extends CCComp { // ======================== 常量 ======================== - /** 怪物出生掉落高度(码):从高处砸落入场,落地时震退范围内英雄 */ - private static readonly MON_DROP_HEIGHT = 400; + /** 怪物出生右侧偏移(码):目标排 X 再加该偏移,使出生点位于屏幕右侧外,贴地从右跑入 */ + private static readonly MON_SPAWN_OFFSET_X = 380; /** * 怪物类型 → 编队优先级(与英雄 HRole 枚举值升序一致): @@ -207,9 +207,10 @@ export class MissionMonCompComp extends CCComp { let scale = -1; const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length]; - const spawnX = this.resolveSpawnX(posIndex); + const landingX = this.resolveSpawnX(posIndex); const landingY = basePos.y; - const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0); + // 出生点位于目标排右侧屏幕外(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() @@ -227,6 +228,8 @@ export class MissionMonCompComp extends CCComp { const move = mon.get(MoveComp); if (move) { move.spawnOrder = this.globalSpawnOrder; + // 推进目标指向目标排 X,而非出生点(出生点在屏幕外) + move.targetX = landingX; } // 应用引擎计算好的最终属性 diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index 0272005b..a2a988cb 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -3,10 +3,9 @@ * @description 关卡刷怪系统(章节-关-节三级推进) * * 游戏机制: - * - 玩家线性推进 1 章 10 关 × 每关 10 小节,进度精确到小节并持久化。 - * - 普通关(1~4, 6~9):9 只小怪,前 3 只立即刷,后续每 2s 刷 1 只。 - * - 精英关(5):3 只精英同时刷出。 - * - Boss 关(10):2 精英先刷,Boss 延迟 1s 刷出。 + * - 玩家线性推进 1 章 10 关 × 每关 5 小节(一关一局,节间自动推进)。 + * - 每关节点固定:1~3 节普通(3 小怪,立即全刷)、第 4 节精英(3 精英同刷)、 + * 第 5 节 Boss(2 小怪先刷,Boss 延迟 1s 刷出)。 * - 怪物属性 = 基础 × 0.8 × (1 + 0.05×(lv-1)) × 类型倍率(精英1.5 / Boss3.0)。 * * 设计层次: @@ -295,8 +294,7 @@ export class StageSpawningEngine { } /** - * 普通关:9 只小怪 - * 前 3 只 batch=0(立即刷),后 6 只 batch=1(定时刷) + * 普通节:NORMAL_MON_COUNT 只小怪全部立即刷出(batch=0) */ private generateNormalSection(cfg: SectionConfig): GeneratedMonster[] { const monsters: GeneratedMonster[] = []; @@ -312,7 +310,7 @@ export class StageSpawningEngine { } /** - * 精英关:3 只精英同时刷出 + * 精英节:3 只精英同时刷出 */ private generateEliteSection(cfg: SectionConfig): GeneratedMonster[] { const monsters: GeneratedMonster[] = []; @@ -334,8 +332,8 @@ export class StageSpawningEngine { } /** - * Boss 关:2 精英 + 1 Boss - * 精英先刷(batch=0),Boss 延迟刷出(batch=0, delay=BOSS_DELAY) + * Boss 节:2 小怪护卫 + 1 Boss + * 小怪先刷(batch=0),Boss 延迟刷出(batch=0, delay=BOSS_DELAY) */ private generateBossSection(cfg: SectionConfig): GeneratedMonster[] { const monsters: GeneratedMonster[] = []; diff --git a/assets/script/game/map/VictoryComp.ts b/assets/script/game/map/VictoryComp.ts index c26d05b6..dbe4f11b 100644 --- a/assets/script/game/map/VictoryComp.ts +++ b/assets/script/game/map/VictoryComp.ts @@ -172,7 +172,7 @@ export class VictoryComp extends CCComp { /** 是否可以复活(由 MissionComp 传入,取决于剩余复活次数) */ private canRevive: boolean = false; - /** 结算类型:section=小节胜利, victory=末关通关, defeat=失败 */ + /** 结算类型:final=通关(确认退出), defeat=失败(再来一次/退出) */ private settleType: string = "defeat"; /** 是否胜利场(驱动主按钮文案与行为) */ private isVictory: boolean = false; @@ -188,7 +188,7 @@ export class VictoryComp extends CCComp { this.node.getChildByName("loading").active = false // 代码绑定按钮点击事件,替代编辑器 Click Events 配置 - // next 按钮按状态分发:小节胜利→下一节,失败→再来一次 + // next 按钮按状态分发:失败→再来一次(一关一局无节间"下一节") this.bindButtonClick(this.next_section_btn, this.on_next_btn_click); this.bindButtonClick(this.confirm_btn, this.victory_end); } @@ -214,8 +214,8 @@ export class VictoryComp extends CCComp { * @param args.rewards 掉落奖励列表 * @param args.game_data 累计数据 { exp, gold, diamond } * @param args.can_revive 是否可复活 - * @param args.settle_type 结算类型:section=小节胜利(下一节按钮),其余=确认退出 - * @param args.settle_title 末关通关标题(如"本关通关"),非空时在结算页顶部展示 + * @param args.settle_type 结算类型:final=通关(确认退出),defeat=失败(再来一次/退出) + * @param args.settle_title 通关标题(如"本关通关"),非空时在结算页顶部展示 */ onAdded(args: any) { this.node.getChildByName("loading").active = false @@ -233,9 +233,9 @@ export class VictoryComp extends CCComp { // 根据是否可复活决定按钮显示 this.node.getChildByName("btns").getChildByName("next").active = !args.can_revive - // 按钮显隐与文案:下一节 / 再来一次 / 确认 + // 按钮显隐与文案:再来一次 / 确认退出(通关隐藏再来一次) this.updateButtonStates(); - // 通关标题:仅末关通关时显示 + // 通关标题:仅通关时显示 this.updateSettleTitle(args.settle_title ? String(args.settle_title) : ""); // 计算总分并写入历史纪录 @@ -779,10 +779,9 @@ export class VictoryComp extends CCComp { // ======================== 操作入口 ======================== /** - * 统一更新按钮显隐与文案: - * section(小节胜利)→ btn="退出",next="下一节" - * final(末关通关) → btn="返回",next 隐藏 - * defeat(失败) → btn="退出",next="再来一次" + * 统一更新按钮显隐与文案(一关一局,无节间"下一节"): + * final(通关) → btn="返回",next 隐藏 + * defeat(失败) → btn="退出",next="再来一次" */ private updateButtonStates() { const btns = this.node.getChildByName("btns"); @@ -800,12 +799,12 @@ export class VictoryComp extends CCComp { } } - // next 按钮:小节胜利→"下一节",失败→"再来一次",末关通关隐藏 + // next 按钮:失败→"再来一次",通关隐藏 if (nextNode) { nextNode.active = this.settleType !== "final"; const label = nextNode.getChildByName("Label")?.getComponent(Label); if (label) { - label.string = this.settleType === "section" ? "下一节" : "再来一次"; + label.string = "再来一次"; } } @@ -816,7 +815,7 @@ export class VictoryComp extends CCComp { } /** - * 通关标题显隐与文案:仅末关通关时显示("本关通关"/"本章通关")。 + * 通关标题显隐与文案:仅通关时显示("本关通关"/"本章通关")。 * 该 Label 为 @property 绑定节点,编辑器中默认隐藏。 * @param title 文案,空串时隐藏 */ @@ -828,41 +827,21 @@ export class VictoryComp extends CCComp { } /** - * next 按钮点击分发: - * 小节胜利 → 派发 NextSection 推进下一节 - * 失败场 → 派发 RestartMission 重开本关 + * next 按钮点击分发:失败场 → 派发 RestartMission 重开本关 + * (一关一局:通关场 next 隐藏,无"下一节") */ on_next_btn_click() { - if (this.settleType === "section") { - oops.message.dispatchEvent(GameEvent.NextSection); - } else { - oops.message.dispatchEvent(GameEvent.RestartMission); - } + oops.message.dispatchEvent(GameEvent.RestartMission); } /** * 确认按钮点击(代码绑定,btns/btn): - * - 小节胜利(section)→ 推进存档到下一节并保存 → 派发 MissionEnd → 关闭结算弹窗 - * - 末关通关(final) → 清理数据 → 派发 MissionEnd → 关闭结算弹窗 - * - 失败(defeat) → 清理数据 → 派发 MissionEnd → 关闭结算弹窗 + * 通关 / 失败统一 → 清理数据 → 派发 MissionEnd → 关闭结算弹窗 * - * Why: 小节胜利点"退出"既要保存进度(避免下次重玩卡在原节), - * 又不能像"下一节"按钮那样直接开战(玩家已选退出,不再继续本局战斗)。 + * Why: 一关一局,存档只在 MissionComp 通关时推进到下一关(section 恒为 1), + * 此处不再做 section 推进,直接结束本局返回。 */ victory_end() { - if (this.settleType === "section") { - // 小节胜利:手动推进 section 到下一节并保存存档,避免点退出后进度丢失。 - // 注:MissionComp 的 NextSection 会进入战斗流程,这里不走该事件以避免自动开战。 - const cur = (smc.data.stage_progress ?? { chapter: 1, stage: 1, section: 1 }) as { chapter: number; stage: number; section: number }; - // 当前节已胜利,写入下一节作为待挑战存档;若已是末节(section=10),则不做 section 推进 - if (cur.section < 10) { - smc.updateStageProgress({ - chapter: cur.chapter, - stage: cur.stage, - section: cur.section + 1, - }); - } - } this.clear_data() oops.message.dispatchEvent(GameEvent.MissionEnd) oops.gui.removeByNode(this.node)