Compare commits
3 Commits
d951896799
...
328f6c83f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
328f6c83f3 | ||
|
|
10e2c0d817 | ||
|
|
1b2545deec |
@@ -5746,10 +5746,10 @@
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_string": "第 {{0}}章{{0}}{{2}}关",
|
||||
"_string": "第 {{0}} - {{1}}关 {{2}}回合",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 20,
|
||||
"_actualFontSize": 17,
|
||||
"_fontSize": 25,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 40,
|
||||
@@ -5827,8 +5827,6 @@
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "4685Eq90tNG7LQanmN1qTk",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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<number, ChapterConfig> = {
|
||||
* 获取小节配置
|
||||
* @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 {
|
||||
|
||||
@@ -45,6 +45,13 @@ export class MoveComp extends ecs.Comp {
|
||||
|
||||
@ecs.register('MoveSystem')
|
||||
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
||||
/**
|
||||
* 节间过场移动动画锁(全局):
|
||||
* 置位期间,存活英雄即使已归位到编队槽位也保持"move"移动动画(配合地图滚动,表现行军推进)。
|
||||
* 由 MissionComp 在节间过场开始(背景滚动启动)时置 true、过场结束(背景滚动停止)时置 false。
|
||||
*/
|
||||
public static transitionMoveLock = false;
|
||||
|
||||
/** 渲染层级重排节流,避免每帧排序 */
|
||||
private readonly renderSortInterval = 0.05;
|
||||
private lastRenderSortAt = 0;
|
||||
@@ -65,6 +72,8 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
private static readonly QUEUE_LEFT_LIMIT = FightSet.HERO_ROW_BACK_X - 120;
|
||||
/** 编队槽位默认间隔(像素);后排人数过多时自动压缩间距 */
|
||||
private static readonly QUEUE_SPACING = 30;
|
||||
/** 编队归位目标时长(秒):任意距离动态提速,保证约 1s 到位 */
|
||||
private static readonly QUEUE_ARRIVE_TIME = 1;
|
||||
/** 编队槽位重算节流间隔(秒):召唤/死亡后最多延迟一次重算 */
|
||||
private readonly queueRebuildInterval = 0.1;
|
||||
private lastQueueRebuildAt = 0;
|
||||
@@ -181,18 +190,30 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
view.node.setPosition(targetX, view.node.position.y, 0);
|
||||
}
|
||||
view.scale = 1;
|
||||
view.status_change("idle");
|
||||
// 节间过场锁定期:即使已归位也保持移动动画(配合地图滚动表现行军推进),过场结束才回待机
|
||||
if (MoveSystem.transitionMoveLock) {
|
||||
view.status_change("move");
|
||||
} else {
|
||||
view.status_change("idle");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/** 未到位:向槽位推进(与战斗推进同速),朝移动方向转身 */
|
||||
/** 未到位:向槽位推进,朝移动方向转身 */
|
||||
const dir = targetX > selfX ? 1 : -1;
|
||||
move.direction = dir;
|
||||
view.scale = dir;
|
||||
/** 编队排队允许突破战斗左边界(LETF_END=后排锚点),扩展至 QUEUE_LEFT_LIMIT */
|
||||
const queueClamp = (x: number): number => Math.max(MoveSystem.QUEUE_LEFT_LIMIT, x);
|
||||
const delta = (model.speed / 3) * this.dt * dir;
|
||||
const newX = queueClamp(selfX + delta);
|
||||
/** 归位速度:按剩余距离动态计算,保证任意距离约 1s 到位;不低于基础移速,避免近距离僵住 */
|
||||
const dist = Math.abs(targetX - selfX);
|
||||
const speed = Math.max(model.speed / 3, dist / MoveSystem.QUEUE_ARRIVE_TIME);
|
||||
const delta = speed * this.dt * dir;
|
||||
let newX = queueClamp(selfX + delta);
|
||||
/** 防过冲:本帧会越过目标则直接贴齐 */
|
||||
if ((dir > 0 && newX > targetX) || (dir < 0 && newX < targetX)) {
|
||||
newX = targetX;
|
||||
}
|
||||
if (Math.abs(newX - selfX) >= 0.01) {
|
||||
view.node.setPosition(newX, view.node.position.y, 0);
|
||||
view.status_change("move");
|
||||
|
||||
@@ -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";
|
||||
@@ -30,6 +23,7 @@ import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/modu
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { MoveSystem } from "../hero/MoveComp";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import { SkillTriggerType } from "../common/config/heroSet";
|
||||
import { HeroViewComp } from "../hero/HeroViewComp";
|
||||
@@ -50,6 +44,8 @@ import { MissionMonCompComp } from "./MissionMonComp";
|
||||
import {
|
||||
getProgressText,
|
||||
getMonsterLv,
|
||||
BOSS_SECTION,
|
||||
SECTIONS_PER_STAGE,
|
||||
} from "../common/config/StageSet";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@@ -65,8 +61,8 @@ export enum MissionPhase {
|
||||
Settle = 7, // 结算阶段(VictoryComp 弹窗,等待玩家点击下一节/下一关/退出)
|
||||
}
|
||||
|
||||
/** 结算类型(驱动 VictoryComp 按钮分流与文案) */
|
||||
type SettleType = "defeat" | "section" | "final";
|
||||
/** 结算类型(驱动 VictoryComp 按钮分流与文案):一关一局后仅 失败 / 通关 两种 */
|
||||
type SettleType = "defeat" | "final";
|
||||
|
||||
//@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理
|
||||
|
||||
@@ -126,14 +122,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 +152,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 +166,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 +181,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 +204,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 +219,15 @@ 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() {
|
||||
// 解除节间过场移动动画锁,避免泄漏到下一关
|
||||
MoveSystem.transitionMoveLock = false;
|
||||
// 退出关卡时恢复地图背景动画
|
||||
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 +299,13 @@ export class MissionComp extends CCComp {
|
||||
|
||||
// ======================== 回合倒计时 ========================
|
||||
|
||||
/** 进入回合间倒计时:按上回合战况分档(快清场压缩垃圾时间,有死亡保留运营窗口) */
|
||||
/** 进入节间过场:停止倒计时状态(背景滚动与英雄归位已在节间过场启动时同步触发) */
|
||||
private startWaveCountdown() {
|
||||
this.waveCountdown = this.computeCountdown();
|
||||
this.waveCountdown = MissionComp.COUNTDOWN_TRANSITION;
|
||||
this.isWaveCountdown = true;
|
||||
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 +314,14 @@ export class MissionComp extends CCComp {
|
||||
this.lastTimeSecond = remain;
|
||||
}
|
||||
|
||||
/** 结束回合间倒计时 */
|
||||
/** 结束节间过场:停背景滚动,进入战斗 */
|
||||
private stopWaveCountdown() {
|
||||
this.isWaveCountdown = false;
|
||||
this.lastTimeSecond = -1;
|
||||
// 过场结束,停掉背景滚动(战斗中保持静止)
|
||||
smc.map?.MapView?.scene?.mapLayer?.stopAnimations();
|
||||
// 解除英雄移动动画锁:归位后恢复待机
|
||||
MoveSystem.transitionMoveLock = false;
|
||||
}
|
||||
|
||||
// ======================== 奖励与广告 ========================
|
||||
@@ -470,9 +457,12 @@ export class MissionComp extends CCComp {
|
||||
|
||||
const phaseName = MissionComp.PhaseNameMap[targetPhase] || "未知";
|
||||
|
||||
// 播放状态切换提示栏动效(过滤掉 None、Prepare 准备阶段、Battle 战斗中阶段)
|
||||
// 播放状态切换提示栏动效(过滤掉 None、Prepare/PrepareStart/PrepareEnd 准备阶段、Battle 战斗中阶段;
|
||||
// 回合提示统一由 BattleStart 的"第 X 回合开始"承担,准备相关阶段不弹提示)
|
||||
if (targetPhase !== MissionPhase.None &&
|
||||
targetPhase !== MissionPhase.Prepare &&
|
||||
targetPhase !== MissionPhase.PrepareStart &&
|
||||
targetPhase !== MissionPhase.PrepareEnd &&
|
||||
targetPhase !== MissionPhase.Battle) {
|
||||
this.playTooltipAnim(phaseName);
|
||||
}
|
||||
@@ -515,6 +505,8 @@ export class MissionComp extends CCComp {
|
||||
case MissionPhase.BattleStart:
|
||||
// 触发战斗开始技能(fstart)
|
||||
this.triggerHeroBattleSkills();
|
||||
// 回合开始提示:"第 X 回合开始"(准备阶段不显示,战斗开始才播)
|
||||
this.playTooltipAnim(`第 ${this.currentWave} 回合开始`);
|
||||
// Boss 预警:Boss 同步场立即登场(eta=0)
|
||||
if (this.isBossWave) {
|
||||
oops.message.dispatchEvent(GameEvent.BossWarning, {
|
||||
@@ -542,16 +534,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 +550,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 +595,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 +607,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 +657,6 @@ export class MissionComp extends CCComp {
|
||||
this.changePhase(MissionPhase.PrepareEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入准备阶段:
|
||||
* - 标记非战斗
|
||||
* - 暂停刷怪
|
||||
* - 显示开始按钮
|
||||
*/
|
||||
private enterPreparePhase() {
|
||||
this.changePhase(MissionPhase.PrepareStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发英雄的战斗开始技能
|
||||
*/
|
||||
@@ -698,8 +676,8 @@ export class MissionComp extends CCComp {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 战斗结束阶段治疗所有英雄(包括墓地英雄),按 FightSet.WAVE_HEAL_RATE 恢复最大生命值比例
|
||||
/**
|
||||
* 战斗结束阶段治疗所有英雄(节间回血),按 FightSet.WAVE_HEAL_RATE 恢复最大生命值比例
|
||||
*/
|
||||
private healAllHeroes() {
|
||||
const healRateBoost = FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.WaveHeal);
|
||||
@@ -735,12 +713,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 +728,9 @@ export class MissionComp extends CCComp {
|
||||
|
||||
|
||||
/**
|
||||
* 打开结算弹窗:
|
||||
* - 暂停游戏
|
||||
* - 按当前进度判定结算类型(小节胜利 / 末关通关 / 失败)
|
||||
* 打开结算弹窗(一关一局:仅整关结束时触发——Boss 节通关 / 英雄全灭判负 / 超时):
|
||||
* - 暂停游戏、切断节间流转
|
||||
* - 胜利即 final(通关),存档推进到下一关;失败即 defeat(进度不变,可复活/再来一次)
|
||||
* - 切入 BattleEnd,2s 后由 autoNextPhase 自动弹出 VictoryComp
|
||||
*
|
||||
* @param e 事件对象(未使用)
|
||||
@@ -766,23 +738,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 +818,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 +848,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 +869,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 +909,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,36 +929,22 @@ export class MissionComp extends CCComp {
|
||||
// ======================== 回合管理 ========================
|
||||
|
||||
/**
|
||||
* 新一回合事件回调:
|
||||
* 1. 进入准备阶段。
|
||||
* 2. 更新当前回合数。
|
||||
* 3. 刷新时间显示。
|
||||
* 新一回合事件回调(SectionStart 时由 MissionMonComp 派发,wave = 回合号):
|
||||
* 1. 更新当前回合号与 HUD 进度。
|
||||
* 2. 按回合配置判定是否含 Boss(驱动 Boss 预警)。
|
||||
*
|
||||
* 注意:金币不再按回合固定发放,改为怪物死亡时掉落(见 HeroAtkSystem)。
|
||||
* 注意:回合推进在 autoNextPhase(PrepareStart) 完成,此处仅同步显示与 Boss 预警;
|
||||
* "第 X 回合开始"提示改在 BattleStart(战斗开始)时播放,准备阶段不显示。
|
||||
*
|
||||
* @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 档)
|
||||
|
||||
if (wave > 1 && this.currentPhase === MissionPhase.Battle) {
|
||||
// 战斗中回合更迭:先进入 BattleEnd,2秒后自动进入下一回合的准备阶段。
|
||||
// Why: NewWave 现在仅在 SectionStart(Prepare 阶段,等待三选一)时派发,
|
||||
// 不加相位守卫会把"下一节/再来一次"从 Prepare 强切 BattleEnd,
|
||||
// 导致玩家还没选英雄就自动开战,场上无英雄直接判负
|
||||
this.changePhase(MissionPhase.BattleEnd);
|
||||
} else {
|
||||
// 第1回合不需要结束上一回合,延迟一点播放提示,避免被开始游戏的loading遮挡
|
||||
this.scheduleOnce(() => {
|
||||
if (this.currentPhase === MissionPhase.Prepare) {
|
||||
this.playTooltipAnim(`第 ${wave} 回合`);
|
||||
}
|
||||
}, 0.5);
|
||||
}
|
||||
// Boss 预警:本回合怪物列表含 Boss 时置位(Boss 回合 Boss 延迟登场,BattleStart 时发预警)
|
||||
this.isBossWave = !!data?.bossWave;
|
||||
|
||||
this.currentWave = wave;
|
||||
smc.vmdata.mission_data.level = wave;
|
||||
@@ -1055,10 +982,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 +995,14 @@ export class MissionComp extends CCComp {
|
||||
allAlive,
|
||||
fastClear,
|
||||
});
|
||||
// 小节胜利:延迟弹结算窗,等待死亡动画播放完
|
||||
this.scheduleSettle(false, true);
|
||||
|
||||
if (this.currentSection >= BOSS_SECTION) {
|
||||
// Boss 节(末节)清场 → 本关通关,延迟弹结算(等死亡动画播完)
|
||||
this.scheduleSettle(false, true);
|
||||
} else {
|
||||
// 非末节清场 → 立即启动节间过场(背景滚动 + 英雄归位同帧启动),自动连战下一节
|
||||
this.beginSectionTransition();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1125,11 +1058,29 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动节间过场(非末节清场):
|
||||
* 1. 立即切出战斗(in_fight=false)→ 英雄触发编队排队、自动归位到左侧站位。
|
||||
* 2. 同时恢复地图背景滚动,表现玩家推进(背景与英雄归位同帧启动,避免一先一后)。
|
||||
* 3. 切 BattleEnd,2s 后由 autoNextPhase 进入 PrepareStart 过场倒计时,倒计时结束自动连战下一节。
|
||||
*
|
||||
* Why: 不等 SETTLE_DELAY 死亡动画,让"归位 + 背景滚动"在清场瞬间就启动,过渡更连贯。
|
||||
*/
|
||||
private beginSectionTransition() {
|
||||
smc.mission.in_fight = false;
|
||||
smc.vmdata.mission_data.in_fight = false;
|
||||
// 恢复地图背景滚动(与英雄归位同步启动,2 倍速表现推进)
|
||||
smc.map?.MapView?.scene?.mapLayer?.playAnimations();
|
||||
// 过场期间英雄保持移动动画:即使已归位也持续行军,地图停才回待机
|
||||
MoveSystem.transitionMoveLock = true;
|
||||
this.changePhase(MissionPhase.BattleEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 战斗超时强制结束(僵局兜底):
|
||||
* 1. 预扣留存分(残留怪销毁后 mon_num 会被下一次同步清零,BattleEnd 的累加会漏扣,故在此显式预扣并置防重标志)。
|
||||
|
||||
@@ -290,6 +290,8 @@ export class MissionHomeComp extends CCComp {
|
||||
home_active() {
|
||||
// 任务结束回到主页:确保指引动画与气泡无残留
|
||||
this.stopStartGuide();
|
||||
// 大厅地图恢复正常滚动速度(清除战斗节间过场的 4x 加速残留)
|
||||
smc.map?.MapView?.scene?.mapLayer?.resetSpeed();
|
||||
this.uodate_data()
|
||||
this.hideAllPages()
|
||||
this.refreshRedDots()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 应用引擎计算好的最终属性
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -86,14 +86,35 @@ export default class MapLayer extends Component {
|
||||
return [this.bg_anm, this.bg_anm1, this.bg_anm2, this.bg_anm3, this.bg_anm4].filter((a): a is Animation => a !== null);
|
||||
}
|
||||
|
||||
/** 背景滚动播放速度倍率(节间过场加快推进感) */
|
||||
private static readonly SCROLL_SPEED = 4;
|
||||
|
||||
public playAnimations(name?: string): void {
|
||||
this.animations.forEach(a => name ? a.play(name) : a.play());
|
||||
this.animations.forEach(a => {
|
||||
a.play(name);
|
||||
// 加快背景滚动速度,强化节间"向前推进"的观感(AnimationState.speed 属性)
|
||||
const clip = a.defaultClip;
|
||||
if (clip) {
|
||||
const state = a.getState(clip.name);
|
||||
if (state) state.speed = MapLayer.SCROLL_SPEED;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public stopAnimations(): void {
|
||||
this.animations.forEach(a => a.stop());
|
||||
}
|
||||
|
||||
/** 恢复背景滚动为正常速度(1x):大厅/非过场时调用,清除过场加速残留 */
|
||||
public resetSpeed(): void {
|
||||
this.animations.forEach(a => {
|
||||
const clip = a.defaultClip;
|
||||
if (!clip) return;
|
||||
const state = a.getState(clip.name);
|
||||
if (state) state.speed = 1;
|
||||
});
|
||||
}
|
||||
|
||||
public pauseAnimations(): void {
|
||||
this.animations.forEach(a => a.pause());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user