From 279e239e3a8a2fb56ef25b7814f04479b3217859 Mon Sep 17 00:00:00 2001 From: panFD Date: Sun, 5 Jul 2026 22:33:42 +0800 Subject: [PATCH] refactor(rogue): replace random spawning with rule-based configs Introduce hard-coded WaveConfigs (1-30), modular SquadLibrary (6 templates), and dual-layer enchant system (WaveEnchantLibrary + MonsterElite). Spawn engine now assembles squads by weight, applies global wave scaling and enchant modifiers, and rolls individual elites with bounded rate. Backward-compatible exports preserved; AffixType marked deprecated. MissionMonComp/MissionComp unchanged. --- assets/script/game/map/RogueConfig.ts | 667 ++++++++++++++++++++------ 1 file changed, 518 insertions(+), 149 deletions(-) diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index 4bcaf83a..f92a8a25 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -1,38 +1,71 @@ /** * @file RogueConfig.ts - * @description 肉鸽刷怪系统 (重构精简版) + * @description 肉鸽刷怪系统(基于硬编码规则 + 模块化随机) * - * 核心规则: - * 1. 最大怪物数量 12 个 - * 2. 1-4 波:怪物数量逐步增加(如 1, 2, 4, 6) - * 3. 第 5 波:出现第一个 Boss - * 4. 5 波之后:随机出现 6-12 个怪物 - * 5. 强度:仅通过怪物数量和波次(等级)来逐步提升,每波增加一定比例基础属性。 + * 设计层次: + * 1. MonsterElite - 个体强化(5 种) + * 2. WaveEnchant - 波次级强化(环境修饰) + * 3. SquadConfig - 小队模板(3-4 只怪的组合单元) + * 4. WaveConfig - 每波硬编码(基础数 + 小队池 + 强化池) + * 5. RogueSpawningEngine - 生成引擎(按规则组合上述配置) + * + * 详细设计见 docs/superpowers/specs/2026-07-05-rogue-config-refactor-design.md */ import { HeroInfo, MonType, MonTypeName } from "../common/config/heroSet"; import { FacSet } from "../common/config/GameSet"; -// ======================== 词缀类型枚举 ======================== +// ======================== 1. 枚举与基础类型 ======================== +/** + * 个体强化类型(精选 5 种) + * 设计权衡:HeroAttrsComp 暂无 atk_cd / regen 字段,因此 Swift/Regen 简化为数值差异化。 + * 待 HeroAttrsComp 升级后可启用 cd_mul / regen 扩展点。 + */ +export enum MonsterElite { + Elite = 0, // 精英:全面强化 + Berserk = 1, // 狂暴:高攻低血 + Shield = 2, // 护盾:坦克 + Regen = 3, // 再生:高血偏弱攻 + Swift = 4, // 疾风:平衡偏输出 +} + +/** 个体强化效果定义 */ +export const MonsterEliteSet: Record = { + [MonsterElite.Elite]: { name: "精英", hp_mul: 1.5, ap_mul: 1.30 }, + [MonsterElite.Berserk]: { name: "狂暴", hp_mul: 0.9, ap_mul: 1.60 }, + [MonsterElite.Shield]: { name: "护盾", hp_mul: 1.8, ap_mul: 0.80 }, + [MonsterElite.Regen]: { name: "再生", hp_mul: 1.3, ap_mul: 0.95 }, + [MonsterElite.Swift]: { name: "疾风", hp_mul: 1.0, ap_mul: 1.15 }, +}; + +/** + * 旧版词缀类型枚举(已废弃) + * @deprecated 已被 MonsterElite 取代,仅保留导出避免破坏外部引用。 + * 新代码请使用 MonsterElite。 + */ export enum AffixType { - Elite = 0, - Berserk = 1, - Shield = 2, - Regen = 3, - Swift = 4, - Giant = 5, - Chain = 6, - SummonerA = 7, - CritRes = 8, - FreezeRes = 9, + Elite = 0, + Berserk = 1, + Shield = 2, + Regen = 3, + Swift = 4, + Giant = 5, + Chain = 6, + SummonerA = 7, + CritRes = 8, + FreezeRes = 9, KnockbackRes = 10, } -// ======================== 怪物 UUID 池 ======================== +// ======================== 2. 怪物 UUID 池 ======================== +/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */ export const MonList: Record = {}; -// 动态从 HeroInfo 中提取怪物分类,避免硬编码 for (const key in HeroInfo) { const info = HeroInfo[key]; if (info.fac === FacSet.MON && info.monType !== undefined) { @@ -43,7 +76,212 @@ for (const key in HeroInfo) { } } -// ======================== 测试模式配置 ======================== +// ======================== 3. 波次级强化库 ======================== + +/** + * 波次级强化(环境修饰符) + * 作用于整波怪物,与 MonsterElite(个体)解耦。 + */ +export interface WaveEnchant { + id: string; + name: string; + desc: string; + hp_mul?: number; // 全员 HP 乘子 + ap_mul?: number; // 全员 AP 乘子 + cd_mul?: number; // 全员攻击间隔乘子(后续扩展点) + elite_rate_mul?: number; // 该波个体强化出现概率乘子 + weight: number; // 在强化池中的抽取权重 +} + +/** 波次强化库(硬编码) */ +export const WaveEnchantLibrary: Record = { + frenzy: { id: "frenzy", name: "狂热浪潮", desc: "全员攻击力+25%", ap_mul: 1.25, weight: 10 }, + ironhide: { id: "ironhide", name: "铁皮大军", desc: "全员生命+40%", hp_mul: 1.40, weight: 10 }, + swift: { id: "swift", name: "疾风突袭", desc: "全员攻速+20%", cd_mul: 0.80, weight: 8 }, + nightmare: { id: "nightmare", name: "梦魇来袭", desc: "个体强化率×2,HP+15%", elite_rate_mul: 2.0, hp_mul: 1.15, weight: 5 }, + fortress: { id: "fortress", name: "钢铁堡垒", desc: "全员 HP+60% AP-15%", hp_mul: 1.60, ap_mul: 0.85, weight: 6 }, +}; + +// ======================== 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. 波次配置表 ======================== + +/** 单波次完整配置 */ +export interface WaveConfig { + /** 基础怪物数(1~12) */ + base_count: number; + /** 可选小队 id 池,引擎按权重抽取拼装到 base_count */ + squad_pool: string[]; + /** 可选波次强化 id 池,按权重抽 0~2 个(可空表示无强化) */ + enchant_pool?: string[]; + /** 是否 Boss 波(首位放 Boss) */ + boss_wave?: boolean; + /** 该波个体强化基础概率(默认按波次递增) */ + elite_base_rate?: number; +} + +/** + * 波次配置表(硬编码 wave 1~30) + * 节奏曲线:教学期(1-4) → 第一Boss(5) → 引入强化(6-9) → 第二Boss(10) → + * 组合多样化(11-14) → 第三Boss(15) → 高压阶段(16-24) → + * 第四Boss(25) → 终极阶段(26-29) → 最终Boss(30) + */ +export const WaveConfigs: Record = { + // ===== 教学期 ===== + 1: { base_count: 3, squad_pool: ["melee_grunt"] }, + 2: { base_count: 4, squad_pool: ["melee_grunt", "mixed_balanced"] }, + 3: { base_count: 5, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"] }, + 4: { base_count: 6, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"] }, + // ===== 第一 Boss ===== + 5: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], boss_wave: true }, + // ===== 引入波次强化 ===== + 6: { base_count: 7, squad_pool: ["assassin_squad", "long_line", "mixed_balanced"], enchant_pool: ["frenzy"], elite_base_rate: 0.10 }, + 7: { base_count: 8, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.12 }, + 8: { base_count: 8, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.14 }, + 9: { base_count: 9, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"],elite_base_rate: 0.16 }, + // ===== 第二 Boss ===== + 10: { base_count: 10, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide"],boss_wave: true, elite_base_rate: 0.18 }, + // ===== 组合多样化 ===== + 11: { base_count: 10, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"],enchant_pool: ["swift", "frenzy"], elite_base_rate: 0.18 }, + 12: { base_count: 10, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.20 }, + 13: { base_count: 11, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.22 }, + 14: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], enchant_pool: ["ironhide", "swift"], elite_base_rate: 0.24 }, + // ===== 第三 Boss(中期高潮) ===== + 15: { base_count: 11, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.25 }, + // ===== 高压阶段 ===== + 16: { base_count: 11, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.26 }, + 17: { base_count: 11, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.27 }, + 18: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], enchant_pool: ["nightmare", "swift"], elite_base_rate: 0.28 }, + 19: { base_count: 12, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], enchant_pool: ["fortress"], elite_base_rate: 0.28 }, + 20: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.30 }, + 21: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "summoner_cult"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.30 }, + 22: { base_count: 12, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.30 }, + 23: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "melee_grunt"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.32 }, + 24: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "long_line"], enchant_pool: ["fortress", "swift"], elite_base_rate: 0.32 }, + // ===== 第四 Boss ===== + 25: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "heavy_shield"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.35 }, + // ===== 终极阶段 ===== + 26: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "long_line", "summoner_cult"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.35 }, + 27: { base_count: 12, squad_pool: ["heavy_shield", "mixed_balanced", "summoner_cult"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.38 }, + 28: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "melee_grunt"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.40 }, + 29: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "summoner_cult"], enchant_pool: ["frenzy", "ironhide", "fortress"], elite_base_rate: 0.42 }, + // ===== 最终 Boss ===== + 30: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide", "swift", "nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.45 }, +}; + +// ======================== 6. 全局难度缩放 ======================== + +/** + * 根据波次返回全局 HP/AP 乘子,每 5 波递进一档 + * @param wave 当前波次 + * @returns hp_mul / ap_mul + */ +export function getGlobalScale(wave: number): { hp_mul: number; ap_mul: number } { + const step = Math.floor((wave - 1) / 5); + return { + hp_mul: 1 + 0.15 * step, // wave 1=1.0, 6=1.15, 11=1.30, 16=1.45, 21=1.60, 26=1.75 + ap_mul: 1 + 0.08 * step, // wave 1=1.0, 6=1.08, 11=1.16, 16=1.24, 21=1.32, 26=1.40 + }; +} + +// ======================== 7. 配置校验 ======================== + +/** + * 校验 WaveConfigs / SquadLibrary / WaveEnchantLibrary 引用一致性 + * 建议在游戏启动时调用一次,便于发现配置错误 + * @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.enchant_pool) { + for (const enchId of cfg.enchant_pool) { + if (!WaveEnchantLibrary[enchId]) { + errors.push(`Wave ${wave} 引用了不存在的强化: ${enchId}`); + } + } + } + if (cfg.base_count < 1 || cfg.base_count > 12) { + errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~12)`); + } + } + + // 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; + + /** @deprecated 已被 elite 取代,始终返回 [],仅向后兼容 */ + affixes: AffixType[]; + + /** 个体强化(无则 undefined) */ + elite?: MonsterElite; + /** 该波触发的波次强化 id 列表 */ + wave_enchants: string[]; + + /** 测试模式专用技能覆盖 */ + testSkills?: { + skill?: { s_uuid: number; cd?: number; overrides?: any }; + atking?: { s_uuid: number; t_num: number; overrides?: any }[]; + atked?: { s_uuid: number; t_num: number; overrides?: any }[]; + dead?: { s_uuid: number; t_num: number; overrides?: any }[]; + fstart?: { s_uuid: number; t_num: number; overrides?: any }[]; + fend?: { s_uuid: number; t_num: number; overrides?: any }[]; + }; +} + +// ======================== 9. 测试模式配置 ======================== export const TestModeConfig = { enable: false, // 默认关闭测试模式 @@ -52,167 +290,113 @@ export const TestModeConfig = { growthRatePerWave: 0.2, monType: MonType.Melee, monUuid: 6001, + /** @deprecated 已被 testElite 取代 */ affixes: [] as AffixType[], spawnCount: 1, + /** 测试个体强化 */ + testElite: undefined as MonsterElite | undefined, + /** 测试波次强化 id */ + testWaveEnchant: undefined as string | undefined, + skill: undefined as { s_uuid: number; cd?: number; overrides?: any } | undefined, atking: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined, atked: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined, dead: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined, fstart: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined, fend: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined, -} +}; -// ======================== 生成结果接口 ======================== - -export interface GeneratedMonster { - uuid: number - type: MonType - hp: number - ap: number - affixes: AffixType[] - isBoss: boolean - spawnIndex: number - testSkills?: { - skill?: { s_uuid: number; cd?: number; overrides?: any }; - atking?: { s_uuid: number; t_num: number; overrides?: any }[]; - atked?: { s_uuid: number; t_num: number; overrides?: any }[]; - dead?: { s_uuid: number; t_num: number; overrides?: any }[]; - fstart?: { s_uuid: number; t_num: number; overrides?: any }[]; - fend?: { s_uuid: number; t_num: number; overrides?: any }[]; - } -} - -// ======================== 生成引擎 ======================== +// ======================== 10. 生成引擎 ======================== +/** + * 肉鸽刷怪生成引擎 + * 按硬编码 WaveConfig 规则,组合小队模板与双层强化系统生成怪物列表 + */ export class RogueSpawningEngine { + /** + * 生成指定波次的怪物列表 + * @param waveNumber 波次(1 起,>30 时复用 wave 30 配置) + * @returns 怪物列表,长度 ≤ 12 + */ generateWave(waveNumber: number): GeneratedMonster[] { if (waveNumber < 1) return []; - // 测试模式拦截 + // 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪 if (TestModeConfig.enable) { - 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), - affixes: [...TestModeConfig.affixes], - isBoss: false, - spawnIndex: i, - testSkills: { - skill: TestModeConfig.skill, - atking: TestModeConfig.atking, - atked: TestModeConfig.atked, - dead: TestModeConfig.dead, - fstart: TestModeConfig.fstart, - fend: TestModeConfig.fend - } - }); - } - return monsters; + return this.generateTestWave(waveNumber); } - // 1. 确定生成数量 - let count = this.getWaveMonsterCount(waveNumber); + // 1. 取硬编码 WaveConfig(>30 时复用 wave 30 配置) + const cfg = WaveConfigs[Math.min(waveNumber, 30)]; - // 第 5 波之后,在 count 的基础上加上随机逻辑 (覆盖原有固定返回值) - if (waveNumber > 5 && !TestModeConfig.enable) { - count = Math.floor(Math.random() * 7) + 6; - } + // 2. 抽取波次级强化(按权重从 enchant_pool 抽 0~2 个) + const enchants = this.pickEnchants(cfg.enchant_pool); - // 限制最大怪物数为 12 - count = Math.min(count, 12); - - // 每 5 波出现一次 Boss - const isBossWave = (waveNumber % 5 === 0); + // 3. 计算波次最终属性乘子 + const globalScale = getGlobalScale(waveNumber); + const enchantHpMul = enchants.reduce((m, e) => m * (e.hp_mul ?? 1), 1); + const enchantApMul = enchants.reduce((m, e) => m * (e.ap_mul ?? 1), 1); + const eliteRateMul = enchants.reduce((m, e) => m * (e.elite_rate_mul ?? 1), 1); + // 4. Boss 位(Boss 波首位占 1 个) const monsters: GeneratedMonster[] = []; - - // 普通怪池 - const normalTypes = [ - MonType.Melee, MonType.Heavy, MonType.Long, - MonType.Support, MonType.Assassin, MonType.Summoner - ]; - - for (let i = 0; i < count; i++) { - let type: MonType; - let isBoss = false; - - if (isBossWave && i === 0) { - // 生成 Boss - isBoss = true; - type = Math.random() > 0.5 ? MonType.MeleeBoss : MonType.LongBoss; - } else { - // 生成普通小怪 - type = normalTypes[Math.floor(Math.random() * normalTypes.length)]; - } - - const uuids = MonList[type] || MonList[MonType.Melee]; - const uuid = uuids[Math.floor(Math.random() * uuids.length)]; - - // 从 HeroInfo 中获取基础属性,如果没找到则给一个兜底值 - const baseInfo = HeroInfo[uuid]; - const baseHp = baseInfo ? baseInfo.hp : 100; - const baseAp = baseInfo ? baseInfo.ap : 10; - - const finalHp = Math.max(1, Math.round(baseHp)); - const finalAp = Math.max(1, Math.round(baseAp)); - - monsters.push({ - uuid, - type, - hp: finalHp, - ap: finalAp, - affixes: [], // 词缀系统已移除,传入空数组 - isBoss: isBoss, - spawnIndex: i, - }); + let remaining = cfg.base_count; + if (cfg.boss_wave) { + monsters.push(this.makeBoss(waveNumber)); + remaining -= 1; } - return monsters; + // 5. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining + const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, waveNumber); + monsters.push(...squadMonsters); + + // 6. 应用全局 & 波次 Enchant 乘子 + const waveEnchantIds = enchants.map(e => e.id); + for (const m of monsters) { + m.hp = Math.max(1, Math.round(m.hp * globalScale.hp_mul * enchantHpMul)); + m.ap = Math.max(1, Math.round(m.ap * globalScale.ap_mul * enchantApMul)); + m.wave_enchants = waveEnchantIds.slice(); + } + + // 7. 应用个体 Elite(非 Boss 怪,每只最多 1 个) + const eliteBaseRate = cfg.elite_base_rate ?? Math.min(0.05 + waveNumber * 0.01, 0.30); + for (const m of monsters) { + if (!m.isBoss && Math.random() < eliteBaseRate * eliteRateMul) { + const elite = this.pickElite(); + this.applyElite(m, elite); + } + } + + // 8. 硬上限保护(理论不会触发,但防止配置错误) + return monsters.slice(0, 12); } + /** 重置引擎状态(保留接口兼容,当前无内部状态) */ reset(): void { - // 重置引擎状态 + // 无可变状态 } - /** - * 导出一个空函数,用于外部获取/预测某个波次的怪物数量 + /** + * 获取指定波次的怪物总数 * @param waveNumber 目标波数 - * @returns 预计生成的怪物总数 + * @returns 预计生成的怪物总数(base_count,受 12 上限约束) */ getWaveMonsterCount(waveNumber: number): number { if (waveNumber < 1) return 0; - if (TestModeConfig.enable) { return Math.max(1, TestModeConfig.spawnCount || 1); } - - let count = 0; - if (waveNumber === 1) { - count = 1; - } else if (waveNumber === 2) { - count = 2; - } else if (waveNumber === 3) { - count = 4; - } else if (waveNumber === 4) { - count = 6; - } else if (waveNumber === 5) { - count = 6; - } else { - // 注意:由于后续波次是 6-12 随机,这里可以返回一个平均值或特定的空逻辑 - // 根据需求,暂时返回固定的 6,或者保留外部自己实现逻辑的口子 - count = 6; - } - return Math.min(count, 12); + const cfg = WaveConfigs[Math.min(waveNumber, 30)]; + return Math.min(cfg.base_count, 12); } - // 向后兼容接口 + /** + * 获取波次槽位配置(向后兼容接口) + * 内部从 generateWave 反推,仅用于历史调用方 + * @param waveNumber 目标波数 + */ getWaveSlotConfig(waveNumber: number): IWaveSlot[] { const generated = this.generateWave(waveNumber); const slotMap = new Map(); @@ -222,7 +406,7 @@ export class RogueSpawningEngine { if (existing) { existing.count++; } else { - slotMap.set(m.type, { count: 1, affixes: m.affixes }); + slotMap.set(m.type, { count: 1, affixes: [] }); } } @@ -232,19 +416,202 @@ export class RogueSpawningEngine { ...(data.affixes.length > 0 ? { affixes: data.affixes } : {}), })); } + + // ======================== 私有生成子算法 ======================== + + /** 测试模式:完全绕过引擎逻辑 */ + 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), + affixes: [...TestModeConfig.affixes], + isBoss: false, + spawnIndex: i, + elite: TestModeConfig.testElite, + wave_enchants: TestModeConfig.testWaveEnchant ? [TestModeConfig.testWaveEnchant] : [], + testSkills: { + 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): 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, + affixes: [], // 兼容字段 + isBoss: true, + spawnIndex: 0, + wave_enchants: [], // 后续统一填充 + }; + } + + /** 生成普通怪物 */ + 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, + affixes: [], // 兼容字段 + isBoss: false, + spawnIndex, + wave_enchants: [], // 后续统一填充 + }; + } + + /** 抽取波次级强化(按权重抽 0~2 个,不重复) */ + private pickEnchants(pool?: string[]): WaveEnchant[] { + if (!pool || pool.length === 0) return []; + const enchants: WaveEnchant[] = []; + + // 第一个 Enchant: 70% 概率抽 1 个 + if (Math.random() < 0.7) { + const first = this.pickWeightedEnchant(pool); + if (first) enchants.push(first); + } + + // 第二个 Enchant: 30% 概率再抽 1 个(不重复) + if (Math.random() < 0.3 && pool.length > 1) { + const remaining = pool.filter(id => !enchants.some(e => e.id === id)); + const second = this.pickWeightedEnchant(remaining); + if (second) enchants.push(second); + } + + return enchants; + } + + /** 抽取个体强化(等概率随机 1 种) */ + private pickElite(): MonsterElite { + const candidates: MonsterElite[] = [ + MonsterElite.Elite, + MonsterElite.Berserk, + MonsterElite.Shield, + MonsterElite.Swift, + MonsterElite.Regen, + ]; + return candidates[Math.floor(Math.random() * candidates.length)]; + } + + /** 应用个体强化到怪物 */ + private applyElite(m: GeneratedMonster, elite: MonsterElite): void { + const def = MonsterEliteSet[elite]; + m.hp = Math.max(1, Math.round(m.hp * def.hp_mul)); + m.ap = Math.max(1, Math.round(m.ap * def.ap_mul)); + m.elite = elite; + } + + /** 按权重从小队 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); + } + + /** 按权重从强化 id 池抽 1 个强化 */ + private pickWeightedEnchant(pool: string[]): WaveEnchant | null { + const valid = pool.map(id => WaveEnchantLibrary[id]).filter(e => e); + 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 - affixes?: AffixType[] + type: number; + count: number; + /** @deprecated 已废弃,新接口不再使用 */ + affixes?: AffixType[]; } -/** +/** * 获取指定波次预计的怪物总数量 * @param waveNumber 目标波数 */ @@ -252,6 +619,7 @@ export function getWaveMonsterCount(waveNumber: number): number { return spawningEngine.getWaveMonsterCount(waveNumber); } +/** 获取波次槽位配置(向后兼容) */ export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] { return spawningEngine.getWaveSlotConfig(waveNumber); } @@ -262,6 +630,7 @@ export const DefaultWaveSlot: IWaveSlot[] = [ { type: MonType.Support, count: 5 }, ]; +/** 波次槽位配置代理(按需生成) */ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = new Proxy( {} as { [wave: number]: IWaveSlot[] }, {