feat: 实现无限关卡 Boss 大招系统并重构关卡配置
This commit is contained in:
@@ -1,17 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* @file StageSet.ts
|
* @file StageSet.ts
|
||||||
* @description 关卡系统配置(章节-关-节三级结构)
|
* @description 关卡系统配置(无限关卡·单线推进)
|
||||||
*
|
*
|
||||||
* 设计要点:
|
* 设计要点:
|
||||||
* - 1 章 = 10 关,每关 = 5 小节(一关一局,节间自动推进),共 50 个小节
|
* - 无限关卡:从第 1 关起无上限推进,每关 = 5 回合(一关一局,回合间自动推进)
|
||||||
* - 每关节点固定:1~3 节普通(3 小怪)、第 4 节精英(3 精英)、第 5 节 Boss(1 Boss + 2 小怪)
|
* - 第 1~4 回合为普通怪,属性逐回合轻量增强(SECTION_RAMP_STEP)
|
||||||
* - 怪物等级 = 关卡号(1-1 的怪为 Lv1,1-5 的怪为 Lv5)
|
* - 第 5 回合为单 Boss 回合(无护卫),Boss 采用超厚血量(BOSS_HP_MUL)
|
||||||
* - 怪物属性 = 基础 × 开局系数 × (1 + 成长斜率 × (lv-1)) × 类型倍率
|
* - 怪物等级 = 关卡号(第 N 关的怪为 LvN,随关卡无限成长)
|
||||||
|
* - 小节配置按需动态生成,不再依赖章节静态表
|
||||||
*
|
*
|
||||||
* 数值基准:
|
* 数值基准:
|
||||||
* - 开局系数 0.8(1 级小怪略弱于英雄)
|
* - 开局系数 0.8(1 级小怪略弱于英雄)
|
||||||
* - 成长斜率 +5%/级(10 级 ≈ 1.45 倍)
|
* - 成长斜率 +5%/级(无限线性成长,曲线平缓可控)
|
||||||
* - 精英倍率 × 1.5,Boss 倍率 × 3.0(精英 × 2)
|
* - 回合增强 +12%/回合(第 4 回合 ≈ 1.36 倍)
|
||||||
|
* - Boss:HP × 12(超厚血量)/ AP × 2.0(攻击克制,避免 Boss 秒杀英雄)
|
||||||
|
*
|
||||||
|
* 核心公式:
|
||||||
|
* 怪物属性 = 基础 × MON_START_COEF × (1 + MON_GROW_SLOPE×(lv-1)) × 回合增强 × 类型倍率
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MonType } from "./heroSet";
|
import { MonType } from "./heroSet";
|
||||||
@@ -20,82 +25,54 @@ import { MonType } from "./heroSet";
|
|||||||
|
|
||||||
/** 怪物开局系数:1 级小怪略弱于英雄(0.8 倍) */
|
/** 怪物开局系数:1 级小怪略弱于英雄(0.8 倍) */
|
||||||
export const MON_START_COEF = 0.8;
|
export const MON_START_COEF = 0.8;
|
||||||
/** 怪物成长斜率:每级 +5% 基础属性 */
|
/** 怪物成长斜率:每级 +5% 基础属性(无限线性成长) */
|
||||||
export const MON_GROW_SLOPE = 0.05;
|
export const MON_GROW_SLOPE = 0.05;
|
||||||
/** 精英属性倍率:同级普通怪的 1.5 倍 */
|
/** 回合内轻量增强步长:第 1→4 回合每回合 +12% 属性 */
|
||||||
export const ELITE_MUL = 1.5;
|
export const SECTION_RAMP_STEP = 0.12;
|
||||||
/** Boss 属性倍率:同级普通怪的 3.0 倍(精英 × 2) */
|
/** Boss 血量倍率:同级普通怪的 12 倍(超厚血量,拖长 Boss 战节奏) */
|
||||||
export const BOSS_MUL = 3.0;
|
export const BOSS_HP_MUL = 12;
|
||||||
/** 每关小节总数(一关一局,节间自动推进) */
|
/** Boss 攻击倍率:同级普通怪的 2.0 倍(显著低于血量倍率,避免 Boss 秒杀英雄) */
|
||||||
|
export const BOSS_AP_MUL = 2.0;
|
||||||
|
/** 每关回合总数(一关一局,回合间自动推进) */
|
||||||
export const SECTIONS_PER_STAGE = 5;
|
export const SECTIONS_PER_STAGE = 5;
|
||||||
/** 精英节号(每关第 4 节为精英节) */
|
/** Boss 回合号(每关第 5 回合,即末回合) */
|
||||||
export const ELITE_SECTION = 4;
|
|
||||||
/** Boss 节号(每关第 5 节为 Boss 节,即末节) */
|
|
||||||
export const BOSS_SECTION = 5;
|
export const BOSS_SECTION = 5;
|
||||||
/** 普通节小怪数量(一关一局压缩后单节 3 只) */
|
/** 普通回合小怪数量 */
|
||||||
export const NORMAL_MON_COUNT = 3;
|
export const NORMAL_MON_COUNT = 3;
|
||||||
/** 普通节开局立即刷出数量(3 只全部立即刷出) */
|
/** 普通回合开局立即刷出数量(3 只全部立即刷出) */
|
||||||
export const NORMAL_FIRST_SPAWN = 3;
|
export const NORMAL_FIRST_SPAWN = 3;
|
||||||
/** 普通节后续刷怪间隔(秒):保留配置位(3 只全立即刷,当前无后续批次) */
|
/** 普通回合后续刷怪间隔(秒):保留配置位(3 只全立即刷,当前无后续批次) */
|
||||||
export const NORMAL_SPAWN_INTERVAL = 2;
|
export const NORMAL_SPAWN_INTERVAL = 2;
|
||||||
/** 精英节精英数量 */
|
/** Boss 回合 Boss 延迟刷出时间(秒) */
|
||||||
export const ELITE_COUNT = 3;
|
|
||||||
/** Boss 节护卫小怪数量 */
|
|
||||||
export const BOSS_ELITE_COUNT = 2;
|
|
||||||
/** Boss 节 Boss 延迟刷出时间(秒) */
|
|
||||||
export const BOSS_DELAY = 1;
|
export const BOSS_DELAY = 1;
|
||||||
|
|
||||||
// ======================== 类型定义 ========================
|
// ======================== 类型定义 ========================
|
||||||
|
|
||||||
/** 小节类型 */
|
/** 回合类型 */
|
||||||
export enum SectionType {
|
export enum SectionType {
|
||||||
Normal = 0, // 普通节:3 小怪
|
Normal = 0, // 普通回合:3 小怪(逐回合轻量增强)
|
||||||
Elite = 1, // 精英节:3 精英
|
Boss = 2, // Boss 回合:1 Boss(超厚血量,无护卫)
|
||||||
Boss = 2, // Boss 节:1 Boss + 2 小怪
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 小节配置 */
|
/** 回合配置 */
|
||||||
export interface SectionConfig {
|
export interface SectionConfig {
|
||||||
/** 小节类型 */
|
/** 回合类型 */
|
||||||
type: SectionType;
|
type: SectionType;
|
||||||
/** 怪物等级(驱动线性成长) */
|
/** 怪物等级(= 关卡号,驱动线性成长) */
|
||||||
monLv: number;
|
monLv: number;
|
||||||
/** 精英数量(精英关/Boss 关用) */
|
/** 回合增强系数(第 n 回合 = 1 + SECTION_RAMP_STEP×(n-1),仅普通回合生效) */
|
||||||
eliteCount?: number;
|
ramp: number;
|
||||||
/** Boss UUID(Boss 关用,可选,缺省随机) */
|
/** 怪物类型池(普通回合用,缺省随机) */
|
||||||
bossUuid?: number;
|
|
||||||
/** 怪物类型池(普通关用,缺省随机) */
|
|
||||||
monTypes?: MonType[];
|
monTypes?: MonType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 大关配置(1-1 ~ 1-10),每关 SECTIONS_PER_STAGE 个小节 */
|
|
||||||
export interface StageConfig {
|
|
||||||
/** 章节号 */
|
|
||||||
chapter: number;
|
|
||||||
/** 关卡号 1~10 */
|
|
||||||
stage: number;
|
|
||||||
/** 小节列表(长度 = SECTIONS_PER_STAGE) */
|
|
||||||
sections: SectionConfig[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 章节配置 */
|
|
||||||
export interface ChapterConfig {
|
|
||||||
/** 章节号 */
|
|
||||||
chapter: number;
|
|
||||||
/** 章节名称 */
|
|
||||||
name: string;
|
|
||||||
/** 10 个大关 */
|
|
||||||
stages: StageConfig[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== 工具函数 ========================
|
// ======================== 工具函数 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算怪物最终属性
|
* 计算怪物最终属性(Boss 血/攻倍率分离:超厚血 + 克制攻)
|
||||||
* @param baseHp 基础 HP
|
* @param baseHp 基础 HP
|
||||||
* @param baseAp 基础 AP
|
* @param baseAp 基础 AP
|
||||||
* @param lv 怪物等级
|
* @param lv 怪物等级
|
||||||
* @param isElite 是否精英
|
|
||||||
* @param isBoss 是否 Boss
|
* @param isBoss 是否 Boss
|
||||||
* @returns 最终 HP/AP
|
* @returns 最终 HP/AP
|
||||||
*/
|
*/
|
||||||
@@ -103,159 +80,72 @@ export function calcMonsterAttrs(
|
|||||||
baseHp: number,
|
baseHp: number,
|
||||||
baseAp: number,
|
baseAp: number,
|
||||||
lv: number,
|
lv: number,
|
||||||
isElite: boolean = false,
|
|
||||||
isBoss: boolean = false
|
isBoss: boolean = false
|
||||||
): { hp: number; ap: number } {
|
): { hp: number; ap: number } {
|
||||||
const growCoef = 1 + MON_GROW_SLOPE * (lv - 1);
|
const growCoef = 1 + MON_GROW_SLOPE * (lv - 1);
|
||||||
let typeMul = 1;
|
const hp = Math.max(1, Math.round(baseHp * MON_START_COEF * growCoef * (isBoss ? BOSS_HP_MUL : 1)));
|
||||||
if (isBoss) {
|
const ap = Math.max(1, Math.round(baseAp * MON_START_COEF * growCoef * (isBoss ? BOSS_AP_MUL : 1)));
|
||||||
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 };
|
return { hp, ap };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定关卡的怪物等级
|
* 获取指定关卡的怪物等级(无限关:等级 = 关卡号,无上限)
|
||||||
* @param stage 关卡号(1~10)
|
* @param stage 关卡号(1~∞)
|
||||||
* @returns 怪物等级
|
* @returns 怪物等级
|
||||||
*/
|
*/
|
||||||
export function getMonsterLv(stage: number): number {
|
export function getMonsterLv(stage: number): number {
|
||||||
return Math.max(1, Math.min(10, stage));
|
return Math.max(1, Math.floor(stage));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断是否为精英节(每关第 ELITE_SECTION 节)
|
* 判断是否为 Boss 回合(每关第 BOSS_SECTION 回合,即末回合)
|
||||||
* @param section 小节号(1~SECTIONS_PER_STAGE)
|
* @param section 回合号(1~SECTIONS_PER_STAGE)
|
||||||
*/
|
|
||||||
export function isEliteSection(section: number): boolean {
|
|
||||||
return section === ELITE_SECTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否为 Boss 节(每关第 BOSS_SECTION 节,即末节)
|
|
||||||
* @param section 小节号(1~SECTIONS_PER_STAGE)
|
|
||||||
*/
|
*/
|
||||||
export function isBossSection(section: number): boolean {
|
export function isBossSection(section: number): boolean {
|
||||||
return section === BOSS_SECTION;
|
return section === BOSS_SECTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取小节类型(按节号区分:1~3 普通 / 4 精英 / 5 Boss)
|
* 获取回合类型(按回合号区分:1~4 普通 / 5 Boss)
|
||||||
* @param section 小节号(1~SECTIONS_PER_STAGE)
|
* @param section 回合号(1~SECTIONS_PER_STAGE)
|
||||||
*/
|
*/
|
||||||
export function getSectionType(section: number): SectionType {
|
export function getSectionType(section: number): SectionType {
|
||||||
if (isBossSection(section)) return SectionType.Boss;
|
return isBossSection(section) ? SectionType.Boss : SectionType.Normal;
|
||||||
if (isEliteSection(section)) return SectionType.Elite;
|
|
||||||
return SectionType.Normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 第 1 章配置 ========================
|
|
||||||
|
|
||||||
/** 生成单个大关的 SECTIONS_PER_STAGE 个小节配置(1~3 普通 / 4 精英 / 5 Boss) */
|
|
||||||
function makeStageSections(stage: number): SectionConfig[] {
|
|
||||||
const monLv = getMonsterLv(stage);
|
|
||||||
const sections: SectionConfig[] = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= SECTIONS_PER_STAGE; i++) {
|
|
||||||
const type = getSectionType(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 chapter 章节号(无限关模式已废弃章节概念,参数仅为兼容旧调用方,恒忽略)
|
||||||
* @param stage 关卡号
|
* @param stage 关卡号(1~∞)
|
||||||
* @param section 小节号(1~SECTIONS_PER_STAGE)
|
* @param section 回合号(1~SECTIONS_PER_STAGE)
|
||||||
* @returns 小节配置,不存在时返回 null
|
* @returns 回合配置,回合号越界时返回 null
|
||||||
*/
|
*/
|
||||||
export function getSectionConfig(chapter: number, stage: number, section: number): SectionConfig | null {
|
export function getSectionConfig(chapter: number, stage: number, section: number): SectionConfig | null {
|
||||||
const ch = ChapterSet[chapter];
|
if (section < 1 || section > SECTIONS_PER_STAGE) return null;
|
||||||
if (!ch) return null;
|
|
||||||
const st = ch.stages.find(s => s.stage === stage);
|
const type = getSectionType(section);
|
||||||
if (!st) return null;
|
const cfg: SectionConfig = {
|
||||||
if (section < 1 || section > st.sections.length) return null;
|
type,
|
||||||
return st.sections[section - 1];
|
monLv: getMonsterLv(stage),
|
||||||
|
ramp: 1 + SECTION_RAMP_STEP * (section - 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type !== SectionType.Boss) {
|
||||||
|
// 普通回合:近战为主,穿插远程/重型
|
||||||
|
cfg.monTypes = [MonType.Melee, MonType.Melee, MonType.Melee, MonType.Heavy, MonType.Long];
|
||||||
|
}
|
||||||
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取大关配置
|
* 获取当前进度显示文本(无限关:只展示 关-回合,章节号恒忽略)
|
||||||
* @param chapter 章节号
|
* @param chapter 章节号(兼容参数,恒忽略)
|
||||||
* @param stage 关卡号
|
* @param stage 关卡号
|
||||||
* @returns 大关配置,不存在时返回 null
|
* @param section 回合号
|
||||||
*/
|
* @returns 格式化文本,如 "5-3"
|
||||||
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 {
|
export function getProgressText(chapter: number, stage: number, section: number): string {
|
||||||
return `${chapter}-${stage}-${section}`;
|
return `${stage}-${section}`;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取大关显示文本
|
|
||||||
* @param chapter 章节号
|
|
||||||
* @param stage 关卡号
|
|
||||||
* @returns 格式化文本,如 "1-5"
|
|
||||||
*/
|
|
||||||
export function getStageText(chapter: number, stage: number): string {
|
|
||||||
return `${chapter}-${stage}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,21 @@ export class Monster extends ecs.Entity {
|
|||||||
if (testSkills.fstart !== undefined) model.fstart = testSkills.fstart;
|
if (testSkills.fstart !== undefined) model.fstart = testSkills.fstart;
|
||||||
if (testSkills.call !== undefined) model.call = testSkills.call;
|
if (testSkills.call !== undefined) model.call = testSkills.call;
|
||||||
}
|
}
|
||||||
|
// Boss 大招注入:在保留普攻的基础上追加(多个大招共存,由 SCastSystem 按 CD 轮转施放,
|
||||||
|
// 含镜头聚焦 + 技能名横幅表现,与英雄大招同一条施法链路)
|
||||||
|
const ults = (this as any)._testSkills?.ults;
|
||||||
|
if (ults && ults.length > 0) {
|
||||||
|
for (const ult of ults) {
|
||||||
|
if (!ult || !ult.s_uuid) continue;
|
||||||
|
model.skills[ult.s_uuid] = {
|
||||||
|
uuid: ult.s_uuid,
|
||||||
|
cd: ult.cd ?? 8,
|
||||||
|
lv: 1,
|
||||||
|
ccd: 0,
|
||||||
|
overrides: ult.overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 按怪物等级 resolve 触发技能到运行时缓存(怪物无 lv 成长,恒用 mon_lv)
|
// 按怪物等级 resolve 触发技能到运行时缓存(怪物无 lv 成长,恒用 mon_lv)
|
||||||
model.runtime_call = resolveTriggerByLv(model.call, mon_lv);
|
model.runtime_call = resolveTriggerByLv(model.call, mon_lv);
|
||||||
|
|||||||
@@ -765,23 +765,20 @@ export class MissionComp extends CCComp {
|
|||||||
// 失败:进度不变,结算窗走复活/再来一次/退出流程
|
// 失败:进度不变,结算窗走复活/再来一次/退出流程
|
||||||
this._settleType = "defeat";
|
this._settleType = "defeat";
|
||||||
} else {
|
} else {
|
||||||
// 通关(Boss 节胜利):结算窗显示通关标题,确认退出并存档进下一关
|
// 通关(Boss 回合胜利):无限关卡模式,存档永远推进到下一关第 1 节
|
||||||
|
// (仅写存档,内存进度保留用于本局结算显示)
|
||||||
this._settleType = "final";
|
this._settleType = "final";
|
||||||
this._settleTitle = this.currentStage >= 10 ? "本章通关" : "本关通关";
|
this._settleTitle = "本关通关";
|
||||||
// 存档推进到下一关第 1 节(仅写存档,内存进度保留用于本局结算显示);
|
smc.updateStageProgress({
|
||||||
// 第 10 关通关即本章全通,存档停留等待后续章节扩展
|
chapter: this.currentChapter,
|
||||||
if (this.currentStage < 10) {
|
stage: this.currentStage + 1,
|
||||||
smc.updateStageProgress({
|
section: 1,
|
||||||
chapter: this.currentChapter,
|
});
|
||||||
stage: this.currentStage + 1,
|
if ((smc as any).updateCloudData) {
|
||||||
section: 1,
|
(smc as any).updateCloudData();
|
||||||
});
|
|
||||||
if ((smc as any).updateCloudData) {
|
|
||||||
(smc as any).updateCloudData();
|
|
||||||
}
|
|
||||||
mLogger.log(this.debugMode, 'MissionComp',
|
|
||||||
`[StageProgress] 本关通关,存档推进: ${this.currentChapter}-${this.currentStage + 1}-1`);
|
|
||||||
}
|
}
|
||||||
|
mLogger.log(this.debugMode, 'MissionComp',
|
||||||
|
`[StageProgress] 本关通关,存档推进: ${this.currentStage + 1}-1`);
|
||||||
}
|
}
|
||||||
// 直接切入 BattleEnd,2s 后由 autoNextPhase 自动流转弹窗结算
|
// 直接切入 BattleEnd,2s 后由 autoNextPhase 自动流转弹窗结算
|
||||||
this.changePhase(MissionPhase.BattleEnd);
|
this.changePhase(MissionPhase.BattleEnd);
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* @file RogueConfig.ts
|
* @file RogueConfig.ts
|
||||||
* @description 关卡刷怪系统(章节-关-节三级推进)
|
* @description 无限关卡刷怪系统(关-回合两级推进)
|
||||||
*
|
*
|
||||||
* 游戏机制:
|
* 游戏机制:
|
||||||
* - 玩家线性推进 1 章 10 关 × 每关 5 小节(一关一局,节间自动推进)。
|
* - 玩家单线无限推进:从第 1 关起无上限,每关 5 回合(一关一局,回合间自动推进)。
|
||||||
* - 每关节点固定:1~3 节普通(3 小怪,立即全刷)、第 4 节精英(3 精英同刷)、
|
* - 第 1~4 回合普通怪(3 小怪立即全刷),属性逐回合轻量增强(SECTION_RAMP_STEP)。
|
||||||
* 第 5 节 Boss(2 小怪先刷,Boss 延迟 1s 刷出)。
|
* - 第 5 回合单 Boss 登场(无护卫,延迟 1s 刷出),超厚血量(BOSS_HP_MUL)。
|
||||||
* - 怪物属性 = 基础 × 0.8 × (1 + 0.05×(lv-1)) × 类型倍率(精英1.5 / Boss3.0)。
|
* - Boss 加载时按攻击定位(近战/中程/远程)随机取型,并随机携带大招:
|
||||||
|
* 前期 1 个,随关卡解锁扩展至多个(BOSS_ULT_UNLOCK_EVERY / BOSS_ULT_MAX_COUNT)。
|
||||||
*
|
*
|
||||||
* 设计层次:
|
* 设计层次:
|
||||||
* 1. StageSet - 关卡配置(章节/关/节 + 数值公式)
|
* 1. StageSet - 关卡数值公式(等级/成长/回合增强/Boss 血攻倍率)
|
||||||
* 2. MonSkillPool/BossSkillPool - 怪物技能池(atking / atked / dead 等全触发类型)
|
* 2. BossPools - Boss 池(按攻击定位分组,动态从 HeroInfo 提取)
|
||||||
* 3. StageSpawningEngine - 生成引擎(按小节配置生成怪物列表)
|
* 3. BossUltPools - Boss 大招池(按定位分组,引用 SkillSet 6100~6204 群体大招段)
|
||||||
|
* 4. StageSpawningEngine - 生成引擎(按回合配置生成怪物列表)
|
||||||
*
|
*
|
||||||
* 核心公式:
|
* 核心公式:
|
||||||
* 最终属性 = HeroInfo 基础属性 × MON_START_COEF × (1 + MON_GROW_SLOPE×(lv-1)) × 类型倍率
|
* 小怪属性 = HeroInfo 基础属性 × MON_START_COEF × (1 + MON_GROW_SLOPE×(lv-1)) × 回合增强
|
||||||
* 1 级小怪 ≈ 英雄 × 0.8,10 级小怪 ≈ 英雄 × 1.16,保持平缓成长。
|
* Boss 属性 = 同上 × BOSS_HP_MUL(血)/ BOSS_AP_MUL(攻),等级 = 关卡号无限成长
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HeroInfo, MonType, MonTypeName, TriggerGrouped } from "../common/config/heroSet";
|
import { HeroInfo, HType, MonType, TriggerGrouped, UltCdLv, UltCdSet } from "../common/config/heroSet";
|
||||||
import { SkillOverrides } from "../common/config/SkillSet";
|
import { SkillOverrides, SkillSet } from "../common/config/SkillSet";
|
||||||
import { FacSet } from "../common/config/GameSet";
|
import { FacSet } from "../common/config/GameSet";
|
||||||
import {
|
import {
|
||||||
SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs,
|
SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs,
|
||||||
NORMAL_MON_COUNT, NORMAL_FIRST_SPAWN, ELITE_COUNT, BOSS_ELITE_COUNT, BOSS_DELAY,
|
NORMAL_MON_COUNT, NORMAL_FIRST_SPAWN, BOSS_DELAY,
|
||||||
MON_START_COEF, MON_GROW_SLOPE, ELITE_MUL, BOSS_MUL,
|
MON_START_COEF,
|
||||||
} from "../common/config/StageSet";
|
} from "../common/config/StageSet";
|
||||||
|
|
||||||
// ======================== 1. 战斗常量 ========================
|
// ======================== 1. 战斗常量 ========================
|
||||||
@@ -79,126 +81,114 @@ 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. 怪物技能池 ========================
|
// ======================== 3. Boss 池(按攻击定位分组) ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 怪物技能套装(一套完整的触发技能配置)
|
* Boss 池:按攻击定位(近战/中程/远程)分组,动态从 HeroInfo 提取。
|
||||||
* 覆盖触发时机:call / atking / atked / dead / fstart / revive
|
* 加载时随机一个定位 → 从对应池随机 Boss;定位池为空时自动回退(如暂无中程 Boss 配置)。
|
||||||
*/
|
*/
|
||||||
export interface MonSkillSet {
|
export const BossPools: Record<HType, number[]> = {
|
||||||
id: string;
|
[HType.Melee]: [],
|
||||||
|
[HType.Mid]: [],
|
||||||
|
[HType.Long]: [],
|
||||||
|
};
|
||||||
|
for (const key in HeroInfo) {
|
||||||
|
const info = HeroInfo[key];
|
||||||
|
if (info.fac !== FacSet.MON) continue;
|
||||||
|
if (info.monType !== MonType.MeleeBoss && info.monType !== MonType.LongBoss) continue;
|
||||||
|
const pool = BossPools[info.type as HType];
|
||||||
|
if (pool) pool.push(info.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== 4. Boss 大招池 ========================
|
||||||
|
|
||||||
|
/** 每解锁 1 个额外大招收紧的关卡间隔(每 5 关 +1 个大招位) */
|
||||||
|
export const BOSS_ULT_UNLOCK_EVERY = 5;
|
||||||
|
/** Boss 同时携带大招数量上限 */
|
||||||
|
export const BOSS_ULT_MAX_COUNT = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定关卡的 Boss 携带大招数量(1 个起步,随关卡解锁扩展)
|
||||||
|
* @param stage 关卡号(1~∞)
|
||||||
|
* @returns 大招数量(1~BOSS_ULT_MAX_COUNT)
|
||||||
|
*/
|
||||||
|
export function getBossUltCount(stage: number): number {
|
||||||
|
const st = Math.max(1, Math.floor(stage));
|
||||||
|
return Math.min(BOSS_ULT_MAX_COUNT, 1 + Math.floor((st - 1) / BOSS_ULT_UNLOCK_EVERY));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Boss 大招条目(s_uuid 引用 SkillSet 6100~6204 群体大招段,按定位分组) */
|
||||||
|
export interface BossUltEntry {
|
||||||
|
s_uuid: number;
|
||||||
name: string;
|
name: string;
|
||||||
/** 普攻技能覆盖(可选,不填则使用怪物默认普攻) */
|
/** 抽取权重 */
|
||||||
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
|
|
||||||
/** 召唤触发 */
|
|
||||||
call?: TriggerGrouped;
|
|
||||||
/** 攻击触发 */
|
|
||||||
atking?: TriggerGrouped;
|
|
||||||
/** 受击触发 */
|
|
||||||
atked?: TriggerGrouped;
|
|
||||||
/** 死亡触发 */
|
|
||||||
dead?: TriggerGrouped;
|
|
||||||
/** 战斗开始触发 */
|
|
||||||
fstart?: TriggerGrouped;
|
|
||||||
/** 复活触发 */
|
|
||||||
revive?: TriggerGrouped;
|
|
||||||
/** 在技能池中的抽取权重 */
|
|
||||||
weight: number;
|
weight: number;
|
||||||
|
/** 技能参数覆盖(伤害系数等) */
|
||||||
|
overrides?: SkillOverrides;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 怪物技能池(硬编码)
|
* Boss 大招池(按攻击定位分组)
|
||||||
* 技能 uuid 引用 SkillSet 中的 6000~6500 段触发技能。
|
* 大招注入 Boss 的 skills 表后由 SCastSystem 按 CD 自动施放(含镜头聚焦 + 技能名横幅)。
|
||||||
*/
|
*/
|
||||||
export const MonSkillPool: Record<string, MonSkillSet> = {
|
export const BossUltPools: Record<HType, BossUltEntry[]> = {
|
||||||
/** 狂暴:攻击触发自身攻击提升 */
|
// 近战系:战士群体斩击(6100~6104 段)
|
||||||
berserk: {
|
[HType.Melee]: [
|
||||||
id: "berserk", name: "狂暴", weight: 10,
|
{ s_uuid: 6100, name: "寒冰斩", weight: 10, overrides: { ap: 180 } },
|
||||||
atking: {
|
{ s_uuid: 6101, name: "裂气斩", weight: 10, overrides: { ap: 180 } },
|
||||||
6401: [{ lv: 1, t_num: 5, overrides: { ap: 1 } }],
|
{ s_uuid: 6102, name: "火焰斩", weight: 10, overrides: { ap: 180 } },
|
||||||
},
|
{ s_uuid: 6103, name: "闪电斩", weight: 8, overrides: { ap: 180 } },
|
||||||
},
|
{ s_uuid: 6104, name: "开天斩", weight: 6, overrides: { ap: 220 } },
|
||||||
/** 坚韧:受击获得护盾 */
|
],
|
||||||
tough: {
|
// 中程系:射手箭雨 + 混合法术(6150~6151 / 6203~6204 段)
|
||||||
id: "tough", name: "坚韧", weight: 8,
|
[HType.Mid]: [
|
||||||
atked: {
|
{ s_uuid: 6150, name: "万箭齐发", weight: 10, overrides: { ap: 150 } },
|
||||||
6301: [{ lv: 1, t_num: 3, overrides: { ap: 2 } }],
|
{ s_uuid: 6151, name: "火箭齐发", weight: 10, overrides: { ap: 150 } },
|
||||||
},
|
{ s_uuid: 6203, name: "能量波", weight: 8, overrides: { ap: 150 } },
|
||||||
},
|
{ s_uuid: 6204, name: "雷暴", weight: 8, overrides: { ap: 150 } },
|
||||||
/** 遗志:死亡时全队攻击提升 */
|
],
|
||||||
legacy: {
|
// 远程系:法师群体法术(6200~6204 段)
|
||||||
id: "legacy", name: "遗志", weight: 6,
|
[HType.Long]: [
|
||||||
dead: {
|
{ s_uuid: 6200, name: "火球术", weight: 10, overrides: { ap: 160 } },
|
||||||
6401: [{ lv: 1, t_num: 1, overrides: { ap: 3 } }],
|
{ s_uuid: 6201, name: "飓风", weight: 10, overrides: { ap: 140 } },
|
||||||
},
|
{ s_uuid: 6202, name: "毒气弹", weight: 8, overrides: { ap: 150 } },
|
||||||
},
|
{ s_uuid: 6203, name: "能量波", weight: 8, overrides: { ap: 150 } },
|
||||||
/** 战吼:战斗开始时全队攻击提升 */
|
{ s_uuid: 6204, name: "雷暴", weight: 8, overrides: { ap: 150 } },
|
||||||
warcry: {
|
],
|
||||||
id: "warcry", name: "战吼", weight: 5,
|
|
||||||
fstart: {
|
|
||||||
6401: [{ lv: 1, t_num: 1, overrides: { ap: 2 } }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/** 吸血:攻击恢复生命 */
|
|
||||||
leech: {
|
|
||||||
id: "leech", name: "吸血", weight: 7,
|
|
||||||
atking: {
|
|
||||||
6302: [{ lv: 1, t_num: 4, overrides: { ap: 150 } }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Boss 专属技能池 */
|
// ======================== 5. 配置校验 ========================
|
||||||
export const BossSkillPool: Record<string, MonSkillSet> = {
|
|
||||||
/** 狂暴领主:攻击触发全队攻击提升 */
|
|
||||||
boss_rage: {
|
|
||||||
id: "boss_rage", name: "狂暴领主", weight: 10,
|
|
||||||
atking: {
|
|
||||||
6401: [{ lv: 1, t_num: 3, overrides: { ap: 3 } }],
|
|
||||||
},
|
|
||||||
dead: {
|
|
||||||
6401: [{ lv: 1, t_num: 1, overrides: { ap: 5 } }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/** 铁壁领主:受击获得高额护盾 */
|
|
||||||
boss_iron: {
|
|
||||||
id: "boss_iron", name: "铁壁领主", weight: 8,
|
|
||||||
atked: {
|
|
||||||
6301: [{ lv: 1, t_num: 2, overrides: { ap: 5 } }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/** 毁灭领主:战斗开始时全队攻击大幅提升 */
|
|
||||||
boss_doom: {
|
|
||||||
id: "boss_doom", name: "毁灭领主", weight: 6,
|
|
||||||
fstart: {
|
|
||||||
6401: [{ lv: 1, t_num: 1, overrides: { ap: 8 } }],
|
|
||||||
},
|
|
||||||
atking: {
|
|
||||||
6401: [{ lv: 1, t_num: 5, overrides: { ap: 2 } }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ======================== 4. 配置校验 ========================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验 MonSkillPool / BossSkillPool 引用一致性
|
* 校验 Boss 池 / Boss 大招池引用一致性
|
||||||
* 建议在游戏启动时调用一次,便于发现配置错误
|
* 建议在游戏启动时调用一次,便于发现配置错误
|
||||||
* @returns 错误信息数组,空数组表示校验通过
|
* @returns 错误信息数组,空数组表示校验通过
|
||||||
*/
|
*/
|
||||||
export function validateRogueConfig(): string[] {
|
export function validateRogueConfig(): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// 校验 Boss 类型在 MonList 中有怪
|
// 校验至少一个定位有可用 Boss
|
||||||
if ((!MonList[MonType.MeleeBoss] || MonList[MonType.MeleeBoss].length === 0)
|
const hasBoss = (Object.keys(BossPools) as unknown as HType[]).some(t => BossPools[t].length > 0);
|
||||||
&& (!MonList[MonType.LongBoss] || MonList[MonType.LongBoss].length === 0)) {
|
if (!hasBoss) {
|
||||||
errors.push("MonList 中无任何 Boss 类型(MeleeBoss/LongBoss)可用 uuid");
|
errors.push("BossPools 中无任何可用 Boss uuid(MeleeBoss/LongBoss 均缺失)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验大招池条目均存在于 SkillSet
|
||||||
|
for (const key in BossUltPools) {
|
||||||
|
const pool = BossPools[key as unknown as HType];
|
||||||
|
const ults = BossUltPools[key as unknown as HType];
|
||||||
|
if (!pool || pool.length === 0) continue; // 空定位池跳过(大招池无消费方)
|
||||||
|
for (const ult of ults) {
|
||||||
|
if (!SkillSet[ult.s_uuid]) {
|
||||||
|
errors.push(`BossUltPools[${key}] 大招 ${ult.s_uuid}(${ult.name}) 未在 SkillSet 中定义`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 5. 生成结果接口 ========================
|
// ======================== 6. 生成结果接口 ========================
|
||||||
|
|
||||||
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
|
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
|
||||||
export interface GeneratedMonster {
|
export interface GeneratedMonster {
|
||||||
@@ -207,10 +197,8 @@ export interface GeneratedMonster {
|
|||||||
hp: number;
|
hp: number;
|
||||||
ap: number;
|
ap: number;
|
||||||
isBoss: boolean;
|
isBoss: boolean;
|
||||||
/** 是否为精英(精英关/Boss 关的精英) */
|
/** 是否为精英(对象池复用实体需显式重置的标记位) */
|
||||||
isElite?: boolean;
|
isElite?: boolean;
|
||||||
/** 是否为 Boss 护卫队成员(供 UI/统计识别) */
|
|
||||||
isBossGuard?: boolean;
|
|
||||||
/** 怪物等级(驱动技能等级增量,Mon.load 第 6 参) */
|
/** 怪物等级(驱动技能等级增量,Mon.load 第 6 参) */
|
||||||
lv: number;
|
lv: number;
|
||||||
spawnIndex: number;
|
spawnIndex: number;
|
||||||
@@ -220,11 +208,15 @@ export interface GeneratedMonster {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 怪物技能套装(覆盖全部触发时机)
|
* 怪物技能套装(技能注入通道,与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp)
|
||||||
* 注入方式与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp
|
* - skill:替换普攻(会清空原技能表)
|
||||||
|
* - ults:追加 Boss 大招(保留普攻,多个大招共存,SCastSystem 按 CD 轮转施放)
|
||||||
|
* - 其余为触发技能组覆盖
|
||||||
*/
|
*/
|
||||||
skills?: {
|
skills?: {
|
||||||
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
|
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
|
||||||
|
/** Boss 大招列表(追加注入,不替换普攻) */
|
||||||
|
ults?: { s_uuid: number; cd?: number; overrides?: SkillOverrides }[];
|
||||||
call?: TriggerGrouped;
|
call?: TriggerGrouped;
|
||||||
atking?: TriggerGrouped;
|
atking?: TriggerGrouped;
|
||||||
atked?: TriggerGrouped;
|
atked?: TriggerGrouped;
|
||||||
@@ -234,7 +226,7 @@ export interface GeneratedMonster {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 6. 测试模式配置 ========================
|
// ======================== 7. 测试模式配置 ========================
|
||||||
|
|
||||||
export const TestModeConfig = {
|
export const TestModeConfig = {
|
||||||
enable: false, // 默认关闭测试模式
|
enable: false, // 默认关闭测试模式
|
||||||
@@ -245,25 +237,27 @@ export const TestModeConfig = {
|
|||||||
spawnCount: 1,
|
spawnCount: 1,
|
||||||
|
|
||||||
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
|
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
|
||||||
|
/** 测试模式注入的大招列表(追加,不替换普攻) */
|
||||||
|
ults: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides }[] | undefined,
|
||||||
atking: undefined as TriggerGrouped | undefined,
|
atking: undefined as TriggerGrouped | undefined,
|
||||||
atked: undefined as TriggerGrouped | undefined,
|
atked: undefined as TriggerGrouped | undefined,
|
||||||
dead: undefined as TriggerGrouped | undefined,
|
dead: undefined as TriggerGrouped | undefined,
|
||||||
fstart: undefined as TriggerGrouped | undefined,
|
fstart: undefined as TriggerGrouped | undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======================== 7. 生成引擎 ========================
|
// ======================== 8. 生成引擎 ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关卡刷怪生成引擎
|
* 无限关卡刷怪生成引擎
|
||||||
* 按章节-关-节配置生成怪物列表,属性走 StageSet 数值公式
|
* 按关-回合配置生成怪物列表,属性走 StageSet 数值公式
|
||||||
*/
|
*/
|
||||||
export class StageSpawningEngine {
|
export class StageSpawningEngine {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成指定小节的怪物列表
|
* 生成指定回合的怪物列表
|
||||||
* @param chapter 章节号
|
* @param chapter 章节号(无限关模式已废弃章节概念,兼容参数恒忽略)
|
||||||
* @param stage 关卡号
|
* @param stage 关卡号(1~∞)
|
||||||
* @param section 小节号(1~10)
|
* @param section 回合号(1~SECTIONS_PER_STAGE)
|
||||||
* @returns 怪物列表(按刷出顺序排列)
|
* @returns 怪物列表(按刷出顺序排列)
|
||||||
*/
|
*/
|
||||||
generateSectionMonsters(chapter: number, stage: number, section: number): GeneratedMonster[] {
|
generateSectionMonsters(chapter: number, stage: number, section: number): GeneratedMonster[] {
|
||||||
@@ -278,15 +272,9 @@ export class StageSpawningEngine {
|
|||||||
return this.makeFallbackMonsters();
|
return this.makeFallbackMonsters();
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (cfg.type) {
|
return cfg.type === SectionType.Boss
|
||||||
case SectionType.Elite:
|
? this.generateBossSection(cfg, stage)
|
||||||
return this.generateEliteSection(cfg);
|
: this.generateNormalSection(cfg);
|
||||||
case SectionType.Boss:
|
|
||||||
return this.generateBossSection(cfg);
|
|
||||||
case SectionType.Normal:
|
|
||||||
default:
|
|
||||||
return this.generateNormalSection(cfg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置引擎状态(每局开始时调用,预留) */
|
/** 重置引擎状态(每局开始时调用,预留) */
|
||||||
@@ -294,7 +282,8 @@ export class StageSpawningEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 普通节:NORMAL_MON_COUNT 只小怪全部立即刷出(batch=0)
|
* 普通回合(1~4):NORMAL_MON_COUNT 只小怪全部立即刷出(batch=0),
|
||||||
|
* 属性乘回合增强系数(cfg.ramp)实现逐回合轻量增强
|
||||||
*/
|
*/
|
||||||
private generateNormalSection(cfg: SectionConfig): GeneratedMonster[] {
|
private generateNormalSection(cfg: SectionConfig): GeneratedMonster[] {
|
||||||
const monsters: GeneratedMonster[] = [];
|
const monsters: GeneratedMonster[] = [];
|
||||||
@@ -304,64 +293,49 @@ export class StageSpawningEngine {
|
|||||||
const type = types[i % types.length];
|
const type = types[i % types.length];
|
||||||
const mon = this.makeMonster(type, cfg.monLv, i);
|
const mon = this.makeMonster(type, cfg.monLv, i);
|
||||||
mon.batch = i < NORMAL_FIRST_SPAWN ? 0 : 1;
|
mon.batch = i < NORMAL_FIRST_SPAWN ? 0 : 1;
|
||||||
|
// 回合内轻量增强:第 n 回合 × (1 + SECTION_RAMP_STEP×(n-1))
|
||||||
|
mon.hp = Math.max(1, Math.round(mon.hp * cfg.ramp));
|
||||||
|
mon.ap = Math.max(1, Math.round(mon.ap * cfg.ramp));
|
||||||
monsters.push(mon);
|
monsters.push(mon);
|
||||||
}
|
}
|
||||||
return monsters;
|
return monsters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 精英节:3 只精英同时刷出
|
* Boss 回合(5):单 Boss 登场(无护卫)
|
||||||
|
* Boss 延迟 BOSS_DELAY 秒刷出,属性由 makeBoss 按血/攻分离倍率计算(超厚血量)
|
||||||
*/
|
*/
|
||||||
private generateEliteSection(cfg: SectionConfig): GeneratedMonster[] {
|
private generateBossSection(cfg: SectionConfig, stage: number): GeneratedMonster[] {
|
||||||
const monsters: GeneratedMonster[] = [];
|
const boss = this.makeBoss(cfg.monLv, stage, 0);
|
||||||
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.batch = 0;
|
||||||
boss.delay = BOSS_DELAY;
|
boss.delay = BOSS_DELAY;
|
||||||
// Boss 属性 = 普通 × BOSS_MUL
|
return [boss];
|
||||||
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;
|
/** 生成普通怪物(属性按 StageSet 公式计算) */
|
||||||
|
private makeMonster(type: MonType, monLv: number, spawnIndex: number): GeneratedMonster {
|
||||||
|
let uuids = MonList[type];
|
||||||
|
if (!uuids || uuids.length === 0) {
|
||||||
|
// 兜底 Melee
|
||||||
|
uuids = MonList[MonType.Melee] || [6001];
|
||||||
|
}
|
||||||
|
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
|
||||||
|
|
||||||
|
const baseInfo = HeroInfo[uuid];
|
||||||
|
const baseHp = baseInfo ? baseInfo.hp : 100;
|
||||||
|
const baseAp = baseInfo ? baseInfo.ap : 10;
|
||||||
|
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uuid,
|
||||||
|
type,
|
||||||
|
hp: attrs.hp,
|
||||||
|
ap: attrs.ap,
|
||||||
|
isBoss: false,
|
||||||
|
lv: monLv,
|
||||||
|
spawnIndex,
|
||||||
|
batch: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 配置缺失兜底:3 只 Lv1 近战小怪 */
|
/** 配置缺失兜底:3 只 Lv1 近战小怪 */
|
||||||
@@ -399,6 +373,7 @@ export class StageSpawningEngine {
|
|||||||
batch: 0,
|
batch: 0,
|
||||||
skills: {
|
skills: {
|
||||||
skill: TestModeConfig.skill,
|
skill: TestModeConfig.skill,
|
||||||
|
ults: TestModeConfig.ults,
|
||||||
atking: TestModeConfig.atking,
|
atking: TestModeConfig.atking,
|
||||||
atked: TestModeConfig.atked,
|
atked: TestModeConfig.atked,
|
||||||
dead: TestModeConfig.dead,
|
dead: TestModeConfig.dead,
|
||||||
@@ -409,77 +384,74 @@ export class StageSpawningEngine {
|
|||||||
return monsters;
|
return monsters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成 Boss(基础面板按 StageSet 公式计算) */
|
/**
|
||||||
private makeBoss(monLv: number, spawnIndex: number): GeneratedMonster {
|
* 生成 Boss:加载时随机攻击定位 → 从对应池随机 Boss 个体 → 随机携带大招
|
||||||
const isMeleeBoss = Math.random() < 0.5;
|
* 属性走 calcMonsterAttrs(isBoss=true):HP × BOSS_HP_MUL(超厚血)/ AP × BOSS_AP_MUL
|
||||||
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
*/
|
||||||
|
private makeBoss(monLv: number, stage: number, spawnIndex: number): GeneratedMonster {
|
||||||
|
const range = this.pickBossRange();
|
||||||
|
const uuid = this.pickBossUuid(range);
|
||||||
|
|
||||||
// Boss 类型兜底:若 MonList 缺失则退回 MeleeBoss
|
|
||||||
let uuids = MonList[type];
|
|
||||||
if (!uuids || uuids.length === 0) {
|
|
||||||
uuids = MonList[MonType.MeleeBoss] || MonList[MonType.Melee] || [6001];
|
|
||||||
}
|
|
||||||
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
|
|
||||||
|
|
||||||
// Boss 基础面板取 HeroInfo 基础值,属性由调用方乘 BOSS_MUL
|
|
||||||
const baseInfo = HeroInfo[uuid];
|
const baseInfo = HeroInfo[uuid];
|
||||||
const baseHp = baseInfo ? baseInfo.hp : 600;
|
const baseHp = baseInfo ? baseInfo.hp : 600;
|
||||||
const baseAp = baseInfo ? baseInfo.ap : 30;
|
const baseAp = baseInfo ? baseInfo.ap : 30;
|
||||||
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false, false);
|
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uuid,
|
uuid,
|
||||||
type,
|
type: baseInfo?.monType ?? MonType.MeleeBoss,
|
||||||
hp: attrs.hp,
|
hp: attrs.hp,
|
||||||
ap: attrs.ap,
|
ap: attrs.ap,
|
||||||
isBoss: true,
|
isBoss: true,
|
||||||
lv: monLv,
|
lv: monLv,
|
||||||
spawnIndex,
|
spawnIndex,
|
||||||
batch: 0,
|
batch: 0,
|
||||||
|
skills: {
|
||||||
|
ults: this.pickBossUlts(range, stage),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成普通怪物(属性按 StageSet 公式计算) */
|
/**
|
||||||
private makeMonster(type: MonType, monLv: number, spawnIndex: number): GeneratedMonster {
|
* 随机 Boss 攻击定位(近战/中程/远程)
|
||||||
let uuids = MonList[type];
|
* 空池自动回退:中程暂无 Boss 配置时落到远程/近战,后续补配即自动生效
|
||||||
|
*/
|
||||||
|
private pickBossRange(): HType {
|
||||||
|
const all: HType[] = [HType.Melee, HType.Mid, HType.Long];
|
||||||
|
const first = all[Math.floor(Math.random() * all.length)];
|
||||||
|
if (BossPools[first].length > 0) return first;
|
||||||
|
// 回退链:随机定位空池时按剩余定位找非空池
|
||||||
|
const fallback = all.find(t => BossPools[t].length > 0);
|
||||||
|
return fallback ?? HType.Melee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从指定定位池随机取 Boss uuid(含全局兜底) */
|
||||||
|
private pickBossUuid(range: HType): number {
|
||||||
|
let uuids = BossPools[range];
|
||||||
if (!uuids || uuids.length === 0) {
|
if (!uuids || uuids.length === 0) {
|
||||||
// 兜底 Melee
|
uuids = MonList[MonType.MeleeBoss] || MonList[MonType.Melee] || [6001];
|
||||||
uuids = MonList[MonType.Melee] || [6001];
|
|
||||||
}
|
}
|
||||||
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
|
return uuids[Math.floor(Math.random() * uuids.length)];
|
||||||
|
|
||||||
const baseInfo = HeroInfo[uuid];
|
|
||||||
const baseHp = baseInfo ? baseInfo.hp : 100;
|
|
||||||
const baseAp = baseInfo ? baseInfo.ap : 10;
|
|
||||||
const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false, false);
|
|
||||||
|
|
||||||
return {
|
|
||||||
uuid,
|
|
||||||
type,
|
|
||||||
hp: attrs.hp,
|
|
||||||
ap: attrs.ap,
|
|
||||||
isBoss: false,
|
|
||||||
lv: monLv,
|
|
||||||
spawnIndex,
|
|
||||||
batch: 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从技能池中按权重抽取一套技能 */
|
/**
|
||||||
private pickSkillSet(pool: string[], library: Record<string, MonSkillSet>): GeneratedMonster["skills"] | undefined {
|
* 按定位抽取 Boss 大招(不重复),数量由关卡进度决定(getBossUltCount)
|
||||||
const valid = pool.map(id => library[id]).filter(s => s);
|
* @returns 大招配置列表;池空时返回 undefined
|
||||||
if (valid.length === 0) return undefined;
|
*/
|
||||||
const picked = this.pickWeighted(valid);
|
private pickBossUlts(range: HType, stage: number): GeneratedMonster["skills"]["ults"] {
|
||||||
if (!picked) return undefined;
|
const pool = [...(BossUltPools[range] ?? [])];
|
||||||
return {
|
const count = Math.min(getBossUltCount(stage), pool.length);
|
||||||
skill: picked.skill,
|
if (count <= 0) return undefined;
|
||||||
call: picked.call,
|
|
||||||
atking: picked.atking,
|
const ults: { s_uuid: number; cd?: number; overrides?: SkillOverrides }[] = [];
|
||||||
atked: picked.atked,
|
for (let i = 0; i < count; i++) {
|
||||||
dead: picked.dead,
|
const picked = this.pickWeighted(pool);
|
||||||
fstart: picked.fstart,
|
if (!picked) break;
|
||||||
revive: picked.revive,
|
pool.splice(pool.indexOf(picked), 1);
|
||||||
};
|
// 大招 CD 取英雄同档标准档(8s),与英雄大招节奏对齐
|
||||||
|
ults.push({ s_uuid: picked.s_uuid, cd: UltCdSet[UltCdLv.Normal].cd, overrides: picked.overrides });
|
||||||
|
}
|
||||||
|
return ults.length > 0 ? ults : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用加权随机抽取 */
|
/** 通用加权随机抽取 */
|
||||||
@@ -496,18 +468,6 @@ export class StageSpawningEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== 8. 全局单例 ========================
|
// ======================== 9. 全局单例 ========================
|
||||||
|
|
||||||
export const spawningEngine = new StageSpawningEngine();
|
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();
|
|
||||||
|
|||||||
Reference in New Issue
Block a user