Files
pixelheros/assets/script/game/map/RogueConfig.ts
panFD 153e0e9a1f feat(map): 调整肉鸽模式怪物数量配置
统一更新全波次基础怪物上限,将普通波基数提升至36,放松波达到54,同时同步更新所有波次的怪物数量配置,优化游戏战斗体验
2026-08-04 21:01:50 +08:00

806 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file RogueConfig.ts
* @description 肉鸽刷怪系统(基于英雄强度的动态难度 + 心流循环)
*
* 设计层次:
* 1. WaveType - 波型(普通 / 压力 / 放松5 波一个心流循环
* 2. WaveConfig - 每波硬编码(数量 + 小队池 + HP/AP 倍率 + 强度微调)
* 3. DynamicTuner - 动态难度调节器(系统根据战况放水 / 加压)
* 4. MonSkillSet - 怪物技能池atking / atked / dead 等全触发类型)
* 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
*
* 核心公式:
* heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄)
* targetPower = heroPower × 波型系数 × wave.power_adjust × DynamicTuner.factor
* scale = targetPower ÷ Σ 怪物基础强度
* 每只怪: hp ×= scale, ap ×= scale
*
* 波次节奏:
* - 最大 20 波,第 20 波通关
* - 每波 30 秒,固定分 3 批,每 10 秒释放一批
* - 普通波 18~36 只,放松波 × 1.5 = 27~54 只
* - wave % 5 === 0 → 压力波(必带 Boss强度高、数量少
* - wave % 5 === 1 → 放松波(数量 × 1.5,强度低,爽快清屏)
*/
import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry, heroInfo } 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. 波型与节奏常量 ========================
/** 波型枚举 */
export enum WaveType {
Normal = 0, // 普通波
Pressure = 1, // 压力波wave % 5 === 0必带 Boss
Relax = 2, // 放松波wave % 5 === 1量大强度低
}
/** 波型名称 */
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;
/** 最大波次(第 20 波通关) */
export const MAX_WAVE = 20;
/** 每波时长(秒) */
export const WAVE_DURATION = 30;
/** 每波分批次数 */
export const BATCH_COUNT = 3;
/** 每批间隔30 秒 / 3 批 = 10 秒 */
export const BATCH_INTERVAL = WAVE_DURATION / BATCH_COUNT;
/** 每波怪物硬上限(放松波 36 × 1.5 = 54 */
export const MAX_MONSTERS = 54;
/**
* 获取指定波次的波型
* @param wave 波次1 起)
* @returns WaveType
*/
export function getWaveType(wave: number): WaveType {
if (wave % 5 === 0) return WaveType.Pressure;
if (wave % 5 === 1) return WaveType.Relax;
return WaveType.Normal;
}
// ======================== 2. 动态难度调节器 ========================
/**
* 动态难度调节器(系统控制放水 / 加压)
* 与硬编码系数并存,用于根据战况实时微调难度。
*
* 用法示例MissionComp 每波结束时调用):
* DynamicTuner.adjust(clearTime, heroDeathCount);
*
* 调节规则(内部硬编码):
* - 清场时间 < 20s 且无英雄死亡 → factor += 0.05(加压)
* - 清场时间 > 28s 或有英雄死亡 → factor -= 0.05(放水)
* - factor 范围钳制 [0.5, 2.0]
*/
export const DynamicTuner = {
/** 当前难度系数(默认 1.0>1 加压,<1 放水) */
factor: 1.0,
/** 系数下限(最多放水到 50% */
MIN_FACTOR: 0.5,
/** 系数上限(最多加压到 200% */
MAX_FACTOR: 2.0,
/** 单次调节步长 */
STEP: 0.05,
/**
* 根据上一波战况自动调节难度
* @param clearTime 清场耗时(秒)
* @param heroDeathCount 英雄死亡数
*/
adjust(clearTime: number, heroDeathCount: number): void {
if (heroDeathCount > 0 || clearTime > WAVE_DURATION * 0.95) {
// 有英雄死亡或清场过慢 → 放水
this.factor = Math.max(this.MIN_FACTOR, this.factor - this.STEP);
} else if (clearTime < WAVE_DURATION * 0.65 && heroDeathCount === 0) {
// 清场过快且无死亡 → 加压
this.factor = Math.min(this.MAX_FACTOR, this.factor + this.STEP);
}
},
/** 重置调节器(每局开始时调用) */
reset(): void {
this.factor = 1.0;
},
};
// ======================== 3. 怪物 UUID 池 ========================
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
export const MonList: Record<number, number[]> = {};
for (const key in HeroInfo) {
const info = HeroInfo[key];
if (info.fac === FacSet.MON && info.monType !== undefined) {
if (!MonList[info.monType]) {
MonList[info.monType] = [];
}
MonList[info.monType].push(info.uuid);
}
}
// ======================== 3.5 怪物金币掉落配置 ========================
/**
* 怪物金币掉落配置(按 MonType 分类)
* - base: 普通怪物死亡时掉落的固定金币数
* - boss: Boss 怪物死亡时掉落的固定金币数(仅对 Boss 类型生效)
*/
export const MonsterGoldSet: Record<number, { base: number; boss: number }> = {
[MonType.Melee]: { base: 1, boss: 0 },
[MonType.Heavy]: { base: 2, boss: 0 },
[MonType.Long]: { base: 2, boss: 0 },
[MonType.Support]: { base: 3, boss: 0 },
[MonType.Summoner]: { base: 3, boss: 0 },
[MonType.Assassin]: { base: 3, boss: 0 },
[MonType.MeleeBoss]: { base: 0, boss: 15 },
[MonType.LongBoss]: { base: 0, boss: 15 },
};
/**
* 获取指定怪物类型的金币掉落数量
* @param monType 怪物类型MonType
* @param isBoss 是否为 Boss
* @returns 掉落的金币数量≥0
*/
export function getMonsterGoldDrop(monType: number, isBoss: boolean): number {
const cfg = MonsterGoldSet[monType];
if (!cfg) return 0;
return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base));
}
// ======================== 4. 小队模板库 ========================
/** 小队内单种怪物的槽位定义 */
export interface SquadSlot {
type: MonType;
count: number;
}
/** 小队模板3-4 只怪的组合单元) */
export interface SquadConfig {
id: string;
name: string;
slots: SquadSlot[];
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 }] },
};
// ======================== 5. 怪物技能池 ========================
/**
* 怪物技能套装(一套完整的触发技能配置)
* 覆盖触发时机call / atking / atked / dead / fstart / fend / revive
*/
export interface MonSkillSet {
id: string;
name: string;
/** 普攻技能覆盖(可选,不填则使用怪物默认普攻) */
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
/** 召唤触发 */
call?: TriggerGrouped;
/** 攻击触发 */
atking?: TriggerGrouped;
/** 受击触发 */
atked?: TriggerGrouped;
/** 死亡触发 */
dead?: TriggerGrouped;
/** 战斗开始触发 */
fstart?: TriggerGrouped;
/** 战斗结束触发 */
fend?: TriggerGrouped;
/** 复活 */
revive?: LvReviveEntry[];
/** 在技能池中的抽取权重 */
weight: number;
}
/**
* 怪物技能池(硬编码)
* 压力波 / Boss 可配置专属技能,普通波随机挂载增加变数。
*
* 技能 uuid 引用 SkillSet 中的 6000~6500 段触发技能。
*/
export const MonSkillPool: Record<string, MonSkillSet> = {
/** 狂暴:攻击触发自身攻击提升 */
berserk: {
id: "berserk", name: "狂暴", weight: 10,
atking: {
6401: [{ lv: 1, t_num: 5, overrides: { ap: 1 } }],
},
},
/** 坚韧:受击获得护盾 */
tough: {
id: "tough", name: "坚韧", weight: 8,
atked: {
6301: [{ lv: 1, t_num: 3, overrides: { ap: 2 } }],
},
},
/** 遗志:死亡时全队攻击提升 */
legacy: {
id: "legacy", name: "遗志", weight: 6,
dead: {
6401: [{ lv: 1, t_num: 1, overrides: { ap: 3 } }],
},
},
/** 战吼:战斗开始时全队攻击提升 */
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 专属技能池(压力波 Boss 随机挂载) */
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 } }],
},
},
};
// ======================== 6. 波次配置表 ========================
/** 单波次完整配置 */
export interface WaveConfig {
/** 基础怪物总数(普通波 36 为上限,放松波自动 × 1.5 = 54 */
base_count: number;
/** 可选小队 id 池,引擎按权重抽取拼装到 base_count */
squad_pool: string[];
/** HP 强化倍率(硬编码,逐波递进) */
hp_mul: number;
/** AP 强化倍率(硬编码,逐波递进) */
ap_mul: number;
/** 强度微调(放水 / 加压,默认 1.0 */
power_adjust?: number;
/** 是否 Boss 波(压力波必为 true */
boss_wave?: boolean;
/** 普通怪技能池 id可选随机挂载 */
skill_pool?: string[];
/** Boss 技能池 idBoss 波专用,随机挂载) */
boss_skill_pool?: string[];
}
/**
* 波次配置表(硬编码 wave 1~20
*
* 心流循环5 波一循环):
* wave % 5 === 0 → 压力波(必带 Boss强度高、数量少
* wave % 5 === 1 → 放松波(数量 × 1.5,强度低)
* 其余 → 普通波(标准强度)
*
* 强度递进hp_mul / ap_mul 每 5 波一档,压力波额外提升。
*/
export const WaveConfigs: Record<number, WaveConfig> = {
// ===== 第一循环:教学期 =====
1: { base_count: 18, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 },
2: { base_count: 21, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 },
3: { base_count: 24, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 },
4: { base_count: 27, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 },
// 压力波:第一 Boss
5: { base_count: 21, 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: 30, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 },
7: { base_count: 30, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] },
8: { base_count: 33, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] },
9: { base_count: 33, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] },
// 压力波:第二 Boss
10: { base_count: 24, 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: 33, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] },
12: { base_count: 33, 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: 36, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] },
14: { base_count: 36, 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: 27, 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"] },
// ===== 第四循环:终极阶段 =====
// 放松波
16: { base_count: 36, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] },
17: { base_count: 36, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"] },
18: { base_count: 36, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"] },
19: { base_count: 36, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"] },
// 压力波:最终 Boss
20: { base_count: 30, 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 引用一致性
* 建议在游戏启动时调用一次,便于发现配置错误
* @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}`);
}
}
if (cfg.skill_pool) {
for (const skillId of cfg.skill_pool) {
if (!MonSkillPool[skillId]) {
errors.push(`Wave ${wave} 引用了不存在的技能: ${skillId}`);
}
}
}
if (cfg.boss_skill_pool) {
for (const skillId of cfg.boss_skill_pool) {
if (!BossSkillPool[skillId]) {
errors.push(`Wave ${wave} 引用了不存在的 Boss 技能: ${skillId}`);
}
}
}
if (cfg.base_count < 1 || cfg.base_count > MAX_MONSTERS) {
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~${MAX_MONSTERS})`);
}
}
// 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`);
}
}
}
return errors;
}
// ======================== 8. 生成结果接口 ========================
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
export interface GeneratedMonster {
uuid: number;
type: MonType;
hp: number;
ap: number;
isBoss: boolean;
spawnIndex: number;
/** 本怪所属批次0~2由 MissionMonComp 按 BATCH_INTERVAL 释放) */
batch: number;
/**
* 怪物技能套装(覆盖全部触发时机)
* 注入方式与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp
*/
skills?: {
skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides };
call?: TriggerGrouped;
atking?: TriggerGrouped;
atked?: TriggerGrouped;
dead?: TriggerGrouped;
fstart?: TriggerGrouped;
fend?: TriggerGrouped;
revive?: LvReviveEntry[];
};
}
// ======================== 9. 测试模式配置 ========================
export const TestModeConfig = {
enable: false, // 默认关闭测试模式
baseHp: 150,
baseAp: 12,
growthRatePerWave: 0.2,
monType: MonType.Melee,
monUuid: 6001,
spawnCount: 1,
skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined,
atking: undefined as TriggerGrouped | undefined,
atked: undefined as TriggerGrouped | undefined,
dead: undefined as TriggerGrouped | undefined,
fstart: undefined as TriggerGrouped | undefined,
fend: undefined as TriggerGrouped | undefined,
};
// ======================== 10. 生成引擎 ========================
/**
* 肉鸽刷怪生成引擎
* 按英雄强度反推怪物强度,结合波型系数与动态调节器生成怪物列表
*/
export class RogueSpawningEngine {
/**
* 生成指定波次的怪物列表
* @param waveNumber 波次1 起,>MAX_WAVE 时复用 wave MAX_WAVE 配置)
* @returns 怪物列表,长度 ≤ MAX_MONSTERS
*/
generateWave(waveNumber: number): GeneratedMonster[] {
if (waveNumber < 1) return [];
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
if (TestModeConfig.enable) {
return this.generateTestWave(waveNumber);
}
const wave = Math.min(waveNumber, MAX_WAVE);
const cfg = WaveConfigs[wave];
const waveType = getWaveType(wave);
const typeRatio = WAVE_TYPE_POWER_RATIO[waveType];
// 1. 计算目标强度
const heroPower = this.getCurrentHeroPower();
const powerAdjust = cfg.power_adjust ?? 1.0;
const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor;
// 2. 确定怪物总数(放松波 × 1.5
let totalCount = cfg.base_count;
if (waveType === WaveType.Relax) {
totalCount = Math.round(totalCount * RELAX_COUNT_MUL);
}
totalCount = Math.min(totalCount, MAX_MONSTERS);
// 3. Boss 位(压力波必带 Boss占 1 个名额)
const monsters: GeneratedMonster[] = [];
let remaining = totalCount;
if (cfg.boss_wave) {
monsters.push(this.makeBoss(wave, cfg));
remaining -= 1;
}
// 4. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, wave);
monsters.push(...squadMonsters);
// 5. 应用硬编码 HP/AP 倍率
for (const m of monsters) {
m.hp = Math.max(1, Math.round(m.hp * cfg.hp_mul));
m.ap = Math.max(1, Math.round(m.ap * cfg.ap_mul));
}
// 6. 按英雄强度反推缩放系数
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 scale = targetPower / totalBasePower;
for (const m of monsters) {
m.hp = Math.max(1, Math.round(m.hp * scale));
m.ap = Math.max(1, Math.round(m.ap * scale));
}
}
// 7. 挂载技能普通怪随机技能池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);
}
}
// 8. 分配批次0~2均匀分布
for (let i = 0; i < monsters.length; i++) {
monsters[i].batch = i % BATCH_COUNT;
monsters[i].spawnIndex = i;
}
// 9. 硬上限保护
return monsters.slice(0, MAX_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);
}
// ======================== 私有生成子算法 ========================
/**
* 计算场上存活英雄的总强度
* 通过 ECS 查询所有 HeroAttrsComp累加 calcHeroPower
*/
private getCurrentHeroPower(): number {
let total = 0;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const attrs = entity.get(HeroAttrsComp);
if (!attrs || attrs.is_dead || attrs.fac !== FacSet.HERO) return;
const info = HeroInfo[attrs.hero_uuid];
if (!info) return;
// 使用英雄当前等级(升级卡驱动),保证强度评估与实战一致
const lv = Math.max(1, attrs.lv || 1);
total += calcHeroPower(info, lv);
});
// 兜底:无英雄时返回基准强度,避免除零
return Math.max(total, 100);
}
/** 测试模式:完全绕过引擎逻辑 */
private generateTestWave(waveNumber: number): GeneratedMonster[] {
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
const count = Math.max(1, TestModeConfig.spawnCount || 1);
const monsters: GeneratedMonster[] = [];
for (let i = 0; i < count; i++) {
monsters.push({
uuid: TestModeConfig.monUuid,
type: TestModeConfig.monType,
hp: Math.round(TestModeConfig.baseHp * growth),
ap: Math.round(TestModeConfig.baseAp * growth),
isBoss: false,
spawnIndex: i,
batch: i % BATCH_COUNT,
skills: {
skill: TestModeConfig.skill,
atking: TestModeConfig.atking,
atked: TestModeConfig.atked,
dead: TestModeConfig.dead,
fstart: TestModeConfig.fstart,
fend: TestModeConfig.fend,
},
});
}
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首位 */
private makeBoss(wave: number, cfg: WaveConfig): GeneratedMonster {
const isMeleeBoss = Math.random() < 0.5;
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
// 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)];
const baseInfo = HeroInfo[uuid];
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),
ap: baseAp,
isBoss: true,
spawnIndex: 0,
batch: 0, // Boss 固定第一批
};
}
/** 生成普通怪物 */
private makeMonster(type: MonType, wave: 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;
return {
uuid,
type,
hp: baseHp,
ap: baseAp,
isBoss: false,
spawnIndex,
batch: 0, // 后续统一分配
};
}
/** 从技能池中按权重抽取一套技能 */
private pickSkillSet(pool: string[], library: Record<string, MonSkillSet>): GeneratedMonster["skills"] | undefined {
const valid = pool.map(id => library[id]).filter(s => s);
if (valid.length === 0) return undefined;
const picked = this.pickWeighted(valid);
if (!picked) return undefined;
return {
skill: picked.skill,
call: picked.call,
atking: picked.atking,
atked: picked.atked,
dead: picked.dead,
fstart: picked.fstart,
fend: picked.fend,
revive: picked.revive,
};
}
/** 按权重从小队 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;
const total = items.reduce((s, it) => s + it.weight, 0);
if (total <= 0) return items[Math.floor(Math.random() * items.length)];
let r = Math.random() * total;
for (const it of items) {
r -= it.weight;
if (r <= 0) return it;
}
return items[items.length - 1];
}
}
// ======================== 11. 全局单例 & 向后兼容导出 ========================
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;
},
}
);