From cede98eab975dbb2797eb650342a63efd6baecee Mon Sep 17 00:00:00 2001 From: walkpan Date: Fri, 15 May 2026 12:28:06 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=88=B7=E6=80=AA=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20RogueConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/script/game/map/RogueConfig.ts | 1181 ++++++++++++++++++++++--- 1 file changed, 1048 insertions(+), 133 deletions(-) diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index 834b85dc..eb7c382c 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -1,167 +1,1082 @@ /** * @file RogueConfig.ts - * @description Roguelike 关卡配置 —— 怪物类型、成长值、波次刷怪方案 + * @description 肉鸽刷怪系统 v2 —— 三层程序化生成架构 * - * 职责: - * 1. 定义怪物属性成长类型(UpType)和每种类型的 AP / HP 每阶段成长值。 - * 2. 定义怪物分类(MonType)和对应的怪物 UUID 池。 - * 3. 定义每一波(Wave)的怪物占位配置(WaveSlotConfig / DefaultWaveSlot)。 - * 4. 提供全局刷怪强度偏差系数(SpawnPowerBias)。 + * 架构:蓝图模板(节奏) + 权重填充(内容) + 自适应微调(数值) + * 主线 30 波 / 10 阶梯 / 每档 3 波(恢复→攀升→高潮) + * 10 种怪物 + 8 种词缀 + 自适应难度 ±15% + * 通关后可选无限模式(分层推进) * - * 设计说明: - * - 战场固定 5 个占位槽(索引 0-4)。 - * - Boss 默认占 3 个槽位,只要有连续 3 格空闲即可放置。 - * - MissionMonComp 在每波开始时读取本配置,决定刷怪组合。 - * - * 注意: - * - StageGrow / StageBossGrow 的索引 [0] 为 AP 成长,[1] 为 HP 成长。 - * - 实际计算公式:base_stat + stage × grow_value × SpawnPowerBias。 + * GDD: /gdd/rogue-spawning.md */ -// ======================== 属性成长类型枚举 ======================== +// ======================== 怪物类型枚举 ======================== -/** 怪物属性成长类型 */ -export enum UpType { - /** 平衡型:AP 和 HP 均匀成长 */ - AP1_HP1 = 0, - /** 强 HP 型:以血量为主成长 */ - HP2 = 1, - /** 强 AP 型:以攻击力为主成长 */ - AP2 = 2 +/** 怪物类型(10 种) */ +export enum MonType { + Melee = 0, + Heavy = 1, + Long = 2, + Support = 3, + Bomber = 4, + Summoner = 5, + Assassin = 6, + Splitter = 7, + MeleeBoss = 8, + LongBoss = 9, } -// ======================== 普通怪成长配置 ======================== - /** - * 普通怪每阶段成长值:[AP 成长, HP 成长] - * 每经历一波(stage +1),怪物的 base_ap / base_hp 增加对应值。 + * 怪物类型名称映射(调试/日志用) + * @example MonTypeName[MonType.Bomber] // => "自爆" */ -export const StageGrow = { - [UpType.AP1_HP1]: [4,10], // 平衡型:每波攻击+4 血量+10 - [UpType.HP2]: [2,20], // 强HP型:每波攻击+2 血量+20 - [UpType.AP2]: [8,0], // 强AP型:每波攻击+8 血量+0 +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", } -// ======================== Boss 额外成长配置 ======================== +// ======================== 词缀类型枚举 ======================== + +/** 怪物词缀(8 种) */ +export enum AffixType { + Elite = 0, + Berserk = 1, + Shield = 2, + Regen = 3, + Swift = 4, + Giant = 5, + Chain = 6, + SummonerA = 7, +} /** - * Boss 在普通怪成长基础上的 **额外** 成长值:[AP 增量, HP 增量] - * 实际 Boss 成长 = StageGrow + StageBossGrow。 + * 词缀配置接口 + * @property name - 词缀中文名(显示/调试用) + * @property hpMultiplier - HP 倍率修饰,乘算到最终 HP 上(1.0 = 无变化) + * @property apMultiplier - AP 倍率修饰,乘算到最终 AP 上(1.0 = 无变化) + * @property cost - 该词缀占用的难度预算额外成本 + * @property tierMin - 首次可出现的最低阶梯编号(Tier),低于此阶梯不会触发 + * @property description - 词缀效果简述 */ -export const StageBossGrow = { - [UpType.AP1_HP1]: [3,16], // 平衡型 Boss:额外攻击+3 血量+16 - [UpType.HP2]: [1,24], // 强HP型 Boss:额外攻击+1 血量+24 - [UpType.AP2]: [10,4], // 强AP型 Boss:额外攻击+10 血量+4 +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 个小怪", + }, +} -/** 怪物类型常量(用于 WaveSlotConfig 中引用) */ -export const MonType = { - /** 近战普通怪 */ - Melee: 0, - /** 远程普通怪 */ - Long: 1, - /** 辅助怪(支持类) */ - Support: 2, - /** 近战 Boss */ - MeleeBoss: 3, - /** 远程 Boss */ - LongBoss: 4, - /** 飞行普通怪 */ - Fly: 5, - /** 飞行 Boss */ - FlyBoss: 6, +/** + * 词缀互斥组:同组内的词缀最多只能出现一个 + * 当随机到互斥冲突时,按 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 列表(随机抽取) */ -export const MonList = { - [MonType.Melee]: [6001,6002,6003], // 近战怪池 - [MonType.Long]: [6004,6005], // 远程怪池 - [MonType.Support]: [6005], // 辅助怪池 - [MonType.MeleeBoss]:[6006,6105], // 近战 Boss 池 - [MonType.LongBoss]:[6104], // 远程 Boss 池 - [MonType.Fly]: [6004, 6005], // 飞行怪池 (占位) - [MonType.FlyBoss]: [6104], // 飞行 Boss 池 (占位) +/** + * 各怪物类型对应的 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], } -// ======================== 全局刷怪强度系数 ======================== +// ======================== 怪物基础属性 & 成本 ======================== /** - * 全局刷怪强度偏差系数。 - * 所有怪物的最终 AP / HP 会乘以此系数。 - * 后期可根据玩家强度动态调整以实现自适应难度。 + * 怪物基础属性接口 + * 定义怪物在 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 const SpawnPowerBias = 1 - -// ======================== 波次占位配置数据结构 ======================== - -/** 单条波次占位配置 */ -export interface IWaveSlot { - /** 怪物类型(参考 MonType) */ - type: number; - /** 该类型的怪物数量 */ - count: number; - /** (可选)每个怪物占用几个槽位,默认 1;大型 Boss 设为 2 (新方案中主要用作标识Boss) */ - slotsPerMon?: number; - /** (可选)飞行层:0=地面, 1=第一层空中(+120), 2=第二层空中(+240)。默认地面为 0,Fly/FlyBoss 类型默认 1 */ - flyLane?: 0 | 1 | 2; -} - -// ========================================================================================= -// 【每波怪物占位与刷怪配置说明】 -// -// 字段说明: -// - type: 怪物类型 (参考 MonType,如近战 0,远程 1,Boss 3,飞行 5 等)。 -// - count: 该类型的怪在场上同时存在几个。 -// - slotsPerMon: (可选) Boss 占位,旧方案占用多格,现作为标识。 -// - flyLane: (可选) 飞行层,0=地面,1=空中1层,2=空中2层。 -// -// 【规则约束】: -// - 突破 5 槽限制,怪物按刷怪顺序依次从 X=280 开始向右(每隔 50)排布。 -// - Boss 和普通怪占据一个相同的 X 锚点,但视觉体积更大。 -// ========================================================================================= - -/** 各波次的怪物占位配置(key = 波次编号) */ -export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = { - - /** 第 1 波:2 近战 + 1 近战Boss(默认占3格) */ - 1: [ - { type: MonType.Melee, count: 1 }, - ], - 2: [ - { type: MonType.Melee, count: 1 }, - { type: MonType.Long, count: 1 }, - ], - 3: [ - { type: MonType.Melee, count: 1 }, - { type: MonType.Long, count: 2 }, - ], - 4: [ - { type: MonType.Melee, count: 2 }, - { type: MonType.Long, count: 2 }, - ], - /** 第 1 波:2 近战 + 1 近战Boss(默认占3格) */ - 10: [ - { type: MonType.MeleeBoss, count: 1 }, - { type: MonType.Long, count: 2 }, - ], - /** 第 2波:2 近战 + 1 远程Boss(默认占3格) */ - 20: [ - { type: MonType.Melee, count: 2 }, - { type: MonType.LongBoss, count: 1 } - ], +export interface MonsterBaseStats { + hp: number + ap: number + cost: number + isBoss: boolean } /** - * 默认占位配置: - * 当 WaveSlotConfig 中找不到对应波次时使用此兜底配置。 - * 默认 2 近战 + 3 远程。 + * 怪物基础属性表 + * key = MonType 枚举值,value = 该类型在 Tier 1 时的基础属性 + * @see MonsterBaseStats 字段说明 */ -export const DefaultWaveSlot: IWaveSlot[] = [ - { type: MonType.Melee, count: 2 }, - { type: MonType.Long, count: 3 } +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], +}