/** * @file RogueConfig.ts * @description 肉鸽刷怪系统 v2 —— 三层程序化生成架构 * * 架构:蓝图模板(节奏) + 权重填充(内容) + 自适应微调(数值) * 主线 30 波 / 10 阶梯 / 每档 3 波(恢复→攀升→高潮) * 10 种怪物 + 8 种词缀 + 自适应难度 ±15% * 通关后可选无限模式(分层推进) * * GDD: /gdd/rogue-spawning.md */ // ======================== 怪物类型枚举 ======================== /** 怪物类型(10 种) */ export enum MonType { Melee = 0, Heavy = 1, Long = 2, Support = 3, Bomber = 4, Summoner = 5, Assassin = 6, Splitter = 7, MeleeBoss = 8, LongBoss = 9, } /** * 怪物类型名称映射(调试/日志用) * @example MonTypeName[MonType.Bomber] // => "自爆" */ export const MonTypeName: Record = { [MonType.Melee]: "近战", [MonType.Heavy]: "重型", [MonType.Long]: "远程", [MonType.Support]: "辅助", [MonType.Bomber]: "自爆", [MonType.Summoner]: "召唤师", [MonType.Assassin]: "刺客", [MonType.Splitter]: "分裂", [MonType.MeleeBoss]: "近战Boss", [MonType.LongBoss]: "远程Boss", } // ======================== 词缀类型枚举 ======================== /** 怪物词缀(8 种) */ export enum AffixType { Elite = 0, Berserk = 1, Shield = 2, Regen = 3, Swift = 4, Giant = 5, Chain = 6, SummonerA = 7, } /** * 词缀配置接口 * @property name - 词缀中文名(显示/调试用) * @property hpMultiplier - HP 倍率修饰,乘算到最终 HP 上(1.0 = 无变化) * @property apMultiplier - AP 倍率修饰,乘算到最终 AP 上(1.0 = 无变化) * @property cost - 该词缀占用的难度预算额外成本 * @property tierMin - 首次可出现的最低阶梯编号(Tier),低于此阶梯不会触发 * @property description - 词缀效果简述 */ export interface AffixConfig { name: string hpMultiplier: number apMultiplier: number cost: number tierMin: number description: string } /** * 词缀详细配置表 * key = AffixType 枚举值,value = 对应词缀的完整配置 * @see AffixConfig 字段说明 */ export const AffixConfigs: Record = { [AffixType.Elite]: { name: "精英", hpMultiplier: 1.5, apMultiplier: 1.3, cost: 20, tierMin: 5, description: "+50% HP, +30% AP", }, [AffixType.Berserk]: { name: "狂暴", hpMultiplier: 1.0, apMultiplier: 1.0, cost: 15, tierMin: 5, description: "攻速 ×1.5 (行为层实现)", }, [AffixType.Shield]: { name: "护盾", hpMultiplier: 1.0, apMultiplier: 1.0, cost: 25, tierMin: 6, description: "开局带 20% HP 伤害吸收盾", }, [AffixType.Regen]: { name: "再生", hpMultiplier: 1.0, apMultiplier: 1.0, cost: 20, tierMin: 7, description: "每秒回复 2% HP", }, [AffixType.Swift]: { name: "疾速", hpMultiplier: 1.0, apMultiplier: 1.0, cost: 10, tierMin: 7, description: "移速 ×2", }, [AffixType.Giant]: { name: "巨型", hpMultiplier: 2.0, apMultiplier: 1.5, cost: 30, tierMin: 8, description: "×2 体型, +100% HP, +50% AP", }, [AffixType.Chain]: { name: "连锁", hpMultiplier: 1.0, apMultiplier: 1.0, cost: 20, tierMin: 9, description: "攻击附带 50% 溅射伤害", }, [AffixType.SummonerA]: { name: "召唤", hpMultiplier: 1.0, apMultiplier: 1.0, cost: 25, tierMin: 10, description: "每 8 秒召唤 1 个小怪", }, } /** * 词缀互斥组:同组内的词缀最多只能出现一个 * 当随机到互斥冲突时,按 AffixPriority 保留优先级更高的词缀 * @example [AffixType.Giant, AffixType.Swift] — 巨型与疾速不能同时出现 */ export const AffixMutualExclusion: AffixType[][] = [ [AffixType.Giant, AffixType.Swift], [AffixType.Regen, AffixType.Shield], ] /** * 词缀优先级(从高到低) * 互斥冲突时保留此数组中靠前的词缀,丢弃靠后的 */ export const AffixPriority: AffixType[] = [ AffixType.Shield, AffixType.Regen, AffixType.Giant, AffixType.Swift, AffixType.Elite, AffixType.Berserk, AffixType.Chain, AffixType.SummonerA, ] // ======================== 蓝图模板类型 ======================== /** 模板类型 */ export enum TemplateType { REST = 0, NORMAL = 1, MIXED = 2, ELITE = 3, BOSS = 4, } /** * 模板类型修正系数 * 乘算到 base_budget 上,控制不同类型模板的预算总量 * - REST: 0.5(恢复波预算减半) * - NORMAL: 1.0(标准预算) * - MIXED: 1.2(混合波预算 +20%) * - ELITE: 0.8(精英波怪少但强,预算略低) * - BOSS: 1.5(Boss 波预算 +50%) */ export const TemplateModifier: Record = { [TemplateType.REST]: 0.5, [TemplateType.NORMAL]: 1.0, [TemplateType.MIXED]: 1.2, [TemplateType.ELITE]: 0.8, [TemplateType.BOSS]: 1.5, } // ======================== 怪物 UUID 池 ======================== /** * 各怪物类型对应的 UUID 列表 * 生成怪物时从对应数组中随机抽取一个 UUID,用于匹配 heroSet.ts 中的怪物配置 * UUID 编号段:6001-6006(兽人)| 6101-6105(亡灵)| 6201-6205(新类型) */ export const MonList: Record = { [MonType.Melee]: [6001, 6002], [MonType.Heavy]: [6003, 6103], [MonType.Long]: [6004, 6102], [MonType.Support]: [6005], [MonType.Bomber]: [6201, 6202], [MonType.Summoner]: [6203], [MonType.Assassin]: [6204], [MonType.Splitter]: [6205], [MonType.MeleeBoss]: [6006, 6105], [MonType.LongBoss]: [6104], } // ======================== 怪物基础属性 & 成本 ======================== /** * 怪物基础属性接口 * 定义怪物在 Tier 1(无词缀、adaptive=1.0)时的原始属性 * @property hp - 基础生命值(final_hp = hp × tier_multiplier × affix_hp_mul × adaptive_factor) * @property ap - 基础攻击力(final_ap = ap × tier_multiplier × affix_ap_mul × adaptive_factor) * @property cost - 在难度预算中的成本,决定每波能出多少只 * @property isBoss - 是否为 Boss 类型(影响词缀叠加上限和 UI 显示) */ export interface MonsterBaseStats { hp: number ap: number cost: number isBoss: boolean } /** * 怪物基础属性表 * key = MonType 枚举值,value = 该类型在 Tier 1 时的基础属性 * @see MonsterBaseStats 字段说明 */ export const MonsterStats: Record = { [MonType.Melee]: { hp: 120, ap: 12, cost: 30, isBoss: false }, [MonType.Heavy]: { hp: 350, ap: 30, cost: 50, isBoss: false }, [MonType.Long]: { hp: 80, ap: 45, cost: 40, isBoss: false }, [MonType.Support]: { hp: 80, ap: 20, cost: 50, isBoss: false }, [MonType.Bomber]: { hp: 60, ap: 80, cost: 35, isBoss: false }, [MonType.Summoner]: { hp: 100, ap: 15, cost: 60, isBoss: false }, [MonType.Assassin]: { hp: 90, ap: 55, cost: 45, isBoss: false }, [MonType.Splitter]: { hp: 150, ap: 20, cost: 55, isBoss: false }, [MonType.MeleeBoss]: { hp: 1500, ap: 20, cost: 200, isBoss: true }, [MonType.LongBoss]: { hp: 350, ap: 30, cost: 200, isBoss: true }, } // ======================== 阶梯(Tier)配置 ======================== /** * 阶梯(Tier)配置接口 * @property multiplier - 属性倍率,所有怪物的 HP/AP 乘以此值(T1=1.0, T10=5.5) * @property budget - 该档基础难度预算,怪物成本总和的上限 * @property availableTypes - 该档允许生成的怪物类型列表(未列入的类型不会出现) * @property isBossTier - 该档第 3 波是否为 Boss 波 */ export interface TierConfig { multiplier: number budget: number availableTypes: MonType[] isBossTier: boolean } /** Boss 出现的 Tier 集合(MiniBoss 或 MajorBoss) */ const BOSS_TIERS = new Set([2, 4, 5, 7, 9, 10]) /** MajorBoss(高难度 Boss)出现的 Tier 集合 */ const MAJOR_BOSS_TIERS = new Set([5, 10]) /** * 10 阶梯配置表 * key = 阶梯编号 1-10,value = 该阶梯的完整配置 * 主线 30 波映射:wave 1-3 → T1, wave 4-6 → T2, ..., wave 28-30 → T10 */ export const TierConfigs: Record = { 1: { multiplier: 1.0, budget: 100, availableTypes: [MonType.Melee], isBossTier: false }, 2: { multiplier: 1.3, budget: 150, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long], isBossTier: true }, 3: { multiplier: 1.6, budget: 200, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long], isBossTier: false }, 4: { multiplier: 1.9, budget: 260, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support], isBossTier: true }, 5: { multiplier: 2.3, budget: 340, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber], isBossTier: true }, 6: { multiplier: 2.8, budget: 440, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin], isBossTier: false }, 7: { multiplier: 3.3, budget: 560, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin, MonType.Summoner, MonType.Splitter], isBossTier: true }, 8: { multiplier: 3.9, budget: 700, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin, MonType.Summoner, MonType.Splitter], isBossTier: false }, 9: { multiplier: 4.6, budget: 860, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin, MonType.Summoner, MonType.Splitter], isBossTier: true }, 10: { multiplier: 5.5, budget: 1050, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin, MonType.Summoner, MonType.Splitter], isBossTier: true }, } /** * 获取指定阶梯的配置 * 支持 Tier 1-10(查表)和 Tier 11+(无限模式,递推计算:T(n) = T(n-1) × 1.2) * * @param tier - 阶梯编号(1-10 主线,11+ 无限模式) * @returns TierConfig 该阶梯的完整配置 * * @example * getTierConfig(1) // { multiplier: 1.0, budget: 100, ... } * getTierConfig(11) // { multiplier: 6.6, budget: 1260, ... } (5.5 × 1.2) */ export function getTierConfig(tier: number): TierConfig { if (TierConfigs[tier]) return TierConfigs[tier] // 无限模式:T(n) = T(n-1) × 1.2 const prev = getTierConfig(tier - 1) const growthRate = InfiniteModeConfig.tierGrowthRate const allTypes: MonType[] = [ MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin, MonType.Summoner, MonType.Splitter, ] return { multiplier: Math.round(prev.multiplier * growthRate * 100) / 100, budget: Math.round(prev.budget * growthRate), availableTypes: allTypes, isBossTier: true, // 无限模式每层都有 Boss } } // ======================== 词缀概率配置 ======================== /** * 每个 Tier 的基础词缀触发概率(0.0-1.0) * T1-T4: 0%(教学期无词缀) * T5: 10%, T6: 15%, T7: 25%, T8: 30%, T9: 40%, T10: 50% * 最终概率 = baseAffixChance × roleMultiplier */ export const BaseAffixChance: Record = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0.10, 6: 0.15, 7: 0.25, 8: 0.30, 9: 0.40, 10: 0.50, } /** * 角色类型对词缀概率的倍率 * final_chance = baseAffixChance[tier] × roleMultiplier * - normal: 1.0x — 普通怪 * - miniBoss: 2.0x — MiniBoss(T2/4/7/9 的 Boss) * - majorBoss: 3.0x — MajorBoss(T5/10 的 Boss) */ export const AffixRoleMultiplier = { normal: 1.0, miniBoss: 2.0, majorBoss: 3.0, } /** * 词缀叠加上限(按角色类型) * - normal: 1 — 普通怪最多 1 个词缀 * - miniBoss: 2 — MiniBoss 最多 2 个词缀 * - majorBoss: 3 — MajorBoss/FinalBoss 最多 3 个词缀 */ export const AffixStackLimit = { normal: 1, miniBoss: 2, majorBoss: 3, } // ======================== 蓝图模板池 ======================== /** * 蓝图模板的怪物槽位定义 * @property typePool - 候选怪物类型数组,生成时从中随机选一个(等概率) * @property countMin - 该槽位最少生成的怪物数量 * @property countMax - 该槽位最多生成的怪物数量(实际在 [min, max] 范围内随机) * @property weight - 槽位权重(保留扩展用,当前各槽位按顺序填充) * @property forceAffix - 若为 true,该槽位中的怪物必定触发词缀判定(至少 50% 概率) */ export interface BlueprintSlot { typePool: MonType[] countMin: number countMax: number weight: number forceAffix?: boolean } /** * 蓝图模板定义 * @property id - 模板唯一标识(如 "R1", "N2", "B3", "TUTORIAL") * @property type - 模板类型(REST/NORMAL/MIXED/ELITE/BOSS),决定模板修正系数 * @property tierMin - 首次可出现的最低 Tier(低于此 Tier 不会选到此模板) * @property slots - 怪物槽位数组,按顺序依次填充 * @property allowAffix - 是否允许对槽位中的怪物进行词缀判定 */ export interface BlueprintTemplate { id: string type: TemplateType tierMin: number slots: BlueprintSlot[] allowAffix: boolean } /** * 蓝图模板池 * 包含所有预定义模板,按类型分为 REST/NORMAL/MIXED/ELITE/BOSS 五类 * 生成引擎根据当前 Tier 和波内位置从中筛选并随机抽取 */ export const BlueprintTemplates: BlueprintTemplate[] = [ // ---- REST 类 ---- { id: "R1", type: TemplateType.REST, tierMin: 1, allowAffix: false, slots: [{ typePool: [MonType.Melee], countMin: 1, countMax: 2, weight: 1.0 }] }, { id: "R2", type: TemplateType.REST, tierMin: 2, allowAffix: false, slots: [{ typePool: [MonType.Melee, MonType.Heavy], countMin: 1, countMax: 3, weight: 1.0 }] }, { id: "R3", type: TemplateType.REST, tierMin: 5, allowAffix: false, slots: [{ typePool: [MonType.Melee, MonType.Long], countMin: 2, countMax: 3, weight: 1.0 }] }, // ---- NORMAL 类 ---- { id: "N1", type: TemplateType.NORMAL, tierMin: 1, allowAffix: false, slots: [{ typePool: [MonType.Melee], countMin: 2, countMax: 4, weight: 1.0 }] }, { id: "N2", type: TemplateType.NORMAL, tierMin: 2, allowAffix: false, slots: [ { typePool: [MonType.Melee], countMin: 1, countMax: 3, weight: 0.6 }, { typePool: [MonType.Long, MonType.Heavy], countMin: 1, countMax: 2, weight: 0.4 }, ] }, { id: "N3", type: TemplateType.NORMAL, tierMin: 4, allowAffix: true, slots: [ { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 3, weight: 0.5 }, { typePool: [MonType.Long], countMin: 1, countMax: 2, weight: 0.3 }, { typePool: [MonType.Support], countMin: 1, countMax: 1, weight: 0.2 }, ] }, { id: "N4", type: TemplateType.NORMAL, tierMin: 6, allowAffix: true, slots: [ { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 4, weight: 0.4 }, { typePool: [MonType.Long, MonType.Assassin], countMin: 1, countMax: 3, weight: 0.3 }, { typePool: [MonType.Support, MonType.Bomber], countMin: 1, countMax: 2, weight: 0.3 }, ] }, // ---- MIXED 类 ---- { id: "M1", type: TemplateType.MIXED, tierMin: 3, allowAffix: true, slots: [ { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 3, weight: 0.4 }, { typePool: [MonType.Long], countMin: 1, countMax: 2, weight: 0.3 }, { typePool: [MonType.Support], countMin: 1, countMax: 1, weight: 0.3 }, ] }, { id: "M2", type: TemplateType.MIXED, tierMin: 5, allowAffix: true, slots: [ { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 4, weight: 0.3 }, { typePool: [MonType.Long, MonType.Assassin], countMin: 2, countMax: 3, weight: 0.3 }, { typePool: [MonType.Bomber], countMin: 1, countMax: 2, weight: 0.2 }, { typePool: [MonType.Support], countMin: 1, countMax: 1, weight: 0.2 }, ] }, { id: "M3", type: TemplateType.MIXED, tierMin: 7, allowAffix: true, slots: [ { typePool: [MonType.Melee, MonType.Heavy], countMin: 3, countMax: 4, weight: 0.3 }, { typePool: [MonType.Long, MonType.Assassin], countMin: 2, countMax: 3, weight: 0.25 }, { typePool: [MonType.Summoner, MonType.Splitter], countMin: 1, countMax: 2, weight: 0.25 }, { typePool: [MonType.Bomber, MonType.Support], countMin: 1, countMax: 2, weight: 0.2 }, ] }, // ---- ELITE 类 ---- { id: "E1", type: TemplateType.ELITE, tierMin: 5, allowAffix: true, slots: [ { typePool: [MonType.Melee, MonType.Heavy], countMin: 1, countMax: 2, weight: 0.5, forceAffix: true }, { typePool: [MonType.Long, MonType.Assassin], countMin: 1, countMax: 2, weight: 0.5, forceAffix: true }, ] }, { id: "E2", type: TemplateType.ELITE, tierMin: 7, allowAffix: true, slots: [ { typePool: [MonType.Heavy], countMin: 1, countMax: 2, weight: 0.3, forceAffix: true }, { typePool: [MonType.Assassin, MonType.Splitter], countMin: 1, countMax: 2, weight: 0.4, forceAffix: true }, { typePool: [MonType.Bomber], countMin: 1, countMax: 2, weight: 0.3, forceAffix: true }, ] }, // ---- BOSS 类 ---- { id: "B1", type: TemplateType.BOSS, tierMin: 2, allowAffix: true, slots: [ { typePool: [MonType.MeleeBoss], countMin: 1, countMax: 1, weight: 1.0 }, { typePool: [MonType.Melee], countMin: 2, countMax: 3, weight: 0.6 }, { typePool: [MonType.Long], countMin: 0, countMax: 2, weight: 0.4 }, ] }, { id: "B2", type: TemplateType.BOSS, tierMin: 4, allowAffix: true, slots: [ { typePool: [MonType.MeleeBoss], countMin: 1, countMax: 1, weight: 1.0 }, { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 3, weight: 0.5 }, { typePool: [MonType.Long, MonType.Support], countMin: 1, countMax: 2, weight: 0.5 }, ] }, { id: "B3", type: TemplateType.BOSS, tierMin: 5, allowAffix: true, slots: [ { typePool: [MonType.MeleeBoss, MonType.LongBoss], countMin: 1, countMax: 1, weight: 1.0 }, { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 4, weight: 0.4 }, { typePool: [MonType.Long, MonType.Assassin], countMin: 1, countMax: 3, weight: 0.3 }, { typePool: [MonType.Bomber, MonType.Support], countMin: 1, countMax: 2, weight: 0.3 }, ] }, { id: "B4", type: TemplateType.BOSS, tierMin: 7, allowAffix: true, slots: [ { typePool: [MonType.MeleeBoss, MonType.LongBoss], countMin: 1, countMax: 1, weight: 1.0 }, { typePool: [MonType.Heavy], countMin: 2, countMax: 3, weight: 0.3 }, { typePool: [MonType.Assassin, MonType.Splitter], countMin: 1, countMax: 2, weight: 0.3 }, { typePool: [MonType.Summoner, MonType.Support], countMin: 1, countMax: 2, weight: 0.2 }, { typePool: [MonType.Bomber], countMin: 1, countMax: 2, weight: 0.2 }, ] }, // ---- 教程专用 ---- { id: "TUTORIAL", type: TemplateType.NORMAL, tierMin: 1, allowAffix: false, slots: [{ typePool: [MonType.Melee], countMin: 1, countMax: 1, weight: 1.0 }] }, ] // ======================== 自适应难度配置 ======================== /** * 自适应难度配置 * @property factorMin - adaptive_factor 下限(0.85,最多降 15%) * @property factorMax - adaptive_factor 上限(1.15,最多加 15%) * @property deltaPerWave - 每波调整步长(0.03) * @property targetClearTime - 目标通关时间(秒),低于此视为"玩家太强" * @property strongThreshold - 英雄存活率高于此视为"太强"(0.8 = 80% 存活) * @property weakThreshold - 英雄存活率低于此视为"太弱"(0.3 = 30% 存活) * @property maxConsecutiveDirection - 允许连续同方向调整的最大波数 * @property antiDriftDelta - 反漂移时的反向微调量 */ export const AdaptiveConfig = { factorMin: 0.85, factorMax: 1.15, deltaPerWave: 0.03, targetClearTime: 15.0, strongThreshold: 0.8, weakThreshold: 0.3, maxConsecutiveDirection: 5, antiDriftDelta: 0.01, } // ======================== 无限模式配置 ======================== /** * 无限模式配置 * @property wavesPerLayer - 每层的波数(4 波:REST→NORMAL→MIXED→BOSS) * @property tierGrowthRate - 属性倍率递增率(每层 ×1.2) * @property bossAffixPerLayer - Boss 每层额外获得的词缀数 * @property megaBossInterval - 每 N 层出现一次 Mega Boss */ export const InfiniteModeConfig = { wavesPerLayer: 4, tierGrowthRate: 1.2, bossAffixPerLayer: 1, megaBossInterval: 3, } // ======================== 向后兼容接口 ======================== /** * 向后兼容:波次占位配置 * @property type - 怪物类型编号(对应 MonType 枚举值) * @property count - 该类型怪物的数量 * @property affixes - 可选,该组怪物的词缀列表(新系统新增字段,旧代码可忽略) */ export interface IWaveSlot { type: number count: number affixes?: AffixType[] } /** 全局刷怪强度偏差系数(保留向后兼容,新系统中固定为 1.0) */ export const SpawnPowerBias = 1.0 // ======================== 生成结果接口 ======================== /** * 最终生成的怪物实例 — 战斗系统的输入数据 * * 战斗系统收到此数组后,按 spawnIndex 顺序依次在战场上放置怪物。 * 需要通过 uuid 去 heroSet.ts 中查找对应的完整怪物配置(路径、技能、速度等)。 * hp/ap 是已经过阶梯成长、词缀修饰、自适应微调后的最终值,直接使用即可。 * * @property uuid - 怪物 UUID,对应 heroSet.ts 中 HeroInfo 的 key(如 6001, 6203) * @property type - 怪物类型(MonType 枚举),用于判断近战/远程/Boss 等行为模式 * @property hp - 最终生命值(= base_hp × tier_multiplier × affix_hp_mul × adaptive_factor) * @property ap - 最终攻击力(= base_ap × tier_multiplier × affix_ap_mul × adaptive_factor) * @property affixes - 词缀数组(空数组表示无词缀)。战斗系统需实现各词缀的行为效果: * - Elite/Berserk/Giant: 属性已在 hp/ap 中体现 * - Shield: 需在战斗中实现伤害吸收盾 * - Regen: 需在战斗中实现每秒回血 * - Swift: 需在战斗中实现移速翻倍 * - Chain: 需在战斗中实现溅射伤害 * - SummonerA: 需在战斗中实现定期召唤 * @property isBoss - 是否为 Boss(影响 UI 显示:Boss 血条独立显示在屏幕顶部) * @property spawnIndex - 在该波次中的生成顺序(0 起始),决定战场上的排列位置 * 排列规则:spawnIndex=0 在最前(X 最小),后续依次向后间隔排列 */ export interface GeneratedMonster { uuid: number type: MonType hp: number ap: number affixes: AffixType[] isBoss: boolean spawnIndex: number } // ======================== 生成引擎 ======================== /** * 肉鸽刷怪引擎 — 核心生成器 * * 职责:根据波次号(或无限模式层号),程序化生成该波的怪物列表。 * 每局游戏创建一个实例,通过 reset() 重置,跨波次保持自适应状态。 * * 使用流程: * 1. 游戏开始 → `const engine = new RogueSpawningEngine()` 或使用全局 `spawningEngine` * 2. 每波开始 → `const monsters = engine.generateWave(waveNumber)` * 3. 将 monsters 传给战斗系统生成怪物 * 4. 战斗结束 → `engine.updateAdaptive(heroesAliveRatio, clearTime)` * 5. 下一波 → 回到步骤 2 * 6. 新一局 → `engine.reset()` * * 对接方: * - 战斗系统:消费 `GeneratedMonster[]` * - UI 系统:读取 `monsters[n].isBoss`、`monsters[n].affixes` 显示 Boss 血条和词缀标识 * - 分数系统:根据 `monsters[n].isBoss` 统计 Boss 击杀 */ export class RogueSpawningEngine { private adaptiveFactor = 1.0 private consecutiveDirection = 0 private lastDirection = 0 // -1 降难, 0 维持, 1 加难 private lastTemplateId = "" private lastTemplateType: TemplateType | null = null private consecutiveTemplateCount = 0 /** * 生成指定波次的怪物列表(主线 1-30 波) * * 内部流程: * 1. 根据 waveNumber 计算 tier 和 waveInTier * 2. W1 特殊处理(固定教程模板:1 个 Melee) * 3. 根据 waveInTier 和 isBossTier 选择蓝图模板 * 4. 计算难度预算 = base_budget × template_modifier × adaptive_factor * 5. 按模板槽位填充怪物、应用词缀、计算最终属性 * * @param waveNumber - 波次编号(1-30) * 1-3 → Tier 1, 4-6 → Tier 2, ..., 28-30 → Tier 10 * < 1 返回空数组 * * @returns GeneratedMonster[] 该波次的怪物实例数组,按 spawnIndex 排列。 * 战斗系统按此顺序在战场上依次放置怪物。 * 每次调用结果不同(随机生成),如需固定结果请在同一次调用中缓存。 * * @example * // 生成第 1 波(教程:1 个 Melee) * const w1 = engine.generateWave(1) * // w1 = [{ uuid: 6001, type: 0, hp: 120, ap: 12, affixes: [], isBoss: false, spawnIndex: 0 }] * * // 生成第 6 波(Tier 2 Boss 波) * const w6 = engine.generateWave(6) * // w6 包含 1 个 MeleeBoss + 若干小怪 */ generateWave(waveNumber: number): GeneratedMonster[] { if (waveNumber < 1) return [] const tier = Math.ceil(waveNumber / 3) const waveInTier = ((waveNumber - 1) % 3) + 1 // 1, 2, or 3 // 特殊:Tier 1 Wave 1 = 教程 if (waveNumber === 1) { return this.spawnFromTemplate( BlueprintTemplates.find(t => t.id === "TUTORIAL")!, 1, 1 ) } const isBossWave = TierConfigs[tier]?.isBossTier && waveInTier === 3 const template = this.selectTemplate(tier, waveInTier, isBossWave) const tierConfig = getTierConfig(tier) const budget = Math.round( tierConfig.budget * TemplateModifier[template.type] * this.adaptiveFactor ) return this.spawnFromTemplate(template, tier, budget) } /** * 生成无限模式指定层的指定波 * * 无限模式结构:每层 4 波(REST → NORMAL → MIXED → BOSS),Tier 从 11 开始递增。 * 属性倍率按 T(n) = T(n-1) × 1.2 递推,无上限。 * * @param layer - 层编号(从 1 开始) * Tier = 10 + layer(layer=1 → Tier 11, multiplier=6.6) * @param waveInLayer - 层内波次编号(1-4) * 1: REST(恢复波) * 2: NORMAL(标准波) * 3: MIXED(混合波) * 4: BOSS(Boss 波,Boss 每层额外获得词缀) * * @returns GeneratedMonster[] 该波的怪物实例数组 * * @example * // 无限模式第 1 层第 4 波(Boss 波,Tier 11) * const boss = engine.generateInfiniteWave(1, 4) * // boss 包含 1 个 Boss + 若干小怪,属性倍率 = 6.6x */ generateInfiniteWave(layer: number, waveInLayer: number): GeneratedMonster[] { const tier = 10 + layer const tierConfig = getTierConfig(tier) let templateType: TemplateType // 无限模式每层 4 波:REST → NORMAL → MIXED → BOSS switch (waveInLayer) { case 1: templateType = TemplateType.REST; break case 2: templateType = TemplateType.NORMAL; break case 3: templateType = TemplateType.MIXED; break case 4: templateType = TemplateType.BOSS; break default: templateType = TemplateType.NORMAL; break } const templates = BlueprintTemplates.filter(t => t.type === templateType && t.tierMin <= 10 ) const template = this.pickRandomTemplate(templates) const budget = Math.round( tierConfig.budget * TemplateModifier[template.type] * this.adaptiveFactor ) return this.spawnFromTemplate(template, tier, budget) } /** * 根据战斗结果更新自适应难度系数 * * 每波战斗结束后调用一次。引擎内部维护 adaptiveFactor(0.85-1.15), * 该系数会乘算到后续所有波的怪物 HP/AP 上。 * * 调整规则: * - 玩家太强(存活率 ≥ 80% 且 通关时间 < 15s)→ adaptiveFactor += 0.03 * - 玩家太弱(存活率 ≤ 30%) → adaptiveFactor -= 0.03 * - 其他情况 → 不调整 * - 连续 5 波同方向 → 强制反向微调 0.01(反漂移) * * @param heroesAliveRatio - 本波结束时英雄存活比例(0.0-1.0) * = 存活英雄数 / 总英雄数 * 3 英雄中存活 2 个 → 0.67 * 3 英雄中存活 3 个 → 1.0 * @param clearTime - 本波通关时间(秒),从怪物全部生成到全部击败的时长 * < 15s 视为"秒杀",配合高存活率判定玩家过强 * * @example * // 3 英雄全部存活,5 秒通关 → 判定为"太强",adaptiveFactor += 0.03 * engine.updateAdaptive(1.0, 5.0) * * // 仅 1 英雄存活 → 判定为"太弱",adaptiveFactor -= 0.03 * engine.updateAdaptive(0.33, 30.0) */ updateAdaptive(heroesAliveRatio: number, clearTime: number): void { let direction = 0 let delta = 0 if (heroesAliveRatio >= AdaptiveConfig.strongThreshold && clearTime < AdaptiveConfig.targetClearTime) { direction = 1 delta = AdaptiveConfig.deltaPerWave } else if (heroesAliveRatio <= AdaptiveConfig.weakThreshold) { direction = -1 delta = -AdaptiveConfig.deltaPerWave } // 反漂移:连续同方向超过阈值时强制反向 if (direction !== 0 && direction === this.lastDirection) { this.consecutiveDirection++ if (this.consecutiveDirection >= AdaptiveConfig.maxConsecutiveDirection) { delta = direction === 1 ? -AdaptiveConfig.antiDriftDelta : AdaptiveConfig.antiDriftDelta this.consecutiveDirection = 0 } } else { this.consecutiveDirection = direction !== 0 ? 1 : 0 } this.lastDirection = direction this.adaptiveFactor = Math.max( AdaptiveConfig.factorMin, Math.min(AdaptiveConfig.factorMax, this.adaptiveFactor + delta) ) } /** * 获取当前自适应难度系数 * @returns number 当前 adaptive_factor 值(0.85-1.15) * 1.0 = 无调整,>1.0 = 更难,<1.0 = 更简单 */ getAdaptiveFactor(): number { return this.adaptiveFactor } /** * 重置引擎状态(新一局游戏开始时调用) * 将 adaptiveFactor 恢复为 1.0,清空模板连续计数和方向记录 */ reset(): void { this.adaptiveFactor = 1.0 this.consecutiveDirection = 0 this.lastDirection = 0 this.lastTemplateId = "" this.lastTemplateType = null this.consecutiveTemplateCount = 0 } // ---- 内部方法 ---- private selectTemplate( tier: number, waveInTier: number, isBossWave: boolean ): BlueprintTemplate { let targetType: TemplateType if (isBossWave) { targetType = TemplateType.BOSS } else if (waveInTier === 1) { targetType = TemplateType.REST } else if (waveInTier === 3) { targetType = TemplateType.MIXED } else { // Wave 2: 从 NORMAL/MIXED/ELITE 中随机 const candidates = [TemplateType.NORMAL, TemplateType.MIXED, TemplateType.ELITE] targetType = candidates[Math.floor(Math.random() * candidates.length)] } // 同一模板被连续抽取 3 次:强制下一次从不同类型模板池中抽取 if (this.consecutiveTemplateCount >= 3 && this.lastTemplateType !== null) { if (targetType === this.lastTemplateType) { const candidates = [TemplateType.REST, TemplateType.NORMAL, TemplateType.MIXED, TemplateType.ELITE, TemplateType.BOSS].filter(t => t !== this.lastTemplateType) targetType = candidates[Math.floor(Math.random() * candidates.length)] } } let templates = BlueprintTemplates.filter(t => t.type === targetType && t.tierMin <= tier && t.id !== "TUTORIAL" ) if (templates.length === 0) { templates = BlueprintTemplates.filter(t => t.type === TemplateType.NORMAL && t.tierMin <= tier && t.id !== "TUTORIAL" ) } return this.pickRandomTemplate(templates) } private pickRandomTemplate(templates: BlueprintTemplate[]): BlueprintTemplate { const idx = Math.floor(Math.random() * templates.length) const picked = templates[idx] if (picked.id === this.lastTemplateId) { this.consecutiveTemplateCount++ } else { this.consecutiveTemplateCount = 1 this.lastTemplateId = picked.id this.lastTemplateType = picked.type } return picked } private spawnFromTemplate( template: BlueprintTemplate, tier: number, budget: number ): GeneratedMonster[] { const monsters: GeneratedMonster[] = [] let remainingBudget = budget let spawnIndex = 0 for (const slot of template.slots) { const count = this.randomInt(slot.countMin, slot.countMax) for (let i = 0; i < count; i++) { // 从 typePool 中按权重随机选一个类型 const type = slot.typePool[Math.floor(Math.random() * slot.typePool.length)] const stats = MonsterStats[type] const tierConfig = getTierConfig(tier) // 计算成本 let cost = stats.cost let affixes: AffixType[] = [] // 生成词缀 if (template.allowAffix && tier >= 5) { affixes = this.generateAffixes(type, tier, slot.forceAffix ?? false) cost += affixes.reduce((sum, a) => sum + AffixConfigs[a].cost, 0) } // 检查预算 if (cost > remainingBudget && monsters.length > 0) continue remainingBudget -= cost // 计算最终属性 const affixHpMul = 1.0 + affixes.reduce( (sum, a) => sum + (AffixConfigs[a].hpMultiplier - 1.0), 0 ) const affixApMul = 1.0 + affixes.reduce( (sum, a) => sum + (AffixConfigs[a].apMultiplier - 1.0), 0 ) const finalHp = Math.max(1, Math.round( stats.hp * tierConfig.multiplier * affixHpMul * this.adaptiveFactor )) const finalAp = Math.max(1, Math.round( stats.ap * tierConfig.multiplier * affixApMul * this.adaptiveFactor )) // 从 UUID 池中随机选一个 const uuids = MonList[type] || MonList[MonType.Melee] const uuid = uuids[Math.floor(Math.random() * uuids.length)] monsters.push({ uuid, type, hp: finalHp, ap: finalAp, affixes, isBoss: stats.isBoss, spawnIndex, }) spawnIndex++ } } // 预算利用率检查 (目标 >= 70%) let totalCost = budget - remainingBudget if (budget > 0 && totalCost / budget < 0.7) { const tierConfig = getTierConfig(tier) const type = MonType.Melee const stats = MonsterStats[type] while (remainingBudget >= stats.cost) { const finalHp = Math.max(1, Math.round(stats.hp * tierConfig.multiplier * this.adaptiveFactor)) const finalAp = Math.max(1, Math.round(stats.ap * tierConfig.multiplier * this.adaptiveFactor)) const uuids = MonList[type] const uuid = uuids[Math.floor(Math.random() * uuids.length)] monsters.push({ uuid, type, hp: finalHp, ap: finalAp, affixes: [], isBoss: false, spawnIndex }) spawnIndex++ remainingBudget -= stats.cost totalCost += stats.cost } } // 保底:至少 1 个 Melee if (monsters.length === 0) { const tierConfig = getTierConfig(tier) monsters.push({ uuid: 6001, type: MonType.Melee, hp: Math.max(1, Math.round(120 * tierConfig.multiplier * this.adaptiveFactor)), ap: Math.max(1, Math.round(12 * tierConfig.multiplier * this.adaptiveFactor)), affixes: [], isBoss: false, spawnIndex: 0, }) } return monsters } private generateAffixes( type: MonType, tier: number, forceAffix: boolean ): AffixType[] { const stats = MonsterStats[type] const maxAffixes = stats.isBoss ? (MAJOR_BOSS_TIERS.has(tier) ? AffixStackLimit.majorBoss : AffixStackLimit.miniBoss) : AffixStackLimit.normal // 计算触发概率 let chance = (BaseAffixChance[tier] ?? 0.5) * (stats.isBoss ? (MAJOR_BOSS_TIERS.has(tier) ? AffixRoleMultiplier.majorBoss : AffixRoleMultiplier.miniBoss) : AffixRoleMultiplier.normal) if (forceAffix) chance = Math.max(chance, 0.5) chance = Math.min(chance, 1.0) // 可用词缀列表(过滤 tierMin) const available = (Object.values(AffixType) as AffixType[]).filter( a => typeof a === "number" && AffixConfigs[a].tierMin <= tier ) if (available.length === 0) return [] const result: AffixType[] = [] for (let i = 0; i < maxAffixes; i++) { if (Math.random() >= chance) continue const candidates = available.filter(a => !result.includes(a)) if (candidates.length === 0) continue result.push(candidates[Math.floor(Math.random() * candidates.length)]) } // 互斥冲突时,按 AffixPriority 保留优先级更高的词缀 for (const group of AffixMutualExclusion) { const present = result.filter(a => group.includes(a)) if (present.length > 1) { present.sort((a, b) => AffixPriority.indexOf(a) - AffixPriority.indexOf(b)) const toRemove = present.slice(1) for (const r of toRemove) { const idx = result.indexOf(r) if (idx !== -1) result.splice(idx, 1) } } } return result } private randomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min } // ---- 向后兼容 ---- /** * 向后兼容:获取指定波次的 IWaveSlot[](汇总后的类型+数量,不含最终属性) * * 将 generateWave() 的结果按怪物类型合并,返回简化的 [{type, count}] 格式。 * 注意:此方法不返回 HP/AP 值,调用方需自行处理属性计算或改用 generateWave()。 * * @param waveNumber - 波次编号(1-30) * @returns IWaveSlot[] 按怪物类型合并后的数组 * - type: MonType 枚举值 * - count: 该类型的怪物总数 * - affixes: 该组怪物的词缀列表(仅首个同类型怪物的词缀) * * @example * engine.getWaveSlotConfig(6) * // [{ type: 8, count: 1 }, { type: 0, count: 2 }, { type: 2, count: 1 }] * // = 1 个 MeleeBoss + 2 个 Melee + 1 个 Long */ getWaveSlotConfig(waveNumber: number): IWaveSlot[] { const generated = this.generateWave(waveNumber) const slotMap = new Map() for (const m of generated) { const existing = slotMap.get(m.type) if (existing) { existing.count++ } else { slotMap.set(m.type, { count: 1, affixes: m.affixes }) } } return Array.from(slotMap.entries()).map(([type, data]) => ({ type, count: data.count, ...(data.affixes.length > 0 ? { affixes: data.affixes } : {}), })) } } // ======================== 全局单例 & 向后兼容导出 ======================== /** * 全局刷怪引擎实例 * 推荐直接使用:`spawningEngine.generateWave(n)` / `spawningEngine.updateAdaptive(...)` * 新一局游戏开始时调用 `spawningEngine.reset()` */ export const spawningEngine = new RogueSpawningEngine() /** * 向后兼容:动态生成波次配置 * 每次调用都会重新随机生成,如需固定结果请缓存返回值或使用 spawningEngine.generateWave() * * @param waveNumber - 波次编号(1-30) * @returns IWaveSlot[] 按 MonType 合并后的怪物数量配置 */ export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] { return spawningEngine.getWaveSlotConfig(waveNumber) } /** * 向后兼容:默认占位配置(波次 > 30 或异常时的兜底配置) * 4 个 Melee + 3 个 Long + 1 个 Support + 1 个 Bomber */ export const DefaultWaveSlot: IWaveSlot[] = [ { type: MonType.Melee, count: 4 }, { type: MonType.Long, count: 3 }, { type: MonType.Support, count: 1 }, { type: MonType.Bomber, count: 1 }, ] /** * 向后兼容:波次配置映射 * 旧代码通过 `WaveSlotConfig[wave]` 访问时,Proxy 会拦截并调用引擎动态生成。 * 注意:每次访问同一波次都会重新随机,结果可能不同。 * 如需固定结果,请改用 `spawningEngine.generateWave()` 并缓存。 * * @example * WaveSlotConfig[5] // 动态生成第 5 波的 IWaveSlot[] * WaveSlotConfig[30] // 动态生成第 30 波的 IWaveSlot[] */ 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 && wave <= 30) { return spawningEngine.getWaveSlotConfig(wave) } if (prop === "toJSON") return () => ({}) return undefined }, has(_target, prop: string) { const wave = parseInt(prop, 10) return !isNaN(wave) && wave >= 1 && wave <= 30 }, } ) // ======================== 旧属性成长系统(向后兼容,新系统不再使用) ======================== /** @deprecated 新系统使用 TierConfigs 中的 tier_multiplier */ export enum UpType { AP1_HP1 = 0, HP2 = 1, AP2 = 2, } /** @deprecated 新系统使用 RogueSpawningEngine */ export const StageGrow = { [UpType.AP1_HP1]: [4, 10] as [number, number], [UpType.HP2]: [2, 20] as [number, number], [UpType.AP2]: [8, 0] as [number, number], } /** @deprecated 新系统使用 RogueSpawningEngine */ export const StageBossGrow = { [UpType.AP1_HP1]: [3, 16] as [number, number], [UpType.HP2]: [1, 24] as [number, number], [UpType.AP2]: [10, 4] as [number, number], }