refactor: 重构关卡战斗流程与AI逻辑,改为单场Boss战模式

1.  近战AI新增警戒线机制,无威胁时退回编队
2.  替换多回合波次为单场Boss战,简化刷怪与回合逻辑
3.  重构招募系统为开局免费三选一流程
4.  调整英雄与怪物基础属性平衡
5.  精简HUD与配置文件冗余代码
This commit is contained in:
panFD
2026-08-13 00:21:54 +08:00
parent a905db8e48
commit c27c81cfa6
9 changed files with 1513 additions and 2202 deletions

View File

@@ -1,203 +1,59 @@
/**
* @file RogueConfig.ts
* @description 肉鸽刷怪系统(基于英雄强度的动态难度 + 心流循环
* @description Boss 组合刷怪系统(单场 Boss 战定胜负
*
* 游戏机制:
* - 整局只有一场战斗:开局英雄三次三选一后自动开战。
* - 怪物从 BossComboLibrary 按权重随机抽一组1 Boss + 4 小怪),
* 战斗开始瞬间一次全量登场X∈[100,320] 均匀铺开),双方立即互相攻击。
* - 怪物全灭 → 胜利结算;英雄全灭 → 失败结算;无回合递进、无分批刷怪。
*
* 设计层次:
* 1. WaveType - 回合类型(普通 / 压力 / 放松5 回合一个心流循环
* 2. WaveConfig - 每回合硬编码(数量 + 小队池 + HP/AP 倍率 + 强度微调
* 3. DynamicTuner - 动态难度调节器(系统根据战况放水 / 加压
* 4. MonSkillSet - 怪物技能池atking / atked / dead 等全触发类型)
* 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
* 1. BossComboLibrary - Boss 组合库(小怪构成 + Boss/小怪技能池 + 抽取权重)
* 2. MonSkillPool/BossSkillPool - 怪物技能池atking / atked / dead 等全触发类型
* 3. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度
*
* 核心公式:
* heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄)
* targetPower = heroPower × 回合类型系数 × wave.power_adjust × DynamicTuner.factor
* hpScale = targetPower × wave.hp_mul ÷ Σ 怪物基础强度
* targetPower = heroPower × BOSS_TYPE_RATIO × FINAL_HP_MUL × MON_POWER_MUL
* hpScale = targetPower ÷ Σ 怪物基础强度
* 怪物 hp = base_hp × hpScale (走总强度框架,自动匹配玩家)
*
* 怪物 ap 独立挂钩玩家输出(不走总强度,避免被 hp 挤占导致"涨不动"
* 怪物 ap = 玩家平均攻击 × getApWaveGrowth(wave) × AP_RELATIVE × 类型系数
* 回合递进取线性 1.0 → 3.0wave 1 → 20
* 说明ap_mul 字段已废弃(保留字段仅为兼容配置表,不再参与计算)
*
* 回合节奏:
* - 最大 20 回合,第 20 回合通关
* - 每回合 30 秒,固定分 3 批,每 10 秒释放一批
* - 普通回合 36~72 只,放松回合 × 1.5 = 54~108 只(全局 MON_COUNT_MUL = 2
* - wave % 5 === 0 → 压力回合(必带 Boss强度高、数量少
* - wave % 5 === 4 → 放松回合(数量 × 1.5,强度低,爽快清屏,大战前夜收割补给)
* 怪物 ap 独立挂钩玩家输出(不走总强度,避免被 hp 挤占):
* 怪物 ap = 玩家平均攻击 × AP_GROWTH_FINAL × AP_RELATIVE × 类型系数
*/
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry, heroInfo } from "../common/config/heroSet";
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry } from "../common/config/heroSet";
import { SkillOverrides } from "../common/config/SkillSet";
import { FacSet } from "../common/config/GameSet";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
// ======================== 1. 回合类型与节奏常量 ========================
// ======================== 1. 战斗常量 ========================
/** 回合类型枚举 */
export enum WaveType {
Normal = 0, // 普通回合
Pressure = 1, // 压力回合wave % 5 === 0必带 Boss
Relax = 2, // 放松回合wave % 5 === 4量大强度低大战前夜的收割补给
}
/** 回合类型名称 */
export const WaveTypeName: Record<WaveType, string> = {
[WaveType.Normal]: "普通",
[WaveType.Pressure]: "压力",
[WaveType.Relax]: "放松",
};
/** 回合类型强度系数(硬编码) */
export const WAVE_TYPE_POWER_RATIO: Record<WaveType, number> = {
[WaveType.Normal]: 0.9, // 普通回合:标准强度
[WaveType.Pressure]: 1.2, // 压力回合:强度高、数量少
[WaveType.Relax]: 0.6, // 放松回合:量大、强度低
};
/** 放松回合数量倍率(相对普通回合) */
export const RELAX_COUNT_MUL = 1.5;
/** 全局怪物数量倍率(在 base_count 基础上整体翻倍,量大管饱) */
export const MON_COUNT_MUL = 2;
/** 全局怪物强度系数2/3 = 强度削弱三分之一,同时作用于 hp 缩放与 ap 挂钩公式) */
export const MON_POWER_MUL = 2 / 3;
/** 最大回合数(第 20 回合通关) */
export const MAX_WAVE = 20;
/** 每回合时长(秒) */
/** 单场战斗时长基准供清屏速度评价BattleBannerComp 清屏奖励)等使用 */
export const WAVE_DURATION = 30;
/** 每回合分批次数 */
export const BATCH_COUNT = 3;
/** 每批间隔30 秒 / 3 批 = 10 秒 */
export const BATCH_INTERVAL = WAVE_DURATION / BATCH_COUNT;
/** 每回合怪物硬上限(放松回合 72 × 1.5 = 108 */
export const MAX_MONSTERS = 108;
/** Boss 护卫队数量(与 Boss 同批压轴进场,占用回合总名额) */
export const BOSS_GUARD_COUNT = 3;
/** 批次怪物数量占比(铺垫 → 加压 → 高潮,第三批另有收尾小队加压) */
export const BATCH_RATIO: number[] = [0.25, 0.35, 0.40];
/** 收尾高潮批额外补入的最强小队数量(非放松回合生效) */
export const FINALE_SQUAD_COUNT = 2;
/** 按回合类型的逐个刷怪间隔(秒):放松回合快速倾泻造潮水感,压力回合稍慢便于聚焦 */
export const SPAWN_INTERVAL_BY_TYPE: Record<WaveType, number> = {
[WaveType.Normal]: 0.18,
[WaveType.Pressure]: 0.25,
[WaveType.Relax]: 0.12,
};
/**
* 怪物 ap 回合递进曲线(挂钩玩家输出,独立于总强度框架)。
* 怪物 ap = 玩家平均攻击 × AP_WAVE_GROWTH(wave) × AP_RELATIVE × 怪物类型系数。
* 线性 1.0 → 3.0wave 1 → 20保证后期伤害有可感知的爬升。
*/
export function getApWaveGrowth(wave: number): number {
const t = Math.min(Math.max(wave, 1), MAX_WAVE);
return 1.0 + (t - 1) / (MAX_WAVE - 1) * 2.0; // wave1=1.0 → wave20=3.0
}
/** 怪物登场固定构成1 个 Boss + 4 个小怪(一次全量登场,打完即整局结束) */
export const BOSS_COUNT = 1;
export const MINION_COUNT = 4;
/** Boss 战强度系数(原压力回合系数) */
export const BOSS_TYPE_RATIO = 1.2;
/** 最终 Boss 战 HP 乘区(原 wave 20 配置) */
export const FINAL_HP_MUL = 1.8;
/** 全局怪物强度系数2/3 = 强度削弱三分之一,同时作用于 hp 缩放与 ap 挂钩公式) */
export const MON_POWER_MUL = 2 / 3;
/** 怪物 ap 最终递进系数(原 wave 20 满档 3.0 */
export const AP_GROWTH_FINAL = 3.0;
/** 怪物 ap 相对玩家平均攻击的基础系数(<1 保证单怪不至于一击致命,由 Boss/精英类型再上浮) */
export const AP_RELATIVE = 0.6;
/** Boss 血量加成(原 wave 20 bossBonusHpMul = 1.6 */
export const BOSS_HP_BONUS_MUL = 1.6;
/** 战斗超时(秒):超过后强制结算整局(残留怪销毁并扣留存分) */
export const BATTLE_TIMEOUT = 90;
/** 回合战斗超时超过后强制结束回合残留怪销毁并扣留存分DynamicTuner 因 clearTime 过大自动放水) */
export const WAVE_TIMEOUT = 75;
/** Boss 回合超时Boss 压轴进场(第 20 秒),击杀耗时更长,放宽兜底阈值 */
export const WAVE_TIMEOUT_BOSS = 90;
/**
* 获取指定回合的回合类型
* @param wave 回合1 起)
* @returns WaveType
*/
export function getWaveType(wave: number): WaveType {
if (wave % 5 === 0) return WaveType.Pressure;
if (wave % 5 === 4) return WaveType.Relax; // 大战前夜的收割补给:清杂攒金币备战 Boss
return WaveType.Normal;
}
// ======================== 2. 动态难度调节器 ========================
/**
* 动态难度调节器(系统控制放水 / 加压)
* 与硬编码系数并存,用于根据战况实时微调难度。
*
* 用法示例MissionComp 每回合结束时调用):
* DynamicTuner.adjust(effectiveClearTime, heroDeathCount);
*
* 设计原则(真隐形 DDA
* - 连续映射desired = 1 + (0.8 - clearTime/WAVE_DURATION) × K清场越快要价越高非离散跳变
* - 滞回:连续同方向判定满 HYSTERESIS 回合才生效,偶发超神/崩盘不立即拉阀门
* - 指数靠拢:每回合向 desired 移动 50%,避免突变被玩家察觉
* - 总幅度钳制 [0.7, 1.3]±30%),防止橡皮筋效应
* - 英雄死亡直接锚定 desired=0.8(温和放水),不与慢清场放水叠加
*/
export const DynamicTuner = {
/** 当前难度系数(默认 1.0>1 加压,<1 放水) */
factor: 1.0,
/** 系数下限(最多放水到 70% */
MIN_FACTOR: 0.7,
/** 系数上限(最多加压到 130% */
MAX_FACTOR: 1.3,
/** 连续映射增益clearTime 每偏离基准 100% 时长factor 偏移 K */
K: 0.5,
/** 滞回:连续同方向判定满 N 回合才生效 */
HYSTERESIS: 2,
/** 连续方向计数(>0 加压倾向,<0 放水倾向) */
streak: 0,
/**
* 根据上一回合战况自动调节难度
* @param clearTime 清场耗时(秒,已含清场加速提前量的还原口径)
* @param heroDeathCount 英雄死亡数
* @returns 本回合是否实际调整了 factor
*/
adjust(clearTime: number, heroDeathCount: number): boolean {
// 1) 连续映射期望系数:基准 0.8×时长不动,更快加压、更慢放水;死亡锚定 0.8
let desired: number;
if (heroDeathCount > 0) {
desired = 1 + (0.8 - 1.2) * this.K; // = 0.8
} else {
desired = 1 + (0.8 - clearTime / WAVE_DURATION) * this.K;
}
desired = Math.min(this.MAX_FACTOR, Math.max(this.MIN_FACTOR, desired));
// 2) 滞回:连续同方向满 HYSTERESIS 回合才向 desired 靠拢
const dir = Math.sign(desired - 1);
if (dir === 0) {
this.streak = 0;
return false;
}
this.streak = (Math.sign(this.streak) === dir) ? this.streak + dir : dir;
if (Math.abs(this.streak) < this.HYSTERESIS) return false;
// 3) 指数靠拢:每回合向 desired 移动 50%,避免跳变
const old = this.factor;
this.factor = old + (desired - old) * 0.5;
this.factor = Math.min(this.MAX_FACTOR, Math.max(this.MIN_FACTOR, this.factor));
return this.factor !== old;
},
/** 重置调节器(每局开始时调用) */
reset(): void {
this.factor = 1.0;
this.streak = 0;
},
};
// ======================== 3. 怪物 UUID 池 ========================
// ======================== 2. 怪物 UUID 池 ========================
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
export const MonList: Record<number, number[]> = {};
@@ -211,7 +67,7 @@ for (const key in HeroInfo) {
}
}
// ======================== 3.5 怪物金币掉落配置 ========================
// ======================== 2.5 怪物金币掉落配置 ========================
/**
* 怪物金币掉落配置(按 MonType 分类)
@@ -241,35 +97,78 @@ export function getMonsterGoldDrop(monType: number, isBoss: boolean): number {
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
}
// ======================== 4. 小队模板库 ========================
// ======================== 3. Boss 组合库 ========================
/** 小队内单种怪物的槽位定义 */
export interface SquadSlot {
/** Boss 组合中小怪的槽位定义type + 数量Boss 另由 BOSS_COUNT 提供) */
export interface BossComboMinionSlot {
type: MonType;
count: number;
}
/** 小队模板3-4 只怪的组合单元 */
export interface SquadConfig {
/** Boss 组合配置一场战斗随机抽一组登场1 Boss + minions 小怪 */
export interface BossComboConfig {
id: string;
name: string;
slots: SquadSlot[];
weight: number; // 在小队池中的抽取权重
/** 小怪构成(合计数量建议等于 MINION_COUNT超出截断、不足自动补近战 */
minions: BossComboMinionSlot[];
/** Boss 技能池 id引用 BossSkillPool可选 */
boss_skill_pool?: string[];
/** 小怪技能池 id引用 MonSkillPool可选 */
skill_pool?: string[];
/** 在组合库中的抽取权重 */
weight: number;
}
/** 小队模板库(硬编码) */
export const SquadLibrary: Record<string, SquadConfig> = {
melee_grunt: { id: "melee_grunt", name: "近战步兵组", weight: 10, slots: [{ type: MonType.Melee, count: 3 }] },
assassin_squad: { id: "assassin_squad", name: "刺客突袭组", weight: 6, slots: [{ type: MonType.Assassin, count: 2 }, { type: MonType.Support, count: 1 }] },
mixed_balanced: { id: "mixed_balanced", name: "平衡混合组", weight: 8, slots: [{ type: MonType.Melee, count: 1 }, { type: MonType.Long, count: 1 }, { type: MonType.Heavy, count: 1 }] },
long_line: { id: "long_line", name: "远程线列组", weight: 7, slots: [{ type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }] },
heavy_shield: { id: "heavy_shield", name: "重盾堡垒组", weight: 5, slots: [{ type: MonType.Heavy, count: 1 }, { type: MonType.Melee, count: 2 }] },
summoner_cult: { id: "summoner_cult", name: "召唤教派组", weight: 4, slots: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }] },
/** Boss 护卫队weight=0 不进随机池,仅供引擎在 Boss 回合直接引用,与 Boss 同批压轴进场) */
boss_guard: { id: "boss_guard", name: "Boss 护卫队", weight: 0, slots: [{ type: MonType.Heavy, count: 1 }, { type: MonType.Melee, count: 2 }] },
/** Boss 组合库(硬编码):每场战斗按权重随机抽一组 */
export const BossComboLibrary: Record<string, BossComboConfig> = {
rage_charge: {
id: "rage_charge", name: "狂暴冲锋组", weight: 10,
minions: [{ type: MonType.Melee, count: 3 }, { type: MonType.Heavy, count: 1 }],
boss_skill_pool: ["boss_rage"],
},
iron_wall: {
id: "iron_wall", name: "铁壁推进组", weight: 8,
minions: [{ type: MonType.Heavy, count: 2 }, { type: MonType.Support, count: 1 }, { type: MonType.Long, count: 1 }],
boss_skill_pool: ["boss_iron"],
skill_pool: ["tough"],
},
doom_ritual: {
id: "doom_ritual", name: "毁灭仪式组", weight: 6,
minions: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }],
boss_skill_pool: ["boss_doom"],
skill_pool: ["warcry"],
},
assassin_raid: {
id: "assassin_raid", name: "刺客突袭组", weight: 7,
minions: [{ type: MonType.Assassin, count: 2 }, { type: MonType.Melee, count: 2 }],
boss_skill_pool: ["boss_rage", "boss_doom"],
skill_pool: ["berserk"],
},
blood_legion: {
id: "blood_legion", name: "吸血军团组", weight: 5,
minions: [{ type: MonType.Melee, count: 2 }, { type: MonType.Long, count: 1 }, { type: MonType.Summoner, count: 1 }],
boss_skill_pool: ["boss_iron", "boss_doom"],
skill_pool: ["leech"],
},
};
// ======================== 5. 怪物技能池 ========================
/**
* 按权重随机抽取一组 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. 怪物技能池 ========================
/**
* 怪物技能套装(一套完整的触发技能配置)
@@ -300,8 +199,6 @@ export interface MonSkillSet {
/**
* 怪物技能池(硬编码)
* 压力回合 / Boss 可配置专属技能,普通回合随机挂载增加变数。
*
* 技能 uuid 引用 SkillSet 中的 6000~6500 段触发技能。
*/
export const MonSkillPool: Record<string, MonSkillSet> = {
@@ -342,7 +239,7 @@ export const MonSkillPool: Record<string, MonSkillSet> = {
},
};
/** Boss 专属技能池(压力回合 Boss 随机挂载) */
/** Boss 专属技能池 */
export const BossSkillPool: Record<string, MonSkillSet> = {
/** 狂暴领主:攻击触发全队攻击提升 */
boss_rage: {
@@ -373,130 +270,55 @@ export const BossSkillPool: Record<string, MonSkillSet> = {
},
};
// ======================== 6. 回合配置表 ========================
/** 单回合完整配置 */
export interface WaveConfig {
/** 基础怪物总数(普通回合 72 为上限,放松回合自动 × 1.5 = 108均已含全局 MON_COUNT_MUL = 2 */
base_count: number;
/** 可选小队 id 池,引擎按权重抽取拼装到 base_count */
squad_pool: string[];
/** HP 成长乘区(并入强度缩放分子,终值 = heroPower × 系数 × hp_mul ÷ Σ基础强度) */
hp_mul: number;
/** @deprecated 已废弃ap 改为挂钩玩家输出 × getApWaveGrowth本字段不再参与计算保留仅为兼容配置表结构 */
ap_mul: number;
/** 强度微调(放水 / 加压,默认 1.0 */
power_adjust?: number;
/** 是否 Boss 回合(压力回合必为 true */
boss_wave?: boolean;
/** 普通怪技能池 id可选随机挂载 */
skill_pool?: string[];
/** Boss 技能池 idBoss 回合专用,随机挂载) */
boss_skill_pool?: string[];
}
// ======================== 5. 配置校验 ========================
/**
* 回合配置表(硬编码 wave 1~20
*
* 心流循环5 回合一循环):
* wave % 5 === 0 → 压力回合(必带 Boss强度高、数量少
* wave % 5 === 4 → 放松回合(数量 × 1.5,强度低,大战前夜收割补给)
* 其余 → 普通回合(标准强度)
*
* 强度递进hp_mul / ap_mul 每 5 回合一档,压力回合额外提升。
*/
export const WaveConfigs: Record<number, WaveConfig> = {
// ===== 第一循环:教学期 =====
1: { base_count: 36, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 },
2: { base_count: 42, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 },
3: { base_count: 48, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 },
// 放松回合量大好清Boss 前收割补给
4: { base_count: 54, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 },
// 压力回合:第一 Boss
5: { base_count: 42, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], hp_mul: 1.15, ap_mul: 1.10, boss_wave: true, boss_skill_pool: ["boss_rage"] },
// ===== 第二循环:引入技能怪 =====
6: { base_count: 60, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 },
7: { base_count: 60, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] },
8: { base_count: 66, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] },
// 放松回合量大好清Boss 前收割补给
9: { base_count: 66, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] },
// 压力回合:第二 Boss
10: { base_count: 48, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], hp_mul: 1.30, ap_mul: 1.25, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron"] },
// ===== 第三循环:组合多样化 =====
11: { base_count: 66, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] },
12: { base_count: 66, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.30, ap_mul: 1.25, skill_pool: ["tough", "warcry"] },
13: { base_count: 72, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] },
// 放松回合量大好清Boss 前收割补给
14: { base_count: 72, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], hp_mul: 1.40, ap_mul: 1.35, skill_pool: ["berserk", "tough", "legacy"] },
// 压力回合:第三 Boss中期高潮
15: { base_count: 54, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.50, ap_mul: 1.40, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"] },
// ===== 第四循环终极阶段17~19 power_adjust 逐步爬坡,为最终 Boss 蓄势) =====
16: { base_count: 72, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] },
17: { base_count: 72, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"], power_adjust: 1.05 },
18: { base_count: 72, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"], power_adjust: 1.10 },
// 放松回合:量大好清,最终 Boss 前收割补给
19: { base_count: 72, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"], power_adjust: 1.20 },
// 压力回合:最终 Boss
20: { base_count: 60, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], hp_mul: 1.80, ap_mul: 1.70, boss_wave: true, boss_skill_pool: ["boss_doom", "boss_rage"] },
};
// ======================== 7. 配置校验 ========================
/**
* 校验 WaveConfigs / SquadLibrary / MonSkillPool 引用一致性
* 校验 BossComboLibrary / MonSkillPool 引用一致性
* 建议在游戏启动时调用一次,便于发现配置错误
* @returns 错误信息数组,空数组表示校验通过
*/
export function validateRogueConfig(): string[] {
const errors: string[] = [];
// 1. 校验 WaveConfigs 引用与范围
for (const wave in WaveConfigs) {
const cfg = WaveConfigs[wave];
for (const squadId of cfg.squad_pool) {
if (!SquadLibrary[squadId]) {
errors.push(`Wave ${wave} 引用了不存在的小队: ${squadId}`);
// 1. 校验组合引用与数量
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`);
}
}
if (cfg.skill_pool) {
for (const skillId of cfg.skill_pool) {
if (minionTotal !== MINION_COUNT) {
errors.push(`组合 ${id} 小怪数量=${minionTotal} 与 MINION_COUNT=${MINION_COUNT} 不一致`);
}
if (combo.skill_pool) {
for (const skillId of combo.skill_pool) {
if (!MonSkillPool[skillId]) {
errors.push(`Wave ${wave} 引用了不存在的技能: ${skillId}`);
errors.push(`组合 ${id} 引用了不存在的技能: ${skillId}`);
}
}
}
if (cfg.boss_skill_pool) {
for (const skillId of cfg.boss_skill_pool) {
if (combo.boss_skill_pool) {
for (const skillId of combo.boss_skill_pool) {
if (!BossSkillPool[skillId]) {
errors.push(`Wave ${wave} 引用了不存在的 Boss 技能: ${skillId}`);
errors.push(`组合 ${id} 引用了不存在的 Boss 技能: ${skillId}`);
}
}
}
if (cfg.base_count < 1 || cfg.base_count > MAX_MONSTERS) {
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~${MAX_MONSTERS})`);
}
if (cfg.power_adjust !== undefined && (cfg.power_adjust < 0.8 || cfg.power_adjust > 1.3)) {
errors.push(`Wave ${wave} power_adjust=${cfg.power_adjust} 越界 (0.8~1.3)`);
}
}
// 2. 校验 SquadLibrary 中所有 type 在 MonList 中有怪
for (const id in SquadLibrary) {
const squad = SquadLibrary[id];
for (const slot of squad.slots) {
if (!MonList[slot.type] || MonList[slot.type].length === 0) {
errors.push(`小队 ${id} 引用的怪物类型 ${slot.type}(${MonTypeName[slot.type] || "?"}) 在 MonList 中无可用 uuid`);
}
}
// 2. 校验 Boss 类型在 MonList 中有怪
if ((!MonList[MonType.MeleeBoss] || MonList[MonType.MeleeBoss].length === 0)
&& (!MonList[MonType.LongBoss] || MonList[MonType.LongBoss].length === 0)) {
errors.push("MonList 中无任何 Boss 类型MeleeBoss/LongBoss可用 uuid");
}
return errors;
}
// ======================== 8. 生成结果接口 ========================
// ======================== 6. 生成结果接口 ========================
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
export interface GeneratedMonster {
@@ -505,10 +327,10 @@ export interface GeneratedMonster {
hp: number;
ap: number;
isBoss: boolean;
/** 是否为 Boss 护卫队成员(与 Boss 同批压轴进场,供 UI/统计识别) */
/** 是否为 Boss 护卫队成员(供 UI/统计识别) */
isBossGuard?: boolean;
spawnIndex: number;
/** 本怪所属批次(0~2由 MissionMonComp 按 BATCH_INTERVAL 释放 */
/** 本怪所属批次(保留字段,单场全量登场恒为 0 */
batch: number;
/**
@@ -527,13 +349,12 @@ export interface GeneratedMonster {
};
}
// ======================== 9. 测试模式配置 ========================
// ======================== 7. 测试模式配置 ========================
export const TestModeConfig = {
enable: false, // 默认关闭测试模式
baseHp: 150,
baseAp: 12,
growthRatePerWave: 0.2,
monType: MonType.Melee,
monUuid: 6001,
spawnCount: 1,
@@ -546,145 +367,80 @@ export const TestModeConfig = {
fend: undefined as TriggerGrouped | undefined,
};
// ======================== 10. 生成引擎 ========================
// ======================== 8. 生成引擎 ========================
/**
* 肉鸽刷怪生成引擎
* 按英雄强度反推怪物强度,结合回合类型系数与动态调节器生成怪物列表
* Boss 组合生成引擎
* 随机抽一组 Boss 组合,按英雄强度反推怪物强度(最终 Boss 战档位)
*/
export class RogueSpawningEngine {
/**
* 生成指定回合的怪物列表
* @param waveNumber 回合1 起,>MAX_WAVE 时复用 wave MAX_WAVE 配置)
* @returns 怪物列表,长度 ≤ MAX_MONSTERS
* 生成一场战斗的怪物列表:随机一组 Boss 组合1 Boss + 组合小怪)。
* Boss 压队尾(登场时由 MissionMonComp 统一布位)。
* @returns 怪物列表小怪在前、Boss 在尾)
*/
generateWave(waveNumber: number): GeneratedMonster[] {
if (waveNumber < 1) return [];
generateBossCombo(): GeneratedMonster[] {
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
if (TestModeConfig.enable) {
return this.generateTestWave(waveNumber);
return this.generateTestWave();
}
const wave = Math.min(waveNumber, MAX_WAVE);
const cfg = WaveConfigs[wave];
const waveType = getWaveType(wave);
const typeRatio = WAVE_TYPE_POWER_RATIO[waveType];
const combo = pickBossCombo();
const monsters: GeneratedMonster[] = [];
// 1. 计算目标强度MON_POWER_MUL = 2/3数量翻倍后单怪强度整体削弱三分之一避免总压力膨胀
const heroPower = this.getCurrentHeroPower();
const powerAdjust = cfg.power_adjust ?? 1.0;
const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor * MON_POWER_MUL;
// 2. 确定怪物总数(放松回合 × 1.5;普通/压力回合预留收尾高潮批名额,防 slice 截掉)
let totalCount = cfg.base_count;
if (waveType === WaveType.Relax) {
totalCount = Math.round(totalCount * RELAX_COUNT_MUL);
} else {
totalCount += this.estimateFinaleCount(cfg.squad_pool);
}
totalCount = Math.min(totalCount, MAX_MONSTERS);
// 3. Boss 位(压力回合必带 Boss先记录延后挂载使其压轴进场而非第 0 秒开场
let boss: GeneratedMonster | null = null;
let remaining = totalCount;
if (cfg.boss_wave) {
boss = this.makeBoss(wave, cfg);
remaining -= 1 + BOSS_GUARD_COUNT; // Boss 1 只 + 护卫队名额
}
// 4. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
const monsters: GeneratedMonster[] = this.assembleSquads(cfg.squad_pool, remaining, wave);
// 4.5 收尾高潮批:非放松回合额外补入最强小队,与 Boss 一样压轴(先补入再统一缩放,保证强度自洽)
const finaleSquad = waveType !== WaveType.Relax ? this.pickStrongestSquad(cfg.squad_pool) : null;
if (finaleSquad) {
for (let s = 0; s < FINALE_SQUAD_COUNT; s++) {
for (const slot of finaleSquad.slots) {
for (let c = 0; c < slot.count; c++) {
const m = this.makeMonster(slot.type, wave, 0);
m.batch = BATCH_COUNT - 1; // 标记收尾批,第 8 步不再覆盖
monsters.push(m);
}
if (combo) {
for (const slot of combo.minions) {
for (let i = 0; i < slot.count; i++) {
monsters.push(this.makeMonster(slot.type, monsters.length));
}
}
}
// 4.6 Boss 与护卫队压队尾,使其在批次分配后落入最后一批(回合内高潮点)
if (boss) {
monsters.push(...this.makeBossGuards(wave));
monsters.push(boss);
// 组合库为空或数量不足兜底:补近战小怪至 MINION_COUNT
while (monsters.length < MINION_COUNT) {
monsters.push(this.makeMonster(MonType.Melee, monsters.length));
}
// 5. 属性计算:
// - hp 走总强度缩放框架自动匹配玩家战力hp = base_hp × hpScale
// - ap 与总强度脱钩,挂钩玩家输出 × 回合递进(解决"ap 随回合涨不动"
// 原方案 ap 被总强度封顶 + hp/ap 共用分母,增长被熨平到每回合 5~8%
// Boss 压队尾
for (let i = 0; i < BOSS_COUNT; i++) {
monsters.push(this.makeBoss());
}
// 属性缩放:直接取最终 Boss 战档位系数
const heroPower = this.getCurrentHeroPower();
const targetPower = heroPower * BOSS_TYPE_RATIO * MON_POWER_MUL;
const totalBasePower = monsters.reduce((sum, m) => {
const info = HeroInfo[m.uuid];
return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap);
}, 0);
if (totalBasePower > 0 && targetPower > 0) {
const hpScale = (targetPower * cfg.hp_mul) / totalBasePower;
const hpScale = (targetPower * FINAL_HP_MUL) / totalBasePower;
const heroAvgAp = this.getCurrentHeroAttack();
const apGrowth = getApWaveGrowth(wave);
for (const m of monsters) {
m.hp = Math.max(1, Math.round(m.hp * hpScale));
// ap = 玩家平均攻击 × 回合递进 × 基础系数 × 类型系数 × 全局强度系数(削弱三分之一)
m.ap = Math.max(1, Math.round(heroAvgAp * apGrowth * AP_RELATIVE * MON_POWER_MUL * this.getApTypeFactor(m)));
// ap = 玩家平均攻击 × 最终递进 × 基础系数 × 全局强度系数 × 类型系数
m.ap = Math.max(1, Math.round(heroAvgAp * AP_GROWTH_FINAL * AP_RELATIVE * MON_POWER_MUL * this.getApTypeFactor(m)));
}
}
// 7. 挂载技能普通怪随机技能池Boss 专属技能池)
// 挂载组合技能Boss 专属池 / 小怪普通池)
for (const m of monsters) {
if (m.isBoss && cfg.boss_skill_pool) {
m.skills = this.pickSkillSet(cfg.boss_skill_pool, BossSkillPool);
} else if (!m.isBoss && cfg.skill_pool) {
m.skills = this.pickSkillSet(cfg.skill_pool, MonSkillPool);
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;
}
// 8. 分配批次:按 BATCH_RATIO 递增加权(铺垫 → 加压 → 高潮),收尾小队/Boss/护卫保持最后一批
this.assignBatches(monsters);
if (boss) {
const lastBatch = BATCH_COUNT - 1;
for (const m of monsters) {
if (m.isBoss || m.isBossGuard) m.batch = lastBatch;
}
}
for (let i = 0; i < monsters.length; i++) {
monsters[i].spawnIndex = i;
}
// 9. 硬上限保护
return monsters.slice(0, MAX_MONSTERS);
return monsters;
}
/** 重置引擎状态(每局开始时调用) */
/** 重置引擎状态(每局开始时调用,预留 */
reset(): void {
DynamicTuner.reset();
}
/**
* 获取指定回合的怪物总数
* @param waveNumber 目标回合数
* @returns 预计生成的怪物总数
*/
getWaveMonsterCount(waveNumber: number): number {
if (waveNumber < 1) return 0;
if (TestModeConfig.enable) {
return Math.max(1, TestModeConfig.spawnCount || 1);
}
const wave = Math.min(waveNumber, MAX_WAVE);
const cfg = WaveConfigs[wave];
const waveType = getWaveType(wave);
let count = cfg.base_count;
if (waveType === WaveType.Relax) {
count = Math.round(count * RELAX_COUNT_MUL);
}
return Math.min(count, MAX_MONSTERS);
}
// ======================== 私有生成子算法 ========================
@@ -710,8 +466,7 @@ export class RogueSpawningEngine {
/**
* 计算场上存活英雄的输出强度(用于怪物 ap 挂钩玩家,独立于总强度框架)。
* 取玩家英雄的"实时 ap × 数量",代表玩家当前能打出/承受的攻击量级
* 怪物 ap 以此为锚,保证玩家输出越高、被反击越痛(威胁感跟随成长)。
* 取玩家英雄的"实时 ap 平均",怪物 ap 以此为锚,保证玩家输出越高、被反击越痛
*/
private getCurrentHeroAttack(): number {
let totalAp = 0;
@@ -728,8 +483,7 @@ export class RogueSpawningEngine {
}
/** 测试模式:完全绕过引擎逻辑 */
private generateTestWave(waveNumber: number): GeneratedMonster[] {
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
private generateTestWave(): GeneratedMonster[] {
const count = Math.max(1, TestModeConfig.spawnCount || 1);
const monsters: GeneratedMonster[] = [];
@@ -737,11 +491,11 @@ export class RogueSpawningEngine {
monsters.push({
uuid: TestModeConfig.monUuid,
type: TestModeConfig.monType,
hp: Math.round(TestModeConfig.baseHp * growth),
ap: Math.round(TestModeConfig.baseAp * growth),
hp: TestModeConfig.baseHp,
ap: TestModeConfig.baseAp,
isBoss: false,
spawnIndex: i,
batch: i % BATCH_COUNT,
batch: 0,
skills: {
skill: TestModeConfig.skill,
atking: TestModeConfig.atking,
@@ -755,33 +509,8 @@ export class RogueSpawningEngine {
return monsters;
}
/**
* 小队拼装:按权重从 squad_pool 抽小队,累计到 target 数量
* 超出部分按剩余位截断
*/
private assembleSquads(pool: string[], target: number, wave: number): GeneratedMonster[] {
const result: GeneratedMonster[] = [];
let filled = 0;
let safety = 20; // 防御性循环上限
while (filled < target && safety-- > 0) {
const squad = this.pickWeightedSquad(pool);
if (!squad) break;
const remaining = target - filled;
for (const slot of squad.slots) {
for (let i = 0; i < slot.count && filled < remaining; i++) {
result.push(this.makeMonster(slot.type, wave, filled));
filled++;
}
if (filled >= remaining) break;
}
}
return result;
}
/** 生成 Boss压轴位batch/spawnIndex 为占位值,由 generateWave 统一分配并强制最后一批) */
private makeBoss(wave: number, _cfg: WaveConfig): GeneratedMonster {
/** 生成 Boss基础血量带最终战加成属性随后由总强度框架统一缩放 */
private makeBoss(): GeneratedMonster {
const isMeleeBoss = Math.random() < 0.5;
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
@@ -796,23 +525,19 @@ export class RogueSpawningEngine {
const baseHp = baseInfo ? baseInfo.hp : 500;
const baseAp = baseInfo ? baseInfo.ap : 30;
// Boss 自带额外血量加成:每 5 回合递增 20%wave 5=1.0, 10=1.2, 15=1.4...
const bossBonusHpMul = 1 + 0.20 * Math.floor(Math.max(0, wave - 5) / 5);
return {
uuid,
type,
hp: Math.round(baseHp * bossBonusHpMul),
hp: Math.round(baseHp * BOSS_HP_BONUS_MUL),
ap: baseAp,
isBoss: true,
spawnIndex: 0, // 占位值,由 generateWave 第 8 步统一分配
batch: 0, // 占位值generateWave 会强制 Boss 进入最后一批
spawnIndex: 0, // 占位值,由 generateBossCombo 统一分配
batch: 0,
};
}
/**
* 怪物 ap 类型系数Boss/护卫上浮,远程/召唤稍降(避免玻璃大炮互秒),近战/重甲基准。
* 配合 getApWaveGrowth 使用,让"谁更疼"有明确的类型区分。
* 怪物 ap 类型系数Boss 上浮,远程/召唤稍降(避免玻璃大炮互秒),近战/重甲基准。
*/
private getApTypeFactor(m: GeneratedMonster): number {
if (m.isBoss) return 2.2; // Boss 单发重,符合"压场"定位
@@ -825,77 +550,8 @@ export class RogueSpawningEngine {
}
}
/** 生成 Boss 护卫队(复用 boss_guard 小队模板),与 Boss 同批压轴进场 */
private makeBossGuards(wave: number): GeneratedMonster[] {
const squad = SquadLibrary["boss_guard"];
const guards: GeneratedMonster[] = [];
for (const slot of squad.slots) {
for (let i = 0; i < slot.count; i++) {
const g = this.makeMonster(slot.type, wave, 0);
g.isBossGuard = true;
guards.push(g);
}
}
return guards;
}
/**
* 批次分配:按 BATCH_RATIO 递增加权切分(铺垫 → 加压 → 高潮)。
* 已预标记 batch 的怪(收尾小队 / Boss / 护卫)不参与切分,保持最后一批。
*/
private assignBatches(monsters: GeneratedMonster[]): void {
const normal = monsters.filter(m => m.batch !== BATCH_COUNT - 1 && !m.isBoss && !m.isBossGuard);
const n = normal.length;
if (n === 0) return;
let cursor = 0;
for (let b = 0; b < BATCH_COUNT - 1; b++) {
const quota = Math.round(n * BATCH_RATIO[b]);
for (let k = 0; k < quota && cursor < n; k++, cursor++) {
normal[cursor].batch = b;
}
}
// 剩余全部进入高潮批
for (; cursor < n; cursor++) {
normal[cursor].batch = BATCH_COUNT - 1;
}
}
/** 预估收尾高潮批额外补入的怪物数量(用于 totalCount 预留名额) */
private estimateFinaleCount(pool: string[]): number {
const squad = this.pickStrongestSquad(pool);
if (!squad) return 0;
let per = 0;
for (const slot of squad.slots) per += slot.count;
return per * FINALE_SQUAD_COUNT;
}
/**
* 识别小队池中最强小队:按槽位 MonType 基础强度calcHeroPower 1 级样本)× 数量加权求和。
* 注意不能用 squad.weight——它是"出现频率"语义而非强度。
*/
private pickStrongestSquad(pool: string[]): SquadConfig | null {
let best: SquadConfig | null = null;
let bestScore = -1;
for (const id of pool) {
const sq = SquadLibrary[id];
if (!sq) continue;
let score = 0;
for (const slot of sq.slots) {
const uuids = MonList[slot.type];
const sample = uuids && uuids.length ? HeroInfo[uuids[0]] : null;
score += (sample ? calcHeroPower(sample, 1) : 100) * slot.count;
}
if (score > bestScore) {
bestScore = score;
best = sq;
}
}
return best;
}
/** 生成普通怪物wave 保留参数位,供后续按回合差异化基础属性扩展) */
private makeMonster(type: MonType, _wave: number, spawnIndex: number): GeneratedMonster {
/** 生成普通怪物 */
private makeMonster(type: MonType, spawnIndex: number): GeneratedMonster {
let uuids = MonList[type];
if (!uuids || uuids.length === 0) {
// 兜底 Melee
@@ -914,7 +570,7 @@ export class RogueSpawningEngine {
ap: baseAp,
isBoss: false,
spawnIndex,
batch: 0, // 后续统一分配
batch: 0,
};
}
@@ -936,13 +592,6 @@ export class RogueSpawningEngine {
};
}
/** 按权重从小队 id 池抽 1 个小队 */
private pickWeightedSquad(pool: string[]): SquadConfig | null {
const valid = pool.map(id => SquadLibrary[id]).filter(s => s);
if (valid.length === 0) return null;
return this.pickWeighted(valid);
}
/** 通用加权随机抽取 */
private pickWeighted<T extends { weight: number }>(items: T[]): T | null {
if (items.length === 0) return null;
@@ -957,57 +606,6 @@ export class RogueSpawningEngine {
}
}
// ======================== 11. 全局单例 & 向后兼容导出 ========================
// ======================== 9. 全局单例 ========================
export const spawningEngine = new RogueSpawningEngine();
/** 历史接口:回合槽位(保留兼容) */
export interface IWaveSlot {
type: number;
count: number;
}
/**
* 获取指定回合预计的怪物总数量
* @param waveNumber 目标回合数
*/
export function getWaveMonsterCount(waveNumber: number): number {
return spawningEngine.getWaveMonsterCount(waveNumber);
}
/** 获取回合槽位配置(向后兼容,从 generateWave 反推) */
export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
const generated = spawningEngine.generateWave(waveNumber);
const slotMap = new Map<number, number>();
for (const m of generated) {
slotMap.set(m.type, (slotMap.get(m.type) || 0) + 1);
}
return Array.from(slotMap.entries()).map(([type, count]) => ({ type, count }));
}
export const DefaultWaveSlot: IWaveSlot[] = [
{ type: MonType.Melee, count: 20 },
{ type: MonType.Long, count: 15 },
{ type: MonType.Support, count: 5 },
];
/** 回合槽位配置代理(按需生成) */
export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = new Proxy(
{} as { [wave: number]: IWaveSlot[] },
{
get(_target, prop: string) {
const wave = parseInt(prop, 10);
if (!isNaN(wave) && wave >= 1) {
return getWaveSlotConfig(wave);
}
if (prop === "toJSON") return () => ({});
return undefined;
},
has(_target, prop: string) {
const wave = parseInt(prop, 10);
return !isNaN(wave) && wave >= 1;
},
}
);