/** * @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 秒释放一批 * - 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.Normal]: "普通", [WaveType.Pressure]: "压力", [WaveType.Relax]: "放松", }; /** 波型强度系数(硬编码) */ export const WAVE_TYPE_POWER_RATIO: Record = { [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; /** 每波怪物硬上限(放松波 12 × 1.5 = 18) */ export const MAX_MONSTERS = 18; /** * 获取指定波次的波型 * @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 = {}; 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 = { [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 = { 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 = { /** 狂暴:攻击触发自身攻击提升 */ 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 = { /** 狂暴领主:攻击触发全队攻击提升 */ 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 { /** 基础怪物总数(普通波 12 为上限,放松波自动 × 1.5) */ 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 技能池 id(Boss 波专用,随机挂载) */ 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 = { // ===== 第一循环:教学期 ===== 1: { base_count: 6, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 }, 2: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 }, 3: { base_count: 8, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 }, 4: { base_count: 9, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 }, // 压力波:第一 Boss 5: { base_count: 7, 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: 10, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 }, 7: { base_count: 10, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] }, 8: { base_count: 11, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] }, 9: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] }, // 压力波:第二 Boss 10: { base_count: 8, 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: 11, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] }, 12: { base_count: 11, 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: 12, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] }, 14: { base_count: 12, 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: 9, 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: 12, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] }, 17: { base_count: 12, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"] }, 18: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"] }, 19: { base_count: 12, 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: 10, 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): 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(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(); 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; }, } );