feat(关卡系统): 实现章节-关-节三级关卡推进机制
本次提交重构了关卡系统,实现了完整的小节级关卡推进流程: 1. 新增关卡配置表StageSet,定义章节-关-节三级结构与怪物数值公式 2. 重构刷怪逻辑,按小节配置生成对应怪物波次 3. 实现关卡进度存档与恢复,支持断点续玩 4. 新增结算弹窗分支逻辑,区分小节胜利/关卡通关/失败状态 5. 优化胜利按钮文案与显示逻辑,将"再来一局"改为"下一关" 6. 新增一套完整的关卡事件系统,驱动各模块协同工作
This commit is contained in:
@@ -1174,7 +1174,7 @@
|
|||||||
},
|
},
|
||||||
"_contentSize": {
|
"_contentSize": {
|
||||||
"__type__": "cc.Size",
|
"__type__": "cc.Size",
|
||||||
"width": 164,
|
"width": 124,
|
||||||
"height": 54.4
|
"height": 54.4
|
||||||
},
|
},
|
||||||
"_anchorPoint": {
|
"_anchorPoint": {
|
||||||
@@ -1210,7 +1210,7 @@
|
|||||||
"b": 255,
|
"b": 255,
|
||||||
"a": 255
|
"a": 255
|
||||||
},
|
},
|
||||||
"_string": "再来一局",
|
"_string": "下一关",
|
||||||
"_horizontalAlign": 1,
|
"_horizontalAlign": 1,
|
||||||
"_verticalAlign": 1,
|
"_verticalAlign": 1,
|
||||||
"_actualFontSize": 40,
|
"_actualFontSize": 40,
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export interface GameDate {
|
|||||||
talent_lv?: number,
|
talent_lv?: number,
|
||||||
/** 天赋当前等级已累计的经验值 */
|
/** 天赋当前等级已累计的经验值 */
|
||||||
talent_exp?: number,
|
talent_exp?: number,
|
||||||
|
/** 关卡进度存档:当前章节-关-节 */
|
||||||
|
stage_progress?: {
|
||||||
|
chapter: number,
|
||||||
|
stage: number,
|
||||||
|
section: number,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
export interface CloudData {
|
export interface CloudData {
|
||||||
openid: string;
|
openid: string;
|
||||||
@@ -145,6 +151,12 @@ export class SingletonModuleComp extends ecs.Comp {
|
|||||||
talent_lv: 0,
|
talent_lv: 0,
|
||||||
/** 当前等级已累计经验 */
|
/** 当前等级已累计经验 */
|
||||||
talent_exp: 0,
|
talent_exp: 0,
|
||||||
|
/** 关卡进度存档:默认从 1-1-1 开始 */
|
||||||
|
stage_progress: {
|
||||||
|
chapter: 1,
|
||||||
|
stage: 1,
|
||||||
|
section: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
vmdata: any = {
|
vmdata: any = {
|
||||||
|
|||||||
@@ -100,5 +100,15 @@ export enum GameEvent {
|
|||||||
HERO_GROWTH_UPDATE = "HERO_GROWTH_UPDATE",
|
HERO_GROWTH_UPDATE = "HERO_GROWTH_UPDATE",
|
||||||
/** 天赋数据变更(等级/点数更新,payload 为空,UI 重新读取 smc.data.talents / talent_points) */
|
/** 天赋数据变更(等级/点数更新,payload 为空,UI 重新读取 smc.data.talents / talent_points) */
|
||||||
TALENT_UPDATE = "TALENT_UPDATE",
|
TALENT_UPDATE = "TALENT_UPDATE",
|
||||||
|
/** 小节开始(payload: { chapter, stage, section },MissionMonComp 据此生成怪物) */
|
||||||
|
SectionStart = "SectionStart",
|
||||||
|
/** 小节结束(payload: { chapter, stage, section },玩家点击"下一节"后推进) */
|
||||||
|
SectionEnd = "SectionEnd",
|
||||||
|
/** 大关结束(payload: { chapter, stage },10 节打完) */
|
||||||
|
StageEnd = "StageEnd",
|
||||||
|
/** 章节结束(payload: { chapter },10 关打完) */
|
||||||
|
ChapterEnd = "ChapterEnd",
|
||||||
|
/** 结算弹窗点击"下一节"(payload 为空,MissionComp 据此推进到下一小节) */
|
||||||
|
NextSection = "NextSection",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
255
assets/script/game/common/config/StageSet.ts
Normal file
255
assets/script/game/common/config/StageSet.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* @file StageSet.ts
|
||||||
|
* @description 关卡系统配置(章节-关-节三级结构)
|
||||||
|
*
|
||||||
|
* 设计要点:
|
||||||
|
* - 1 章 = 10 关,每关 = 10 小节,共 100 个小节
|
||||||
|
* - 第 5 关为精英关,第 10 关为 Boss 关,其余为普通关
|
||||||
|
* - 怪物等级 = 关卡号(1-1 的怪为 Lv1,1-5 的怪为 Lv5)
|
||||||
|
* - 怪物属性 = 基础 × 开局系数 × (1 + 成长斜率 × (lv-1)) × 类型倍率
|
||||||
|
*
|
||||||
|
* 数值基准:
|
||||||
|
* - 开局系数 0.8(1 级小怪略弱于英雄)
|
||||||
|
* - 成长斜率 +5%/级(10 级 ≈ 1.45 倍)
|
||||||
|
* - 精英倍率 × 1.5,Boss 倍率 × 3.0(精英 × 2)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MonType } from "./heroSet";
|
||||||
|
|
||||||
|
// ======================== 常量 ========================
|
||||||
|
|
||||||
|
/** 怪物开局系数:1 级小怪略弱于英雄(0.8 倍) */
|
||||||
|
export const MON_START_COEF = 0.8;
|
||||||
|
/** 怪物成长斜率:每级 +5% 基础属性 */
|
||||||
|
export const MON_GROW_SLOPE = 0.05;
|
||||||
|
/** 精英属性倍率:同级普通怪的 1.5 倍 */
|
||||||
|
export const ELITE_MUL = 1.5;
|
||||||
|
/** Boss 属性倍率:同级普通怪的 3.0 倍(精英 × 2) */
|
||||||
|
export const BOSS_MUL = 3.0;
|
||||||
|
/** 普通关小怪数量 */
|
||||||
|
export const NORMAL_MON_COUNT = 9;
|
||||||
|
/** 普通关开局立即刷出数量 */
|
||||||
|
export const NORMAL_FIRST_SPAWN = 3;
|
||||||
|
/** 普通关后续刷怪间隔(秒) */
|
||||||
|
export const NORMAL_SPAWN_INTERVAL = 5;
|
||||||
|
/** 精英关精英数量 */
|
||||||
|
export const ELITE_COUNT = 3;
|
||||||
|
/** Boss 关精英数量 */
|
||||||
|
export const BOSS_ELITE_COUNT = 2;
|
||||||
|
/** Boss 关 Boss 延迟刷出时间(秒) */
|
||||||
|
export const BOSS_DELAY = 1;
|
||||||
|
|
||||||
|
// ======================== 类型定义 ========================
|
||||||
|
|
||||||
|
/** 小节类型 */
|
||||||
|
export enum SectionType {
|
||||||
|
Normal = 0, // 普通关:9 小怪
|
||||||
|
Elite = 1, // 精英关:3 精英
|
||||||
|
Boss = 2, // Boss 关:1 Boss + 2 精英
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 小节配置 */
|
||||||
|
export interface SectionConfig {
|
||||||
|
/** 小节类型 */
|
||||||
|
type: SectionType;
|
||||||
|
/** 怪物等级(驱动线性成长) */
|
||||||
|
monLv: number;
|
||||||
|
/** 精英数量(精英关/Boss 关用) */
|
||||||
|
eliteCount?: number;
|
||||||
|
/** Boss UUID(Boss 关用,可选,缺省随机) */
|
||||||
|
bossUuid?: number;
|
||||||
|
/** 怪物类型池(普通关用,缺省随机) */
|
||||||
|
monTypes?: MonType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 大关配置(1-1 ~ 1-10) */
|
||||||
|
export interface StageConfig {
|
||||||
|
/** 章节号 */
|
||||||
|
chapter: number;
|
||||||
|
/** 关卡号 1~10 */
|
||||||
|
stage: number;
|
||||||
|
/** 10 个小节 */
|
||||||
|
sections: SectionConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 章节配置 */
|
||||||
|
export interface ChapterConfig {
|
||||||
|
/** 章节号 */
|
||||||
|
chapter: number;
|
||||||
|
/** 章节名称 */
|
||||||
|
name: string;
|
||||||
|
/** 10 个大关 */
|
||||||
|
stages: StageConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 工具函数 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算怪物最终属性
|
||||||
|
* @param baseHp 基础 HP
|
||||||
|
* @param baseAp 基础 AP
|
||||||
|
* @param lv 怪物等级
|
||||||
|
* @param isElite 是否精英
|
||||||
|
* @param isBoss 是否 Boss
|
||||||
|
* @returns 最终 HP/AP
|
||||||
|
*/
|
||||||
|
export function calcMonsterAttrs(
|
||||||
|
baseHp: number,
|
||||||
|
baseAp: number,
|
||||||
|
lv: number,
|
||||||
|
isElite: boolean = false,
|
||||||
|
isBoss: boolean = false
|
||||||
|
): { hp: number; ap: number } {
|
||||||
|
const growCoef = 1 + MON_GROW_SLOPE * (lv - 1);
|
||||||
|
let typeMul = 1;
|
||||||
|
if (isBoss) {
|
||||||
|
typeMul = BOSS_MUL;
|
||||||
|
} else if (isElite) {
|
||||||
|
typeMul = ELITE_MUL;
|
||||||
|
}
|
||||||
|
const hp = Math.max(1, Math.round(baseHp * MON_START_COEF * growCoef * typeMul));
|
||||||
|
const ap = Math.max(1, Math.round(baseAp * MON_START_COEF * growCoef * typeMul));
|
||||||
|
return { hp, ap };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定关卡的怪物等级
|
||||||
|
* @param stage 关卡号(1~10)
|
||||||
|
* @returns 怪物等级
|
||||||
|
*/
|
||||||
|
export function getMonsterLv(stage: number): number {
|
||||||
|
return Math.max(1, Math.min(10, stage));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为精英关
|
||||||
|
* @param stage 关卡号(1~10)
|
||||||
|
*/
|
||||||
|
export function isEliteStage(stage: number): boolean {
|
||||||
|
return stage === 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为 Boss 关
|
||||||
|
* @param stage 关卡号(1~10)
|
||||||
|
*/
|
||||||
|
export function isBossStage(stage: number): boolean {
|
||||||
|
return stage === 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取小节类型
|
||||||
|
* @param stage 关卡号(1~10)
|
||||||
|
*/
|
||||||
|
export function getSectionType(stage: number): SectionType {
|
||||||
|
if (isBossStage(stage)) return SectionType.Boss;
|
||||||
|
if (isEliteStage(stage)) return SectionType.Elite;
|
||||||
|
return SectionType.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 第 1 章配置 ========================
|
||||||
|
|
||||||
|
/** 生成单个大关的 10 个小节配置 */
|
||||||
|
function makeStageSections(stage: number): SectionConfig[] {
|
||||||
|
const type = getSectionType(stage);
|
||||||
|
const monLv = getMonsterLv(stage);
|
||||||
|
const sections: SectionConfig[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
const section: SectionConfig = { type, monLv };
|
||||||
|
|
||||||
|
if (type === SectionType.Elite) {
|
||||||
|
section.eliteCount = ELITE_COUNT;
|
||||||
|
} else if (type === SectionType.Boss) {
|
||||||
|
section.eliteCount = BOSS_ELITE_COUNT;
|
||||||
|
// bossUuid 缺省随机,由生成引擎从 Boss 池抽取
|
||||||
|
} else {
|
||||||
|
// 普通关:近战为主,穿插远程/重型
|
||||||
|
section.monTypes = [MonType.Melee, MonType.Melee, MonType.Melee, MonType.Heavy, MonType.Long];
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.push(section);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 第 1 章配置 */
|
||||||
|
export const Chapter1: ChapterConfig = {
|
||||||
|
chapter: 1,
|
||||||
|
name: "兽人入侵",
|
||||||
|
stages: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化第 1 章 10 个大关
|
||||||
|
for (let stage = 1; stage <= 10; stage++) {
|
||||||
|
Chapter1.stages.push({
|
||||||
|
chapter: 1,
|
||||||
|
stage,
|
||||||
|
sections: makeStageSections(stage),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 所有章节配置表(当前仅第 1 章,后续扩展) */
|
||||||
|
export const ChapterSet: Record<number, ChapterConfig> = {
|
||||||
|
1: Chapter1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======================== 查询接口 ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取小节配置
|
||||||
|
* @param chapter 章节号
|
||||||
|
* @param stage 关卡号
|
||||||
|
* @param section 小节号(1~10)
|
||||||
|
* @returns 小节配置,不存在时返回 null
|
||||||
|
*/
|
||||||
|
export function getSectionConfig(chapter: number, stage: number, section: number): SectionConfig | null {
|
||||||
|
const ch = ChapterSet[chapter];
|
||||||
|
if (!ch) return null;
|
||||||
|
const st = ch.stages.find(s => s.stage === stage);
|
||||||
|
if (!st) return null;
|
||||||
|
if (section < 1 || section > st.sections.length) return null;
|
||||||
|
return st.sections[section - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取大关配置
|
||||||
|
* @param chapter 章节号
|
||||||
|
* @param stage 关卡号
|
||||||
|
* @returns 大关配置,不存在时返回 null
|
||||||
|
*/
|
||||||
|
export function getStageConfig(chapter: number, stage: number): StageConfig | null {
|
||||||
|
const ch = ChapterSet[chapter];
|
||||||
|
if (!ch) return null;
|
||||||
|
return ch.stages.find(s => s.stage === stage) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取章节配置
|
||||||
|
* @param chapter 章节号
|
||||||
|
* @returns 章节配置,不存在时返回 null
|
||||||
|
*/
|
||||||
|
export function getChapterConfig(chapter: number): ChapterConfig | null {
|
||||||
|
return ChapterSet[chapter] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前进度显示文本
|
||||||
|
* @param chapter 章节号
|
||||||
|
* @param stage 关卡号
|
||||||
|
* @param section 小节号
|
||||||
|
* @returns 格式化文本,如 "1-5-3"
|
||||||
|
*/
|
||||||
|
export function getProgressText(chapter: number, stage: number, section: number): string {
|
||||||
|
return `${chapter}-${stage}-${section}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取大关显示文本
|
||||||
|
* @param chapter 章节号
|
||||||
|
* @param stage 关卡号
|
||||||
|
* @returns 格式化文本,如 "1-5"
|
||||||
|
*/
|
||||||
|
export function getStageText(chapter: number, stage: number): string {
|
||||||
|
return `${chapter}-${stage}`;
|
||||||
|
}
|
||||||
9
assets/script/game/common/config/StageSet.ts.meta
Normal file
9
assets/script/game/common/config/StageSet.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "f2599ec4-e0c3-4d35-8ca3-5054e674d927",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -3,28 +3,25 @@
|
|||||||
* @description 任务(关卡)核心控制组件(UI + 逻辑层)
|
* @description 任务(关卡)核心控制组件(UI + 逻辑层)
|
||||||
*
|
*
|
||||||
* 职责:
|
* 职责:
|
||||||
* 1. 管理单局游戏的 **完整生命周期**:初始化 → 准备阶段 → 战斗阶段 → 结算。
|
* 1. 管理单局游戏的 **完整生命周期**:初始化 → 小节循环(战斗→结算弹窗)→ 章节通关。
|
||||||
* 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。
|
* 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。
|
||||||
* 3. 管理怪物数量阈值(暂停 / 恢复刷怪的上下限)。
|
* 3. 驱动小节推进:SectionStart → 战斗 → VictoryComp 结算 → 下一节/下一关。
|
||||||
* 4. 处理新一回合事件(NewWave),进入准备阶段并发放金币奖励。
|
* 4. 处理关卡进度持久化(chapter / stage / section)。
|
||||||
* 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。
|
* 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。
|
||||||
* 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。
|
* 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。
|
||||||
*
|
*
|
||||||
* 关键设计:
|
* 关键设计:
|
||||||
* - mission_start() 初始化所有游戏数据 → 进入准备阶段 → 显示 loading。
|
* - mission_start() 初始化所有游戏数据 → 恢复存档进度 → 进入第一小节。
|
||||||
* - 准备阶段(enterPreparePhase):停止刷怪,显示开始按钮。
|
* - 每节即一局:怪物全灭后弹 VictoryComp 结算。
|
||||||
* - 战斗阶段(to_fight):开始刷怪,隐藏按钮,由 update 驱动。
|
* - 第 1~9 节胜利点"下一节"继续;第 10 节胜利显示"本关通关",确认退出并存档进下一关。
|
||||||
* - 怪物数量管理采用 max/resume 双阈值:
|
* - 每节完全独立:进入下一节时重派 MissionStart 触发三选一,英雄重新召唤。
|
||||||
* * 超过 max → 暂停刷怪(stop_spawn_mon=true)
|
|
||||||
* * 降至 resume 以下 → 恢复刷怪
|
|
||||||
* - cleanComponents() 在任务开始/结束时销毁所有英雄和技能 ECS 实体。
|
* - cleanComponents() 在任务开始/结束时销毁所有英雄和技能 ECS 实体。
|
||||||
* - clearBattlePools() 回收对象池(Monster / Skill / Tooltip)。
|
* - clearBattlePools() 回收对象池(Monster / Skill / Tooltip)。
|
||||||
*
|
*
|
||||||
* 依赖:
|
* 依赖:
|
||||||
* - smc.mission —— 全局任务运行状态(play / pause / in_fight / stop_spawn_mon 等)
|
* - smc.mission —— 全局任务运行状态(play / pause / in_fight / stop_spawn_mon 等)
|
||||||
* - smc.vmdata.mission_data —— 局内数据(金币 / 回合数 / 怪物数量等)
|
* - smc.vmdata.mission_data —— 局内数据(金币 / 回合数 / 怪物数量 / 关卡进度等)
|
||||||
* - FightSet —— 战斗常量配置
|
* - StageSet —— 关卡配置与数值公式
|
||||||
* - CardInitCoins —— 初始金币数
|
|
||||||
* - UIID.Victory —— 结算弹窗
|
* - UIID.Victory —— 结算弹窗
|
||||||
*/
|
*/
|
||||||
import { _decorator, Vec3, Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label, CCInteger, tween, v3, Tween, Widget, UIOpacity } from "cc";
|
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 { FieldSkillType } from "../common/config/SkillSet";
|
||||||
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
|
||||||
import { spawningEngine, WAVE_DURATION, BATTLE_TIMEOUT } from "./RogueConfig";
|
import { spawningEngine, WAVE_DURATION, BATTLE_TIMEOUT } from "./RogueConfig";
|
||||||
|
import {
|
||||||
|
getProgressText,
|
||||||
|
} from "../common/config/StageSet";
|
||||||
const { ccclass, property } = _decorator;
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
/** 任务(关卡)生命周期阶段 */
|
/** 任务(关卡)生命周期阶段 */
|
||||||
@@ -60,16 +60,19 @@ export enum MissionPhase {
|
|||||||
BattleStart = 4, // 战斗开始阶段 (2s)
|
BattleStart = 4, // 战斗开始阶段 (2s)
|
||||||
Battle = 5, // 战斗阶段 (刷怪、战斗中)
|
Battle = 5, // 战斗阶段 (刷怪、战斗中)
|
||||||
BattleEnd = 6, // 战斗结束阶段 (2s)
|
BattleEnd = 6, // 战斗结束阶段 (2s)
|
||||||
Settle = 7 // 结算阶段
|
Settle = 7, // 结算阶段(VictoryComp 弹窗,等待玩家点击下一节/下一关/退出)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 结算类型(驱动 VictoryComp 按钮分流与文案) */
|
||||||
|
type SettleType = "defeat" | "section" | "final";
|
||||||
|
|
||||||
//@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理
|
//@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MissionComp —— 任务(关卡)核心控制器
|
* MissionComp —— 任务(关卡)核心控制器
|
||||||
*
|
*
|
||||||
* 驱动单局游戏的完整流程:准备 → 战斗 → 结算。
|
* 驱动单局游戏的完整流程:准备 → 战斗 → 小节结算 → 大关结算 → 章节结算。
|
||||||
* 管理战斗计时、怪物数量控制、英雄全灭检测和金币奖励发放。
|
* 管理战斗计时、怪物数量控制、英雄全灭检测和关卡进度推进。
|
||||||
*/
|
*/
|
||||||
@ccclass('MissionComp')
|
@ccclass('MissionComp')
|
||||||
@ecs.register('MissionComp', false)
|
@ecs.register('MissionComp', false)
|
||||||
@@ -102,7 +105,7 @@ export class MissionComp extends CCComp {
|
|||||||
[MissionPhase.BattleStart]: "战斗开始",
|
[MissionPhase.BattleStart]: "战斗开始",
|
||||||
[MissionPhase.Battle]: "战斗中",
|
[MissionPhase.Battle]: "战斗中",
|
||||||
[MissionPhase.BattleEnd]: "战斗结束",
|
[MissionPhase.BattleEnd]: "战斗结束",
|
||||||
[MissionPhase.Settle]: "结算阶段"
|
[MissionPhase.Settle]: "结算阶段",
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================== 运行时状态 ========================
|
// ======================== 运行时状态 ========================
|
||||||
@@ -171,11 +174,24 @@ export class MissionComp extends CCComp {
|
|||||||
private isWaveCountdown: boolean = false;
|
private isWaveCountdown: boolean = false;
|
||||||
/** 本局是否胜利(open_Victory 时记录,供结算弹窗使用) */
|
/** 本局是否胜利(open_Victory 时记录,供结算弹窗使用) */
|
||||||
private _victory: boolean = false;
|
private _victory: boolean = false;
|
||||||
|
/** 本次结算类型(小节胜利/末关通关/失败,供结算弹窗按钮分流) */
|
||||||
|
private _settleType: SettleType = "defeat";
|
||||||
|
/** 末关通关时结算页展示的标题("本关通关"/"本章通关"),其余结算为空 */
|
||||||
|
private _settleTitle: string = "";
|
||||||
/** 结算弹窗延迟(秒):等待最后一个英雄/怪物死亡动画播完再切 BattleEnd */
|
/** 结算弹窗延迟(秒):等待最后一个英雄/怪物死亡动画播完再切 BattleEnd */
|
||||||
private static readonly SETTLE_DELAY = 1.5;
|
private static readonly SETTLE_DELAY = 1.5;
|
||||||
/** 结算是否已预约调度(防止 0.2s 同步周期内重复 scheduleOnce) */
|
/** 结算是否已预约调度(防止 0.2s 同步周期内重复 scheduleOnce) */
|
||||||
private _settleScheduled: boolean = false;
|
private _settleScheduled: boolean = false;
|
||||||
|
|
||||||
|
// ======================== 关卡进度状态 ========================
|
||||||
|
|
||||||
|
/** 当前章节号 */
|
||||||
|
public currentChapter: number = 1;
|
||||||
|
/** 当前关卡号(1~10) */
|
||||||
|
public currentStage: number = 1;
|
||||||
|
/** 当前小节号(1~10) */
|
||||||
|
public currentSection: number = 1;
|
||||||
|
|
||||||
// ======================== ECS 查询匹配器(预缓存) ========================
|
// ======================== ECS 查询匹配器(预缓存) ========================
|
||||||
|
|
||||||
/** 匹配拥有 HeroViewComp 的实体(英雄/怪物视图) */
|
/** 匹配拥有 HeroViewComp 的实体(英雄/怪物视图) */
|
||||||
@@ -196,6 +212,8 @@ export class MissionComp extends CCComp {
|
|||||||
this.on(GameEvent.MissionEnd, this.mission_end, this)
|
this.on(GameEvent.MissionEnd, this.mission_end, this)
|
||||||
this.on(GameEvent.NewWave, this.onNewWave, this)
|
this.on(GameEvent.NewWave, this.onNewWave, this)
|
||||||
this.on(GameEvent.DO_AD_BACK, this.do_ad, 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)
|
this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||||||
// 开局免费三选一选完后自动进入战斗
|
// 开局免费三选一选完后自动进入战斗
|
||||||
oops.message.on("OpeningPicksDone", this.onOpeningPicksDone, this);
|
oops.message.on("OpeningPicksDone", this.onOpeningPicksDone, this);
|
||||||
@@ -317,7 +335,7 @@ export class MissionComp extends CCComp {
|
|||||||
if (phaseNode) {
|
if (phaseNode) {
|
||||||
const label = phaseNode.getComponent(Label);
|
const label = phaseNode.getComponent(Label);
|
||||||
if (label) {
|
if (label) {
|
||||||
label.string = `下一回合 ${remain}s`;
|
label.string = `下一节 ${remain}s`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,15 +378,22 @@ export class MissionComp extends CCComp {
|
|||||||
* 1. 取消上一局延迟回调。
|
* 1. 取消上一局延迟回调。
|
||||||
* 2. 清理残留实体。
|
* 2. 清理残留实体。
|
||||||
* 3. 初始化全部局内数据。
|
* 3. 初始化全部局内数据。
|
||||||
* 4. 分发 FightReady 事件。
|
* 4. 恢复关卡进度存档。
|
||||||
* 5. 进入准备阶段并显示 loading。
|
* 5. 进入准备阶段并显示 loading。
|
||||||
*/
|
*/
|
||||||
async mission_start() {
|
async mission_start() {
|
||||||
this.unscheduleAllCallbacks();
|
this.unscheduleAllCallbacks();
|
||||||
this.cleanComponents();
|
this.cleanComponents();
|
||||||
this.data_init()
|
this.data_init()
|
||||||
|
this.loadStageProgress();
|
||||||
oops.message.dispatchEvent(GameEvent.FightReady)
|
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")
|
let loading = this.node.getChildByName("loading")
|
||||||
if (loading) {
|
if (loading) {
|
||||||
loading.active = true
|
loading.active = true
|
||||||
@@ -468,8 +493,7 @@ export class MissionComp extends CCComp {
|
|||||||
if (phaseNode) {
|
if (phaseNode) {
|
||||||
const label = phaseNode.getComponent(Label);
|
const label = phaseNode.getComponent(Label);
|
||||||
if (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 = `第 ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)} 节`;
|
||||||
label.string = `第 ${wave} 回合`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阶段切换动感表现:只在进入战斗阶段跳动一下,让流程充满心流体验
|
// 阶段切换动感表现:只在进入战斗阶段跳动一下,让流程充满心流体验
|
||||||
@@ -642,7 +666,10 @@ export class MissionComp extends CCComp {
|
|||||||
all_alive: this.checkAllHeroAlive(),
|
all_alive: this.checkAllHeroAlive(),
|
||||||
rewards: this.rewards,
|
rewards: this.rewards,
|
||||||
game_data: this.game_data,
|
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 {
|
} else {
|
||||||
// 如果 play 已经是 false,说明是通过 fight_end 进来的
|
// 如果 play 已经是 false,说明是通过 fight_end 进来的
|
||||||
@@ -732,6 +759,12 @@ export class MissionComp extends CCComp {
|
|||||||
this.to_fight();
|
this.to_fight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 结算弹窗"下一节"事件:关闭弹窗并推进到下一小节 */
|
||||||
|
private onNextSection() {
|
||||||
|
oops.gui.remove(UIID.Victory);
|
||||||
|
this.advanceToNextSection();
|
||||||
|
}
|
||||||
|
|
||||||
/** 开局免费三选一选完,自动进入战斗(等效于点击开始按钮) */
|
/** 开局免费三选一选完,自动进入战斗(等效于点击开始按钮) */
|
||||||
private onOpeningPicksDone() {
|
private onOpeningPicksDone() {
|
||||||
if (this.currentPhase !== MissionPhase.Prepare) return;
|
if (this.currentPhase !== MissionPhase.Prepare) return;
|
||||||
@@ -743,7 +776,8 @@ export class MissionComp extends CCComp {
|
|||||||
/**
|
/**
|
||||||
* 打开结算弹窗:
|
* 打开结算弹窗:
|
||||||
* - 暂停游戏
|
* - 暂停游戏
|
||||||
* - 打开 VictoryComp 弹窗
|
* - 按当前进度判定结算类型(小节胜利 / 末关通关 / 失败)
|
||||||
|
* - 切入 BattleEnd,2s 后由 autoNextPhase 自动弹出 VictoryComp
|
||||||
*
|
*
|
||||||
* @param e 事件对象(未使用)
|
* @param e 事件对象(未使用)
|
||||||
* @param is_hero_dead 是否因英雄全灭触发
|
* @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) {
|
open_Victory(e: any, is_hero_dead: boolean = false, victory: boolean = false) {
|
||||||
// 战斗失败或胜利,标记暂停状态以切断回合流转逻辑
|
// 战斗失败或胜利,标记暂停状态以切断回合流转逻辑
|
||||||
smc.mission.pause = true;
|
smc.mission.pause = true;
|
||||||
// 记录胜利标记,供 BattleEnd 阶段结算弹窗使用
|
// 记录胜利标记,供结算弹窗使用
|
||||||
this._victory = victory;
|
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 自动流转弹窗结算
|
// 直接切入 BattleEnd,2s 后由 autoNextPhase 自动流转弹窗结算
|
||||||
this.changePhase(MissionPhase.BattleEnd);
|
this.changePhase(MissionPhase.BattleEnd);
|
||||||
}
|
}
|
||||||
@@ -835,6 +896,83 @@ export class MissionComp extends CCComp {
|
|||||||
smc.vmdata.scores.gold_earned += smc.vmdata.mission_data.coin;
|
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);
|
this.handleHeroWipe(heroCount);
|
||||||
|
|
||||||
// 怪物全灭检测:一场定胜负,全灭即整局通关,直接进入结算
|
// 怪物全灭检测:本小节怪物全灭 + 待刷队列空 → 弹结算窗(小节胜利,按钮按进度显示下一节/下一关)
|
||||||
const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0;
|
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) {
|
if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
|
||||||
// oops.message 全局事件总线:清屏庆祝(横幅/奖励由 BattleBannerComp 消费),需在结算前派发(clearTime 归零前取值)
|
// oops.message 全局事件总线:清屏庆祝(横幅/奖励由 BattleBannerComp 消费),需在结算前派发(clearTime 归零前取值)
|
||||||
@@ -915,7 +1053,7 @@ export class MissionComp extends CCComp {
|
|||||||
allAlive,
|
allAlive,
|
||||||
fastClear,
|
fastClear,
|
||||||
});
|
});
|
||||||
// 最后一个怪物死亡:延迟结算,等待死亡动画播放完再弹窗
|
// 小节胜利:延迟弹结算窗,等待死亡动画播放完
|
||||||
this.scheduleSettle(false, true);
|
this.scheduleSettle(false, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -961,6 +1099,7 @@ export class MissionComp extends CCComp {
|
|||||||
// 延迟期间局内状态可能已变化(如复活、任务结束),兜底校验
|
// 延迟期间局内状态可能已变化(如复活、任务结束),兜底校验
|
||||||
if (!smc.mission.play || smc.mission.pause) return;
|
if (!smc.mission.play || smc.mission.pause) return;
|
||||||
if (this.currentPhase !== MissionPhase.Battle) return;
|
if (this.currentPhase !== MissionPhase.Battle) return;
|
||||||
|
// 胜利/失败统一走结算弹窗:open_Victory 内部按进度判定小节/大关/章节结算类型
|
||||||
this.open_Victory(null, is_hero_dead, victory);
|
this.open_Victory(null, is_hero_dead, victory);
|
||||||
}, MissionComp.SETTLE_DELAY);
|
}, MissionComp.SETTLE_DELAY);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
* @description 怪物(Monster)登场管理组件(逻辑层)
|
* @description 怪物(Monster)登场管理组件(逻辑层)
|
||||||
*
|
*
|
||||||
* 职责:
|
* 职责:
|
||||||
* 1. 预生成战斗怪物:FightReady 时通过 RogueSpawningEngine.generateBossCombo 随机一组 Boss 组合,并立即登场展示(供玩家侦查后再选英雄)。
|
* 1. 按小节配置刷怪:监听 SectionStart 事件,从 StageSpawningEngine 获取怪物列表。
|
||||||
* 2. 战斗开始:PhasePrepareEnd 时恢复怪物行动(in_fight 前怪物仅原地待命,不会推进/索敌)。
|
* 2. 普通关:前 3 只立即刷,后续每 5s 刷 1 只(共 9 只)。
|
||||||
|
* 3. 精英关:3 只精英同时刷出。
|
||||||
|
* 4. Boss 关:2 精英先刷,Boss 延迟 1s 刷出。
|
||||||
|
* 5. 维护小节刷怪计数,供 MissionComp 检测小节结束。
|
||||||
*
|
*
|
||||||
* 关键设计:
|
* 关键设计:
|
||||||
* - 怪物按类型落排(近战/Boss→前排、远程/刺客→中排、辅助/召唤→后排),与英雄三排镜像对应。
|
* - 怪物按类型落排(近战/Boss→前排、远程/刺客→中排、辅助/召唤→后排),与英雄三排镜像对应。
|
||||||
@@ -24,6 +27,7 @@ import { MonType } from "../common/config/heroSet";
|
|||||||
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
|
import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig";
|
||||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||||
import { MoveComp } from "../hero/MoveComp";
|
import { MoveComp } from "../hero/MoveComp";
|
||||||
|
import { NORMAL_FIRST_SPAWN, NORMAL_SPAWN_INTERVAL } from "../common/config/StageSet";
|
||||||
|
|
||||||
const { ccclass, property } = _decorator;
|
const { ccclass, property } = _decorator;
|
||||||
|
|
||||||
@@ -77,68 +81,116 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
|
|
||||||
/** 全局生成顺序计数器(用于渲染层级排序) */
|
/** 全局生成顺序计数器(用于渲染层级排序) */
|
||||||
private globalSpawnOrder: number = 0;
|
private globalSpawnOrder: number = 0;
|
||||||
/** 战斗场次(本局恒为 1,保留供 HUD/事件协议) */
|
/** 当前小节目标怪物总数 */
|
||||||
private currentWave: number = 1;
|
private sectionTargetCount: number = 0;
|
||||||
/** 本场战斗的怪物总数 */
|
/** 当前小节已刷出怪物数量 */
|
||||||
private waveTargetCount: number = 0;
|
private sectionSpawnedCount: number = 0;
|
||||||
/** 已登场的怪物数量 */
|
/** 待刷出队列(batch=1 的怪物) */
|
||||||
private waveSpawnedCount: number = 0;
|
private pendingQueue: GeneratedMonster[] = [];
|
||||||
/** 待登场队列(FightReady 时整组进入,PhasePrepareEnd 时一次全量刷出) */
|
/** 刷怪定时器已累积时间 */
|
||||||
private spawnQueue: GeneratedMonster[] = [];
|
private spawnTimer: number = 0;
|
||||||
|
/** 是否正在刷怪中 */
|
||||||
|
private isSpawning: boolean = false;
|
||||||
|
|
||||||
// ======================== 生命周期 ========================
|
// ======================== 生命周期 ========================
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.on(GameEvent.FightReady, this.fight_ready, this);
|
this.on(GameEvent.SectionStart, this.onSectionStart, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(dt: number): void {
|
protected update(dt: number): void {
|
||||||
// 待登场统计(战斗结束检测与 HUD 进度消费)
|
// 待登场统计(战斗结束检测与 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() { }
|
start() { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 战斗准备:重置运行时状态并预生成本场 Boss 组合。
|
* 小节开始事件回调:生成怪物列表并按节奏刷出
|
||||||
|
* @param event 事件名
|
||||||
|
* @param data { chapter, stage, section }
|
||||||
*/
|
*/
|
||||||
fight_ready() {
|
private onSectionStart(event: string, data: any) {
|
||||||
smc.vmdata.mission_data.mon_num = 0;
|
const chapter = Number(data?.chapter ?? 1);
|
||||||
smc.mission.stop_spawn_mon = false;
|
const stage = Number(data?.stage ?? 1);
|
||||||
|
const section = Number(data?.section ?? 1);
|
||||||
|
|
||||||
|
// 重置小节状态
|
||||||
this.globalSpawnOrder = 0;
|
this.globalSpawnOrder = 0;
|
||||||
this.currentWave = 1;
|
this.sectionSpawnedCount = 0;
|
||||||
this.waveTargetCount = 0;
|
this.pendingQueue = [];
|
||||||
this.waveSpawnedCount = 0;
|
this.spawnTimer = 0;
|
||||||
this.spawnQueue = [];
|
this.isSpawning = false;
|
||||||
|
smc.vmdata.mission_data.mon_num = 0;
|
||||||
// 随机一组 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 = [];
|
|
||||||
smc.vmdata.mission_data.pending_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) {
|
if (TestModeConfig.enable) {
|
||||||
mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启");
|
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, {
|
oops.message.dispatchEvent(GameEvent.NewWave, {
|
||||||
wave: this.currentWave,
|
wave: section,
|
||||||
total: this.waveTargetCount,
|
total: this.sectionTargetCount,
|
||||||
bossWave: monsters.some(m => m.isBoss),
|
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 monData 怪物生成数据
|
||||||
* 效果:高优先级类型永远占据更靠前的排;当更高优先级类型插入时,低优先级类型自动向后让位。
|
|
||||||
*
|
|
||||||
* @param index 当前怪物在排序后列表中的索引(名次)
|
|
||||||
* @returns 出生 X 坐标
|
|
||||||
*/
|
*/
|
||||||
private resolveSpawnX(index: number): number {
|
private spawnMonster(monData: GeneratedMonster) {
|
||||||
if (index === 0) return FightSet.MON_ROW_FRONT_X;
|
const posIndex = monData.spawnIndex;
|
||||||
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
|
|
||||||
) {
|
|
||||||
let mon = ecs.getEntity<Monster>(Monster);
|
let mon = ecs.getEntity<Monster>(Monster);
|
||||||
let scale = -1;
|
let scale = -1;
|
||||||
|
|
||||||
@@ -183,7 +217,7 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
(mon as any)._testSkills = monData.skills;
|
(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 登场(震屏/音效由表现层消费)
|
// oops.message 全局事件总线:Boss 登场(震屏/音效由表现层消费)
|
||||||
if (monData.isBoss) {
|
if (monData.isBoss) {
|
||||||
@@ -202,6 +236,22 @@ export class MissionMonCompComp extends CCComp {
|
|||||||
model.hp_max = monData.hp;
|
model.hp_max = monData.hp;
|
||||||
model.hp = model.hp_max;
|
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 组件移除时触发 */
|
/** ECS 组件移除时触发 */
|
||||||
|
|||||||
@@ -1,55 +1,38 @@
|
|||||||
/**
|
/**
|
||||||
* @file RogueConfig.ts
|
* @file RogueConfig.ts
|
||||||
* @description Boss 组合刷怪系统(单场 Boss 战定胜负)
|
* @description 关卡刷怪系统(章节-关-节三级推进)
|
||||||
*
|
*
|
||||||
* 游戏机制:
|
* 游戏机制:
|
||||||
* - 整局只有一场战斗:开局英雄三次三选一后自动开战。
|
* - 玩家线性推进 1 章 10 关 × 每关 10 小节,进度精确到小节并持久化。
|
||||||
* - 怪物从 BossComboLibrary 按权重随机抽一组(固定 3 只怪:0~2 Boss + 剩余小怪),
|
* - 普通关(1~4, 6~9):9 只小怪,前 3 只立即刷,后续每 5s 刷 1 只。
|
||||||
* 战斗开始瞬间一次全量登场(X∈[100,320] 均匀铺开),双方立即互相攻击。
|
* - 精英关(5):3 只精英同时刷出。
|
||||||
* - 怪物全灭 → 胜利结算;英雄全灭 → 失败结算;无回合递进、无分批刷怪。
|
* - Boss 关(10):2 精英先刷,Boss 延迟 1s 刷出。
|
||||||
|
* - 怪物属性 = 基础 × 0.8 × (1 + 0.05×(lv-1)) × 类型倍率(精英1.5 / Boss3.0)。
|
||||||
*
|
*
|
||||||
* 设计层次:
|
* 设计层次:
|
||||||
* 1. BossComboLibrary - Boss 组合库(小怪构成 + Boss/小怪技能池 + 抽取权重)
|
* 1. StageSet - 关卡配置(章节/关/节 + 数值公式)
|
||||||
* 2. MonSkillPool/BossSkillPool - 怪物技能池(atking / atked / dead 等全触发类型)
|
* 2. MonSkillPool/BossSkillPool - 怪物技能池(atking / atked / dead 等全触发类型)
|
||||||
* 3. RogueSpawningEngine - 生成引擎(基础属性 × 可调系数)
|
* 3. StageSpawningEngine - 生成引擎(按小节配置生成怪物列表)
|
||||||
*
|
*
|
||||||
* 核心公式(基础属性 × 可调系数):
|
* 核心公式:
|
||||||
* 小怪 hp = HeroInfo 基础hp × MON_HP_MUL,小怪 ap = HeroInfo 基础ap × MON_AP_MUL
|
* 最终属性 = HeroInfo 基础属性 × MON_START_COEF × (1 + MON_GROW_SLOPE×(lv-1)) × 类型倍率
|
||||||
* Boss 基础面板固定为初始英雄 2 倍强度(BOSS_BASE_HP/BOSS_BASE_AP),与小怪共用同一套系数,
|
* 1 级小怪 ≈ 英雄 × 0.8,10 级小怪 ≈ 英雄 × 1.16,保持平缓成长。
|
||||||
* 场上 Boss 恰好为小怪 2 倍强度(如需额外压场可调 BOSS_HP_BONUS_MUL)。
|
|
||||||
* 小怪基础面板与 1 级英雄持平(近战镜像 hp300/ap22/cd1.8),默认系数校准为
|
|
||||||
* 整场战斗(3 英雄 vs 3 只怪)双方对砍 ≈ WAVE_DURATION(30s)。
|
|
||||||
* 手感微调只改 MON_HP_MUL / MON_AP_MUL / BOSS_HP_BONUS_MUL 三个系数即可。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HeroInfo, MonType, MonTypeName, TriggerGrouped } from "../common/config/heroSet";
|
import { HeroInfo, MonType, MonTypeName, TriggerGrouped } from "../common/config/heroSet";
|
||||||
import { SkillOverrides } from "../common/config/SkillSet";
|
import { SkillOverrides } from "../common/config/SkillSet";
|
||||||
import { FacSet } from "../common/config/GameSet";
|
import { FacSet } from "../common/config/GameSet";
|
||||||
|
import {
|
||||||
|
SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs,
|
||||||
|
NORMAL_MON_COUNT, NORMAL_FIRST_SPAWN, ELITE_COUNT, BOSS_ELITE_COUNT, BOSS_DELAY,
|
||||||
|
MON_START_COEF, MON_GROW_SLOPE, ELITE_MUL, BOSS_MUL,
|
||||||
|
} from "../common/config/StageSet";
|
||||||
|
|
||||||
// ======================== 1. 战斗常量 ========================
|
// ======================== 1. 战斗常量 ========================
|
||||||
|
|
||||||
/** 单场战斗时长基准(秒):供清屏速度评价(BattleBannerComp 清屏奖励)等使用 */
|
/** 单场战斗时长基准(秒):供清屏速度评价(BattleBannerComp 清屏奖励)等使用 */
|
||||||
export const WAVE_DURATION = 30;
|
export const WAVE_DURATION = 30;
|
||||||
|
|
||||||
/** 怪物登场固定构成:单场战斗固定 3 只怪,Boss 数量由组合配置决定(0~2 个),
|
|
||||||
* 剩余位置由小怪填充。例如:3 小怪、1 Boss + 2 小怪、2 Boss + 1 小怪(最大难度) */
|
|
||||||
export const BOSS_COUNT = 2;
|
|
||||||
export const MINION_COUNT = 3;
|
|
||||||
|
|
||||||
/** 怪物血量可调系数:最终 hp = HeroInfo 基础hp × MON_HP_MUL。
|
|
||||||
* 2.6 = 校准后怪物总血量 / 3 英雄 30s 输出(≈30 DPS),双方对砍 ≈ 30s 分胜负 */
|
|
||||||
export const MON_HP_MUL = 2.6;
|
|
||||||
/** 怪物攻击可调系数:最终 ap = HeroInfo 基础ap × MON_AP_MUL。
|
|
||||||
* 0.9 使 4 小怪+1 Boss 的总 DPS ≈ 20,30s 恰好磨光 3 英雄总血量(≈600) */
|
|
||||||
export const MON_AP_MUL = 0.9;
|
|
||||||
|
|
||||||
/** Boss 基础面板 = 初始英雄基准(hp300/ap22)× 2 倍强度,
|
|
||||||
* 不读取 Boss HeroInfo 的差异化高基础值,避免数值膨胀 */
|
|
||||||
export const BOSS_BASE_HP = 600;
|
|
||||||
export const BOSS_BASE_AP = 44;
|
|
||||||
|
|
||||||
/** Boss 血量额外加成(在 MON_HP_MUL 之上乘算,默认 1.0 表示不再额外膨胀,保留作压场微调旋钮) */
|
|
||||||
export const BOSS_HP_BONUS_MUL = 1.0;
|
|
||||||
/** 战斗超时(秒):超过后强制结算整局(残留怪销毁并扣留存分) */
|
/** 战斗超时(秒):超过后强制结算整局(残留怪销毁并扣留存分) */
|
||||||
export const BATTLE_TIMEOUT = 90;
|
export const BATTLE_TIMEOUT = 90;
|
||||||
|
|
||||||
@@ -97,89 +80,7 @@ export function getMonsterGoldDrop(monType: number, isBoss: boolean): number {
|
|||||||
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
|
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 3. Boss 组合库 ========================
|
// ======================== 3. 怪物技能池 ========================
|
||||||
|
|
||||||
/** Boss 组合中小怪的槽位定义(type + 数量,Boss 另由 BOSS_COUNT 提供) */
|
|
||||||
export interface BossComboMinionSlot {
|
|
||||||
type: MonType;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boss 组合配置:一场战斗随机抽一组登场(固定 3 只怪,Boss 数量由 bossCount 决定) */
|
|
||||||
export interface BossComboConfig {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
/** 本场 Boss 数量(0~2),剩余位置由小怪填充 */
|
|
||||||
bossCount: number;
|
|
||||||
/** 小怪构成(合计数量建议等于 MINION_COUNT - bossCount,超出截断、不足自动补近战) */
|
|
||||||
minions: BossComboMinionSlot[];
|
|
||||||
/** Boss 技能池 id(引用 BossSkillPool,可选) */
|
|
||||||
boss_skill_pool?: string[];
|
|
||||||
/** 小怪技能池 id(引用 MonSkillPool,可选) */
|
|
||||||
skill_pool?: string[];
|
|
||||||
/** 在组合库中的抽取权重 */
|
|
||||||
weight: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boss 组合库(硬编码):每场战斗按权重随机抽一组,固定 3 只怪。
|
|
||||||
* 组合类型:
|
|
||||||
* - 3 小怪(0 Boss):均衡难度
|
|
||||||
* - 1 Boss + 2 小怪:标准难度
|
|
||||||
* - 2 Boss + 1 小怪:最大难度 */
|
|
||||||
export const BossComboLibrary: Record<string, BossComboConfig> = {
|
|
||||||
/** 3 小怪:均衡组合 */
|
|
||||||
rage_charge: {
|
|
||||||
id: "rage_charge", name: "狂暴冲锋组", weight: 10, bossCount: 0,
|
|
||||||
minions: [{ type: MonType.Melee, count: 2 }, { type: MonType.Heavy, count: 1 }],
|
|
||||||
},
|
|
||||||
iron_wall: {
|
|
||||||
id: "iron_wall", name: "铁壁推进组", weight: 8, bossCount: 0,
|
|
||||||
minions: [{ type: MonType.Heavy, count: 2 }, { type: MonType.Support, count: 1 }],
|
|
||||||
skill_pool: ["tough"],
|
|
||||||
},
|
|
||||||
doom_ritual: {
|
|
||||||
id: "doom_ritual", name: "毁灭仪式组", weight: 6, bossCount: 0,
|
|
||||||
minions: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 1 }, { type: MonType.Support, count: 1 }],
|
|
||||||
skill_pool: ["warcry"],
|
|
||||||
},
|
|
||||||
/** 1 Boss + 2 小怪:标准难度 */
|
|
||||||
assassin_raid: {
|
|
||||||
id: "assassin_raid", name: "刺客突袭组", weight: 7, bossCount: 1,
|
|
||||||
minions: [{ type: MonType.Assassin, count: 1 }, { type: MonType.Melee, count: 1 }],
|
|
||||||
boss_skill_pool: ["boss_rage", "boss_doom"],
|
|
||||||
skill_pool: ["berserk"],
|
|
||||||
},
|
|
||||||
blood_legion: {
|
|
||||||
id: "blood_legion", name: "吸血军团组", weight: 5, bossCount: 1,
|
|
||||||
minions: [{ type: MonType.Melee, count: 1 }, { type: MonType.Long, count: 1 }],
|
|
||||||
boss_skill_pool: ["boss_iron", "boss_doom"],
|
|
||||||
skill_pool: ["leech"],
|
|
||||||
},
|
|
||||||
/** 2 Boss + 1 小怪:最大难度 */
|
|
||||||
twin_boss: {
|
|
||||||
id: "twin_boss", name: "双王降临", weight: 3, bossCount: 2,
|
|
||||||
minions: [{ type: MonType.Support, count: 1 }],
|
|
||||||
boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按权重随机抽取一组 Boss 组合
|
|
||||||
* @returns 组合配置,库为空时返回 null
|
|
||||||
*/
|
|
||||||
export function pickBossCombo(): BossComboConfig | null {
|
|
||||||
const combos = Object.values(BossComboLibrary).filter(c => c && c.weight > 0);
|
|
||||||
if (combos.length === 0) return null;
|
|
||||||
const total = combos.reduce((s, c) => s + c.weight, 0);
|
|
||||||
let r = Math.random() * total;
|
|
||||||
for (const c of combos) {
|
|
||||||
r -= c.weight;
|
|
||||||
if (r <= 0) return c;
|
|
||||||
}
|
|
||||||
return combos[combos.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 4. 怪物技能池 ========================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 怪物技能套装(一套完整的触发技能配置)
|
* 怪物技能套装(一套完整的触发技能配置)
|
||||||
@@ -279,50 +180,17 @@ export const BossSkillPool: Record<string, MonSkillSet> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================== 5. 配置校验 ========================
|
// ======================== 4. 配置校验 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验 BossComboLibrary / MonSkillPool 引用一致性
|
* 校验 MonSkillPool / BossSkillPool 引用一致性
|
||||||
* 建议在游戏启动时调用一次,便于发现配置错误
|
* 建议在游戏启动时调用一次,便于发现配置错误
|
||||||
* @returns 错误信息数组,空数组表示校验通过
|
* @returns 错误信息数组,空数组表示校验通过
|
||||||
*/
|
*/
|
||||||
export function validateRogueConfig(): string[] {
|
export function validateRogueConfig(): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// 1. 校验组合引用与数量(固定 3 只怪:bossCount + minionTotal 必须等于 MINION_COUNT)
|
// 校验 Boss 类型在 MonList 中有怪
|
||||||
for (const id in BossComboLibrary) {
|
|
||||||
const combo = BossComboLibrary[id];
|
|
||||||
let minionTotal = 0;
|
|
||||||
for (const slot of combo.minions) {
|
|
||||||
minionTotal += slot.count;
|
|
||||||
if (!MonList[slot.type] || MonList[slot.type].length === 0) {
|
|
||||||
errors.push(`组合 ${id} 引用的怪物类型 ${slot.type}(${MonTypeName[slot.type] || "?"}) 在 MonList 中无可用 uuid`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const totalMon = combo.bossCount + minionTotal;
|
|
||||||
if (totalMon !== MINION_COUNT) {
|
|
||||||
errors.push(`组合 ${id} 总怪数=${totalMon}(bossCount=${combo.bossCount} + minions=${minionTotal})与 MINION_COUNT=${MINION_COUNT} 不一致`);
|
|
||||||
}
|
|
||||||
if (combo.bossCount < 0 || combo.bossCount > BOSS_COUNT) {
|
|
||||||
errors.push(`组合 ${id} bossCount=${combo.bossCount} 超出允许范围 [0, ${BOSS_COUNT}]`);
|
|
||||||
}
|
|
||||||
if (combo.skill_pool) {
|
|
||||||
for (const skillId of combo.skill_pool) {
|
|
||||||
if (!MonSkillPool[skillId]) {
|
|
||||||
errors.push(`组合 ${id} 引用了不存在的技能: ${skillId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (combo.boss_skill_pool) {
|
|
||||||
for (const skillId of combo.boss_skill_pool) {
|
|
||||||
if (!BossSkillPool[skillId]) {
|
|
||||||
errors.push(`组合 ${id} 引用了不存在的 Boss 技能: ${skillId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 校验 Boss 类型在 MonList 中有怪
|
|
||||||
if ((!MonList[MonType.MeleeBoss] || MonList[MonType.MeleeBoss].length === 0)
|
if ((!MonList[MonType.MeleeBoss] || MonList[MonType.MeleeBoss].length === 0)
|
||||||
&& (!MonList[MonType.LongBoss] || MonList[MonType.LongBoss].length === 0)) {
|
&& (!MonList[MonType.LongBoss] || MonList[MonType.LongBoss].length === 0)) {
|
||||||
errors.push("MonList 中无任何 Boss 类型(MeleeBoss/LongBoss)可用 uuid");
|
errors.push("MonList 中无任何 Boss 类型(MeleeBoss/LongBoss)可用 uuid");
|
||||||
@@ -331,7 +199,7 @@ export function validateRogueConfig(): string[] {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 6. 生成结果接口 ========================
|
// ======================== 5. 生成结果接口 ========================
|
||||||
|
|
||||||
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
|
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
|
||||||
export interface GeneratedMonster {
|
export interface GeneratedMonster {
|
||||||
@@ -340,11 +208,17 @@ export interface GeneratedMonster {
|
|||||||
hp: number;
|
hp: number;
|
||||||
ap: number;
|
ap: number;
|
||||||
isBoss: boolean;
|
isBoss: boolean;
|
||||||
|
/** 是否为精英(精英关/Boss 关的精英) */
|
||||||
|
isElite?: boolean;
|
||||||
/** 是否为 Boss 护卫队成员(供 UI/统计识别) */
|
/** 是否为 Boss 护卫队成员(供 UI/统计识别) */
|
||||||
isBossGuard?: boolean;
|
isBossGuard?: boolean;
|
||||||
|
/** 怪物等级(驱动技能等级增量,Mon.load 第 6 参) */
|
||||||
|
lv: number;
|
||||||
spawnIndex: number;
|
spawnIndex: number;
|
||||||
/** 本怪所属批次(保留字段,单场全量登场恒为 0) */
|
/** 本怪所属批次(0=首批立即刷,1=后续定时刷) */
|
||||||
batch: number;
|
batch: number;
|
||||||
|
/** 延迟刷出时间(秒),0 表示立即 */
|
||||||
|
delay?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 怪物技能套装(覆盖全部触发时机)
|
* 怪物技能套装(覆盖全部触发时机)
|
||||||
@@ -361,7 +235,7 @@ export interface GeneratedMonster {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 7. 测试模式配置 ========================
|
// ======================== 6. 测试模式配置 ========================
|
||||||
|
|
||||||
export const TestModeConfig = {
|
export const TestModeConfig = {
|
||||||
enable: false, // 默认关闭测试模式
|
enable: false, // 默认关闭测试模式
|
||||||
@@ -378,74 +252,138 @@ export const TestModeConfig = {
|
|||||||
fstart: undefined as TriggerGrouped | undefined,
|
fstart: undefined as TriggerGrouped | undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================== 8. 生成引擎 ========================
|
// ======================== 7. 生成引擎 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boss 组合生成引擎
|
* 关卡刷怪生成引擎
|
||||||
* 随机抽一组 Boss 组合,怪物最终属性 = HeroInfo 基础属性 × 可调系数
|
* 按章节-关-节配置生成怪物列表,属性走 StageSet 数值公式
|
||||||
*/
|
*/
|
||||||
export class RogueSpawningEngine {
|
export class StageSpawningEngine {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成一场战斗的怪物列表:随机一组 Boss 组合(固定 3 只怪,Boss 数量由 combo.bossCount 决定)。
|
* 生成指定小节的怪物列表
|
||||||
* Boss 压队尾(登场时由 MissionMonComp 统一布位)。
|
* @param chapter 章节号
|
||||||
* @returns 怪物列表(小怪在前、Boss 在尾)
|
* @param stage 关卡号
|
||||||
|
* @param section 小节号(1~10)
|
||||||
|
* @returns 怪物列表(按刷出顺序排列)
|
||||||
*/
|
*/
|
||||||
generateBossCombo(): GeneratedMonster[] {
|
generateSectionMonsters(chapter: number, stage: number, section: number): GeneratedMonster[] {
|
||||||
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
|
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
|
||||||
if (TestModeConfig.enable) {
|
if (TestModeConfig.enable) {
|
||||||
return this.generateTestWave();
|
return this.generateTestWave();
|
||||||
}
|
}
|
||||||
|
|
||||||
const combo = pickBossCombo();
|
const cfg = getSectionConfig(chapter, stage, section);
|
||||||
const monsters: GeneratedMonster[] = [];
|
if (!cfg) {
|
||||||
const bossCount = combo ? Math.max(0, Math.min(combo.bossCount, BOSS_COUNT)) : 0;
|
// 配置缺失兜底:生成 3 只 Lv1 近战小怪
|
||||||
|
return this.makeFallbackMonsters();
|
||||||
if (combo) {
|
|
||||||
for (const slot of combo.minions) {
|
|
||||||
for (let i = 0; i < slot.count; i++) {
|
|
||||||
monsters.push(this.makeMonster(slot.type, monsters.length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 组合库为空或数量不足兜底:补近战小怪至 MINION_COUNT - bossCount
|
|
||||||
const minionTarget = MINION_COUNT - bossCount;
|
|
||||||
while (monsters.length < minionTarget) {
|
|
||||||
monsters.push(this.makeMonster(MonType.Melee, monsters.length));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boss 压队尾
|
switch (cfg.type) {
|
||||||
for (let i = 0; i < bossCount; i++) {
|
case SectionType.Elite:
|
||||||
monsters.push(this.makeBoss());
|
return this.generateEliteSection(cfg);
|
||||||
|
case SectionType.Boss:
|
||||||
|
return this.generateBossSection(cfg);
|
||||||
|
case SectionType.Normal:
|
||||||
|
default:
|
||||||
|
return this.generateNormalSection(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 属性缩放:基础属性 × 可调系数(Boss 基础面板已是英雄 2 倍,BOSS_HP_BONUS_MUL 默认 1.0),
|
|
||||||
// 校准目标:Boss 场上强度恰为小怪 2 倍,双方对砍 ≈ 30s
|
|
||||||
for (const m of monsters) {
|
|
||||||
m.hp = Math.max(1, Math.round(m.hp * MON_HP_MUL * (m.isBoss ? BOSS_HP_BONUS_MUL : 1)));
|
|
||||||
m.ap = Math.max(1, Math.round(m.ap * MON_AP_MUL));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 挂载组合技能(Boss 专属池 / 小怪普通池)
|
|
||||||
for (const m of monsters) {
|
|
||||||
if (m.isBoss && combo?.boss_skill_pool) {
|
|
||||||
m.skills = this.pickSkillSet(combo.boss_skill_pool, BossSkillPool);
|
|
||||||
} else if (!m.isBoss && combo?.skill_pool) {
|
|
||||||
m.skills = this.pickSkillSet(combo.skill_pool, MonSkillPool);
|
|
||||||
}
|
|
||||||
m.batch = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < monsters.length; i++) {
|
|
||||||
monsters[i].spawnIndex = i;
|
|
||||||
}
|
|
||||||
return monsters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置引擎状态(每局开始时调用,预留) */
|
/** 重置引擎状态(每局开始时调用,预留) */
|
||||||
reset(): void {
|
reset(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 普通关:9 只小怪
|
||||||
|
* 前 3 只 batch=0(立即刷),后 6 只 batch=1(定时刷)
|
||||||
|
*/
|
||||||
|
private generateNormalSection(cfg: SectionConfig): GeneratedMonster[] {
|
||||||
|
const monsters: GeneratedMonster[] = [];
|
||||||
|
const types = cfg.monTypes ?? [MonType.Melee];
|
||||||
|
|
||||||
|
for (let i = 0; i < NORMAL_MON_COUNT; i++) {
|
||||||
|
const type = types[i % types.length];
|
||||||
|
const mon = this.makeMonster(type, cfg.monLv, i);
|
||||||
|
mon.batch = i < NORMAL_FIRST_SPAWN ? 0 : 1;
|
||||||
|
monsters.push(mon);
|
||||||
|
}
|
||||||
|
return monsters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精英关:3 只精英同时刷出
|
||||||
|
*/
|
||||||
|
private generateEliteSection(cfg: SectionConfig): GeneratedMonster[] {
|
||||||
|
const monsters: GeneratedMonster[] = [];
|
||||||
|
const count = cfg.eliteCount ?? ELITE_COUNT;
|
||||||
|
// 精英池:近战/重型/远程/刺客
|
||||||
|
const eliteTypes = [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Assassin];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const type = eliteTypes[i % eliteTypes.length];
|
||||||
|
const mon = this.makeMonster(type, cfg.monLv, i);
|
||||||
|
mon.isElite = true;
|
||||||
|
mon.batch = 0;
|
||||||
|
// 精英属性 = 普通 × ELITE_MUL
|
||||||
|
mon.hp = Math.max(1, Math.round(mon.hp * ELITE_MUL));
|
||||||
|
mon.ap = Math.max(1, Math.round(mon.ap * ELITE_MUL));
|
||||||
|
monsters.push(mon);
|
||||||
|
}
|
||||||
|
return monsters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boss 关:2 精英 + 1 Boss
|
||||||
|
* 精英先刷(batch=0),Boss 延迟刷出(batch=0, delay=BOSS_DELAY)
|
||||||
|
*/
|
||||||
|
private generateBossSection(cfg: SectionConfig): GeneratedMonster[] {
|
||||||
|
const monsters: GeneratedMonster[] = [];
|
||||||
|
const eliteCount = cfg.eliteCount ?? BOSS_ELITE_COUNT;
|
||||||
|
const eliteTypes = [MonType.Melee, MonType.Heavy];
|
||||||
|
|
||||||
|
// 先刷精英
|
||||||
|
for (let i = 0; i < eliteCount; i++) {
|
||||||
|
const type = eliteTypes[i % eliteTypes.length];
|
||||||
|
const mon = this.makeMonster(type, cfg.monLv, i);
|
||||||
|
mon.isElite = true;
|
||||||
|
mon.isBossGuard = true;
|
||||||
|
mon.batch = 0;
|
||||||
|
mon.hp = Math.max(1, Math.round(mon.hp * ELITE_MUL));
|
||||||
|
mon.ap = Math.max(1, Math.round(mon.ap * ELITE_MUL));
|
||||||
|
monsters.push(mon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boss 延迟刷出
|
||||||
|
const boss = this.makeBoss(cfg.monLv, monsters.length);
|
||||||
|
boss.batch = 0;
|
||||||
|
boss.delay = BOSS_DELAY;
|
||||||
|
// Boss 属性 = 普通 × BOSS_MUL
|
||||||
|
boss.hp = Math.max(1, Math.round(boss.hp * BOSS_MUL));
|
||||||
|
boss.ap = Math.max(1, Math.round(boss.ap * BOSS_MUL));
|
||||||
|
monsters.push(boss);
|
||||||
|
|
||||||
|
return monsters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配置缺失兜底:3 只 Lv1 近战小怪 */
|
||||||
|
private makeFallbackMonsters(): GeneratedMonster[] {
|
||||||
|
const monsters: GeneratedMonster[] = [];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
monsters.push({
|
||||||
|
uuid: 6001,
|
||||||
|
type: MonType.Melee,
|
||||||
|
hp: Math.round(560 * MON_START_COEF),
|
||||||
|
ap: Math.round(22 * MON_START_COEF),
|
||||||
|
isBoss: false,
|
||||||
|
lv: 1,
|
||||||
|
spawnIndex: i,
|
||||||
|
batch: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return monsters;
|
||||||
|
}
|
||||||
|
|
||||||
/** 测试模式:完全绕过引擎逻辑 */
|
/** 测试模式:完全绕过引擎逻辑 */
|
||||||
private generateTestWave(): GeneratedMonster[] {
|
private generateTestWave(): GeneratedMonster[] {
|
||||||
const count = Math.max(1, TestModeConfig.spawnCount || 1);
|
const count = Math.max(1, TestModeConfig.spawnCount || 1);
|
||||||
@@ -458,6 +396,7 @@ export class RogueSpawningEngine {
|
|||||||
hp: TestModeConfig.baseHp,
|
hp: TestModeConfig.baseHp,
|
||||||
ap: TestModeConfig.baseAp,
|
ap: TestModeConfig.baseAp,
|
||||||
isBoss: false,
|
isBoss: false,
|
||||||
|
lv: 1,
|
||||||
spawnIndex: i,
|
spawnIndex: i,
|
||||||
batch: 0,
|
batch: 0,
|
||||||
skills: {
|
skills: {
|
||||||
@@ -472,8 +411,8 @@ export class RogueSpawningEngine {
|
|||||||
return monsters;
|
return monsters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成 Boss(基础面板固定为初始英雄 2 倍强度,属性随后由 generateBossCombo 统一系数缩放) */
|
/** 生成 Boss(基础面板按 StageSet 公式计算) */
|
||||||
private makeBoss(): GeneratedMonster {
|
private makeBoss(monLv: number, spawnIndex: number): GeneratedMonster {
|
||||||
const isMeleeBoss = Math.random() < 0.5;
|
const isMeleeBoss = Math.random() < 0.5;
|
||||||
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
||||||
|
|
||||||
@@ -484,19 +423,26 @@ export class RogueSpawningEngine {
|
|||||||
}
|
}
|
||||||
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
|
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
|
||||||
|
|
||||||
|
// Boss 基础面板取 HeroInfo 基础值,属性由调用方乘 BOSS_MUL
|
||||||
|
const baseInfo = HeroInfo[uuid];
|
||||||
|
const baseHp = baseInfo ? baseInfo.hp : 600;
|
||||||
|
const baseAp = baseInfo ? baseInfo.ap : 30;
|
||||||
|
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false, false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uuid,
|
uuid,
|
||||||
type,
|
type,
|
||||||
hp: BOSS_BASE_HP,
|
hp: attrs.hp,
|
||||||
ap: BOSS_BASE_AP,
|
ap: attrs.ap,
|
||||||
isBoss: true,
|
isBoss: true,
|
||||||
spawnIndex: 0, // 占位值,由 generateBossCombo 统一分配
|
lv: monLv,
|
||||||
|
spawnIndex,
|
||||||
batch: 0,
|
batch: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成普通怪物 */
|
/** 生成普通怪物(属性按 StageSet 公式计算) */
|
||||||
private makeMonster(type: MonType, spawnIndex: number): GeneratedMonster {
|
private makeMonster(type: MonType, monLv: number, spawnIndex: number): GeneratedMonster {
|
||||||
let uuids = MonList[type];
|
let uuids = MonList[type];
|
||||||
if (!uuids || uuids.length === 0) {
|
if (!uuids || uuids.length === 0) {
|
||||||
// 兜底 Melee
|
// 兜底 Melee
|
||||||
@@ -507,13 +453,15 @@ export class RogueSpawningEngine {
|
|||||||
const baseInfo = HeroInfo[uuid];
|
const baseInfo = HeroInfo[uuid];
|
||||||
const baseHp = baseInfo ? baseInfo.hp : 100;
|
const baseHp = baseInfo ? baseInfo.hp : 100;
|
||||||
const baseAp = baseInfo ? baseInfo.ap : 10;
|
const baseAp = baseInfo ? baseInfo.ap : 10;
|
||||||
|
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false, false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uuid,
|
uuid,
|
||||||
type,
|
type,
|
||||||
hp: baseHp,
|
hp: attrs.hp,
|
||||||
ap: baseAp,
|
ap: attrs.ap,
|
||||||
isBoss: false,
|
isBoss: false,
|
||||||
|
lv: monLv,
|
||||||
spawnIndex,
|
spawnIndex,
|
||||||
batch: 0,
|
batch: 0,
|
||||||
};
|
};
|
||||||
@@ -550,6 +498,18 @@ export class RogueSpawningEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 9. 全局单例 ========================
|
// ======================== 8. 全局单例 ========================
|
||||||
|
|
||||||
export const spawningEngine = new RogueSpawningEngine();
|
export const spawningEngine = new StageSpawningEngine();
|
||||||
|
|
||||||
|
// 保留旧接口兼容(逐步迁移期使用)
|
||||||
|
/** @deprecated 请使用 StageSpawningEngine.generateSectionMonsters */
|
||||||
|
export class RogueSpawningEngine extends StageSpawningEngine {
|
||||||
|
/** @deprecated 请使用 generateSectionMonsters */
|
||||||
|
generateBossCombo(): GeneratedMonster[] {
|
||||||
|
return this.generateSectionMonsters(1, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated 请使用 spawningEngine */
|
||||||
|
export const rogueSpawningEngine = new RogueSpawningEngine();
|
||||||
|
|||||||
@@ -120,6 +120,16 @@ export class VictoryComp extends CCComp {
|
|||||||
@property({ type: Prefab, tooltip: "钻石奖励条目预制体(reward_item_diamond.prefab,挂 RewardItemComp)" })
|
@property({ type: Prefab, tooltip: "钻石奖励条目预制体(reward_item_diamond.prefab,挂 RewardItemComp)" })
|
||||||
reward_diamond_prefab: Prefab = null!;
|
reward_diamond_prefab: Prefab = null!;
|
||||||
|
|
||||||
|
// ======================== 关卡推进绑定 ========================
|
||||||
|
|
||||||
|
/** 下一节按钮(小节胜利时显示,末关通关/失败时隐藏) */
|
||||||
|
@property({ type: Node, tooltip: "下一节按钮(小节胜利时显示)" })
|
||||||
|
next_section_btn: Node = null!;
|
||||||
|
|
||||||
|
/** 通关标题(末关通关时显示"本关通关/本章通关") */
|
||||||
|
@property({ type: Label, tooltip: "通关标题 Label(末关通关时显示,默认隐藏)" })
|
||||||
|
settle_title: Label = null!;
|
||||||
|
|
||||||
/** 调试日志开关 */
|
/** 调试日志开关 */
|
||||||
debugMode: boolean = false;
|
debugMode: boolean = false;
|
||||||
/** 奖励等级(预留) */
|
/** 奖励等级(预留) */
|
||||||
@@ -158,6 +168,13 @@ export class VictoryComp extends CCComp {
|
|||||||
/** 是否可以复活(由 MissionComp 传入,取决于剩余复活次数) */
|
/** 是否可以复活(由 MissionComp 传入,取决于剩余复活次数) */
|
||||||
private canRevive: boolean = false;
|
private canRevive: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结算类型(由 MissionComp 传入,驱动主按钮行为):
|
||||||
|
* section - 小节胜利(主按钮"下一节",派发 NextSection)
|
||||||
|
* 其余 - 末关通关 / 失败(主按钮确认退出,走原结算退出流程)
|
||||||
|
*/
|
||||||
|
private settleType: string = "defeat";
|
||||||
|
|
||||||
/** 加载时隐藏 loading 遮罩 */
|
/** 加载时隐藏 loading 遮罩 */
|
||||||
protected onLoad(): void {
|
protected onLoad(): void {
|
||||||
this.node.getChildByName("loading").active = false
|
this.node.getChildByName("loading").active = false
|
||||||
@@ -170,6 +187,8 @@ export class VictoryComp extends CCComp {
|
|||||||
* @param args.rewards 掉落奖励列表
|
* @param args.rewards 掉落奖励列表
|
||||||
* @param args.game_data 累计数据 { exp, gold, diamond }
|
* @param args.game_data 累计数据 { exp, gold, diamond }
|
||||||
* @param args.can_revive 是否可复活
|
* @param args.can_revive 是否可复活
|
||||||
|
* @param args.settle_type 结算类型:section=小节胜利(下一节按钮),其余=确认退出
|
||||||
|
* @param args.settle_title 末关通关标题(如"本关通关"),非空时在结算页顶部展示
|
||||||
*/
|
*/
|
||||||
onAdded(args: any) {
|
onAdded(args: any) {
|
||||||
this.node.getChildByName("loading").active = false
|
this.node.getChildByName("loading").active = false
|
||||||
@@ -178,9 +197,17 @@ export class VictoryComp extends CCComp {
|
|||||||
this.game_data = args.game_data
|
this.game_data = args.game_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 结算类型:缺省按失败处理
|
||||||
|
this.settleType = args.settle_type ?? "defeat";
|
||||||
|
|
||||||
// 根据是否可复活决定按钮显示
|
// 根据是否可复活决定按钮显示
|
||||||
this.node.getChildByName("btns").getChildByName("next").active = !args.can_revive
|
this.node.getChildByName("btns").getChildByName("next").active = !args.can_revive
|
||||||
|
|
||||||
|
// 下一节按钮:仅小节胜利时显示
|
||||||
|
this.updateNextButtonState();
|
||||||
|
// 通关标题:仅末关通关时显示
|
||||||
|
this.updateSettleTitle(args.settle_title ? String(args.settle_title) : "");
|
||||||
|
|
||||||
// 计算总分并写入历史纪录
|
// 计算总分并写入历史纪录
|
||||||
this.calculateTotalScore(!!args.victory, !!args.all_alive);
|
this.calculateTotalScore(!!args.victory, !!args.all_alive);
|
||||||
|
|
||||||
@@ -715,7 +742,41 @@ export class VictoryComp extends CCComp {
|
|||||||
|
|
||||||
// ======================== 操作入口 ========================
|
// ======================== 操作入口 ========================
|
||||||
|
|
||||||
/** 退出战斗:清理数据 → 触发任务结束 → 关闭弹窗 */
|
/**
|
||||||
|
* 下一节按钮显隐:仅小节胜利(settleType === "section")时显示。
|
||||||
|
* 该按钮为 @property 绑定节点,编辑器中已设计好样式与位置。
|
||||||
|
*/
|
||||||
|
private updateNextButtonState() {
|
||||||
|
if (this.next_section_btn && this.next_section_btn.isValid) {
|
||||||
|
this.next_section_btn.active = this.settleType === "section";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通关标题显隐与文案:仅末关通关时显示("本关通关"/"本章通关")。
|
||||||
|
* 该 Label 为 @property 绑定节点,编辑器中默认隐藏。
|
||||||
|
* @param title 文案,空串时隐藏
|
||||||
|
*/
|
||||||
|
private updateSettleTitle(title: string) {
|
||||||
|
if (this.settle_title && this.settle_title.isValid) {
|
||||||
|
this.settle_title.string = title;
|
||||||
|
this.settle_title.node.active = !!title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下一节按钮点击(编辑器 Click Events 绑定):
|
||||||
|
* 派发 NextSection,由 MissionComp 关窗并推进下一小节。
|
||||||
|
*/
|
||||||
|
on_next_section() {
|
||||||
|
oops.message.dispatchEvent(GameEvent.NextSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主按钮点击(编辑器 Click Events 绑定,btns/next):
|
||||||
|
* 确认结算退出(原有退出流程)。
|
||||||
|
* 注:下一节按钮为独立节点,不经过本方法。
|
||||||
|
*/
|
||||||
victory_end() {
|
victory_end() {
|
||||||
this.clear_data()
|
this.clear_data()
|
||||||
oops.message.dispatchEvent(GameEvent.MissionEnd)
|
oops.message.dispatchEvent(GameEvent.MissionEnd)
|
||||||
|
|||||||
Reference in New Issue
Block a user