同步调整了文档中的波次、阶梯、词缀、预算等全部数值配置,同时更新了RogueConfig.ts中的代码配置,将原30波10阶梯架构重构为15波5阶梯版本,包括调整怪物生成模板、属性倍率、词缀解锁等级和无限模式起始阶梯等核心参数。
1088 lines
43 KiB
TypeScript
1088 lines
43 KiB
TypeScript
/**
|
||
* @file RogueConfig.ts
|
||
* @description 肉鸽刷怪系统 v2 —— 三层程序化生成架构
|
||
*
|
||
* 架构:蓝图模板(节奏) + 权重填充(内容) + 自适应微调(数值)
|
||
* 主线 15 波 / 5 阶梯 / 每档 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<number, string> = {
|
||
[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,
|
||
CritRes = 8,
|
||
FreezeRes = 9,
|
||
KnockbackRes = 10,
|
||
}
|
||
|
||
/**
|
||
* 词缀配置接口
|
||
* @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, AffixConfig> = {
|
||
[AffixType.Elite]: {
|
||
name: "精英", hpMultiplier: 1.5, apMultiplier: 1.3,
|
||
cost: 20, tierMin: 3, description: "+50% HP, +30% AP",
|
||
},
|
||
[AffixType.Berserk]: {
|
||
name: "狂暴", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 15, tierMin: 3, description: "攻速 ×1.5 (行为层实现)",
|
||
},
|
||
[AffixType.Shield]: {
|
||
name: "护盾", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 25, tierMin: 3, description: "开局带 抵御2次 伤害吸收盾",
|
||
},
|
||
[AffixType.Regen]: {
|
||
name: "再生", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 20, tierMin: 4, description: "每秒回复 2% HP",
|
||
},
|
||
[AffixType.Swift]: {
|
||
name: "疾速", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 10, tierMin: 4, description: "移速 ×2",
|
||
},
|
||
[AffixType.Giant]: {
|
||
name: "巨型", hpMultiplier: 2.0, apMultiplier: 1.5,
|
||
cost: 30, tierMin: 4, description: "×2 体型, +100% HP, +50% AP",
|
||
},
|
||
[AffixType.Chain]: {
|
||
name: "连锁", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 20, tierMin: 5, description: "攻击附带 50% 溅射伤害",
|
||
},
|
||
[AffixType.SummonerA]: {
|
||
name: "召唤", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 25, tierMin: 5, description: "每 8 秒召唤 1 个小怪",
|
||
},
|
||
[AffixType.CritRes]: {
|
||
name: "坚韧", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 15, tierMin: 3, description: "+暴击抗性",
|
||
},
|
||
[AffixType.FreezeRes]: {
|
||
name: "防寒", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 15, tierMin: 3, description: "+冰冻抗性",
|
||
},
|
||
[AffixType.KnockbackRes]: {
|
||
name: "稳固", hpMultiplier: 1.0, apMultiplier: 1.0,
|
||
cost: 15, tierMin: 3, description: "+击退抗性",
|
||
},
|
||
}
|
||
|
||
/**
|
||
* 词缀互斥组:同组内的词缀最多只能出现一个
|
||
* 当随机到互斥冲突时,按 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, AffixType.CritRes, AffixType.FreezeRes, AffixType.KnockbackRes,
|
||
]
|
||
|
||
// ======================== 蓝图模板类型 ========================
|
||
|
||
/** 模板类型 */
|
||
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, number> = {
|
||
[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<number, number[]> = {
|
||
[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, MonsterBaseStats> = {
|
||
[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([1, 2, 3, 4, 5])
|
||
/** MajorBoss(高难度 Boss)出现的 Tier 集合 */
|
||
const MAJOR_BOSS_TIERS = new Set([3, 5])
|
||
|
||
/**
|
||
* 5 阶梯配置表
|
||
* key = 阶梯编号 1-5,value = 该阶梯的完整配置
|
||
* 主线 15 波映射:wave 1-3 → T1, wave 4-6 → T2, ..., wave 13-15 → T5
|
||
*/
|
||
export const TierConfigs: Record<number, TierConfig> = {
|
||
1: { multiplier: 1.0, budget: 500, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long], isBossTier: false },
|
||
2: { multiplier: 1.6, budget: 1000, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support], isBossTier: true },
|
||
3: { multiplier: 2.5, budget: 1800, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin], isBossTier: true },
|
||
4: { multiplier: 3.8, budget: 3000, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin, MonType.Summoner, MonType.Splitter], isBossTier: true },
|
||
5: { multiplier: 5.5, budget: 5000, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Bomber, MonType.Assassin, MonType.Summoner, MonType.Splitter], isBossTier: true },
|
||
}
|
||
|
||
/**
|
||
* 获取指定阶梯的配置
|
||
* 支持 Tier 1-5(查表)和 Tier 6+(无限模式,递推计算:T(n) = T(n-1) × 1.2)
|
||
*
|
||
* @param tier - 阶梯编号(1-5 主线,6+ 无限模式)
|
||
* @returns TierConfig 该阶梯的完整配置
|
||
*/
|
||
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-T2: 0%(前期无词缀)
|
||
* T3: 15%, T4: 30%, T5: 50%
|
||
* 最终概率 = baseAffixChance × roleMultiplier
|
||
*/
|
||
export const BaseAffixChance: Record<number, number> = {
|
||
1: 0, 2: 0, 3: 0.15, 4: 0.30, 5: 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: 5, countMax: 10, weight: 1.0 }] },
|
||
{ id: "R2", type: TemplateType.REST, tierMin: 1, allowAffix: false,
|
||
slots: [{ typePool: [MonType.Melee, MonType.Heavy], countMin: 5, countMax: 15, weight: 1.0 }] },
|
||
{ id: "R3", type: TemplateType.REST, tierMin: 3, allowAffix: false,
|
||
slots: [{ typePool: [MonType.Melee, MonType.Long], countMin: 10, countMax: 15, weight: 1.0 }] },
|
||
|
||
// ---- NORMAL 类 ----
|
||
{ id: "N1", type: TemplateType.NORMAL, tierMin: 1, allowAffix: false,
|
||
slots: [{ typePool: [MonType.Melee], countMin: 10, countMax: 20, weight: 1.0 }] },
|
||
{ id: "N2", type: TemplateType.NORMAL, tierMin: 1, allowAffix: false,
|
||
slots: [
|
||
{ typePool: [MonType.Melee], countMin: 5, countMax: 15, weight: 0.6 },
|
||
{ typePool: [MonType.Long, MonType.Heavy], countMin: 5, countMax: 10, weight: 0.4 },
|
||
] },
|
||
{ id: "N3", type: TemplateType.NORMAL, tierMin: 2, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.Melee, MonType.Heavy], countMin: 10, countMax: 15, weight: 0.5 },
|
||
{ typePool: [MonType.Long], countMin: 5, countMax: 10, weight: 0.3 },
|
||
{ typePool: [MonType.Support], countMin: 5, countMax: 5, weight: 0.2 },
|
||
] },
|
||
{ id: "N4", type: TemplateType.NORMAL, tierMin: 3, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.Melee, MonType.Heavy], countMin: 10, countMax: 20, weight: 0.4 },
|
||
{ typePool: [MonType.Long, MonType.Assassin], countMin: 5, countMax: 15, weight: 0.3 },
|
||
{ typePool: [MonType.Support, MonType.Bomber], countMin: 5, countMax: 10, weight: 0.3 },
|
||
] },
|
||
|
||
// ---- MIXED 类 ----
|
||
{ id: "M1", type: TemplateType.MIXED, tierMin: 2, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.Melee, MonType.Heavy], countMin: 10, countMax: 15, weight: 0.4 },
|
||
{ typePool: [MonType.Long], countMin: 5, countMax: 10, weight: 0.3 },
|
||
{ typePool: [MonType.Support], countMin: 5, countMax: 5, weight: 0.3 },
|
||
] },
|
||
{ id: "M2", type: TemplateType.MIXED, tierMin: 3, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.Melee, MonType.Heavy], countMin: 10, countMax: 20, weight: 0.3 },
|
||
{ typePool: [MonType.Long, MonType.Assassin], countMin: 10, countMax: 15, weight: 0.3 },
|
||
{ typePool: [MonType.Bomber], countMin: 5, countMax: 10, weight: 0.2 },
|
||
{ typePool: [MonType.Support], countMin: 5, countMax: 5, weight: 0.2 },
|
||
] },
|
||
{ id: "M3", type: TemplateType.MIXED, tierMin: 4, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.Melee, MonType.Heavy], countMin: 15, countMax: 20, weight: 0.3 },
|
||
{ typePool: [MonType.Long, MonType.Assassin], countMin: 10, countMax: 15, weight: 0.25 },
|
||
{ typePool: [MonType.Summoner, MonType.Splitter], countMin: 5, countMax: 10, weight: 0.25 },
|
||
{ typePool: [MonType.Bomber, MonType.Support], countMin: 5, countMax: 10, weight: 0.2 },
|
||
] },
|
||
|
||
// ---- ELITE 类 ----
|
||
{ id: "E1", type: TemplateType.ELITE, tierMin: 3, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.Melee, MonType.Heavy], countMin: 5, countMax: 10, weight: 0.5, forceAffix: true },
|
||
{ typePool: [MonType.Long, MonType.Assassin], countMin: 5, countMax: 10, weight: 0.5, forceAffix: true },
|
||
] },
|
||
{ id: "E2", type: TemplateType.ELITE, tierMin: 4, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.Heavy], countMin: 5, countMax: 10, weight: 0.3, forceAffix: true },
|
||
{ typePool: [MonType.Assassin, MonType.Splitter], countMin: 5, countMax: 10, weight: 0.4, forceAffix: true },
|
||
{ typePool: [MonType.Bomber], countMin: 5, countMax: 10, weight: 0.3, forceAffix: true },
|
||
] },
|
||
|
||
// ---- BOSS 类 ----
|
||
{ id: "B1", type: TemplateType.BOSS, tierMin: 1, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.MeleeBoss], countMin: 1, countMax: 1, weight: 1.0 },
|
||
{ typePool: [MonType.Melee], countMin: 10, countMax: 15, weight: 0.6 },
|
||
{ typePool: [MonType.Long], countMin: 0, countMax: 10, weight: 0.4 },
|
||
] },
|
||
{ id: "B2", type: TemplateType.BOSS, tierMin: 2, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.MeleeBoss], countMin: 1, countMax: 1, weight: 1.0 },
|
||
{ typePool: [MonType.Melee, MonType.Heavy], countMin: 10, countMax: 15, weight: 0.5 },
|
||
{ typePool: [MonType.Long, MonType.Support], countMin: 5, countMax: 10, weight: 0.5 },
|
||
] },
|
||
{ id: "B3", type: TemplateType.BOSS, tierMin: 3, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.MeleeBoss, MonType.LongBoss], countMin: 1, countMax: 1, weight: 1.0 },
|
||
{ typePool: [MonType.Melee, MonType.Heavy], countMin: 10, countMax: 20, weight: 0.4 },
|
||
{ typePool: [MonType.Long, MonType.Assassin], countMin: 5, countMax: 15, weight: 0.3 },
|
||
{ typePool: [MonType.Bomber, MonType.Support], countMin: 5, countMax: 10, weight: 0.3 },
|
||
] },
|
||
{ id: "B4", type: TemplateType.BOSS, tierMin: 4, allowAffix: true,
|
||
slots: [
|
||
{ typePool: [MonType.MeleeBoss, MonType.LongBoss], countMin: 1, countMax: 1, weight: 1.0 },
|
||
{ typePool: [MonType.Heavy], countMin: 10, countMax: 15, weight: 0.3 },
|
||
{ typePool: [MonType.Assassin, MonType.Splitter], countMin: 5, countMax: 10, weight: 0.3 },
|
||
{ typePool: [MonType.Summoner, MonType.Support], countMin: 5, countMax: 10, weight: 0.2 },
|
||
{ typePool: [MonType.Bomber], countMin: 5, countMax: 10, weight: 0.2 },
|
||
] },
|
||
|
||
// ---- 教程专用 ----
|
||
{ id: "TUTORIAL", type: TemplateType.NORMAL, tierMin: 1, allowAffix: false,
|
||
slots: [{ typePool: [MonType.Melee], countMin: 5, countMax: 5, 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-15 波)
|
||
*
|
||
* 内部流程:
|
||
* 1. 根据 waveNumber 计算 tier 和 waveInTier
|
||
* 2. W1 特殊处理(固定教程模板:5 个 Melee)
|
||
* 3. 根据 waveInTier 和 isBossTier 选择蓝图模板
|
||
* 4. 计算难度预算 = base_budget × template_modifier × adaptive_factor
|
||
* 5. 按模板槽位填充怪物、应用词缀、计算最终属性
|
||
*
|
||
* @param waveNumber - 波次编号(1-15)
|
||
* 1-3 → Tier 1, 4-6 → Tier 2, ..., 13-15 → Tier 5
|
||
* < 1 返回空数组
|
||
*
|
||
* @returns GeneratedMonster[] 该波次的怪物实例数组,按 spawnIndex 排列。
|
||
* 战斗系统按此顺序在战场上依次放置怪物。
|
||
* 每次调用结果不同(随机生成),如需固定结果请在同一次调用中缓存。
|
||
*
|
||
* @example
|
||
* // 生成第 1 波(教程:5 个 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 从 6 开始递增。
|
||
* 属性倍率按 T(n) = T(n-1) × 1.2 递推,无上限。
|
||
*
|
||
* @param layer - 层编号(从 1 开始)
|
||
* Tier = 5 + layer(layer=1 → Tier 6, multiplier=6.6)
|
||
* @param waveInLayer - 层内波次编号(1-4)
|
||
* 1: REST(恢复波)
|
||
* 2: NORMAL(标准波)
|
||
* 3: MIXED(混合波)
|
||
* 4: BOSS(Boss 波,Boss 每层额外获得词缀)
|
||
*
|
||
* @returns GeneratedMonster[] 该波的怪物实例数组
|
||
*
|
||
* @example
|
||
* // 无限模式第 1 层第 4 波(Boss 波,Tier 6)
|
||
* const boss = engine.generateInfiniteWave(1, 4)
|
||
* // boss 包含 1 个 Boss + 若干小怪,属性倍率 = 6.6x
|
||
*/
|
||
generateInfiniteWave(layer: number, waveInLayer: number): GeneratedMonster[] {
|
||
const tier = 5 + 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 <= 5
|
||
)
|
||
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-15)
|
||
* @returns IWaveSlot[] 按怪物类型合并后的数组
|
||
* - type: MonType 枚举值
|
||
* - count: 该类型的怪物总数
|
||
* - affixes: 该组怪物的词缀列表(仅首个同类型怪物的词缀)
|
||
*
|
||
* @example
|
||
* engine.getWaveSlotConfig(6)
|
||
* // [{ type: 8, count: 1 }, { type: 0, count: 10 }, { type: 2, count: 5 }]
|
||
* // = 1 个 MeleeBoss + 10 个 Melee + 5 个 Long
|
||
*/
|
||
getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
||
const generated = this.generateWave(waveNumber)
|
||
const slotMap = new Map<number, { count: number; affixes: AffixType[] }>()
|
||
|
||
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)
|
||
}
|
||
|
||
/**
|
||
* 向后兼容:默认占位配置(波次 > 15 或异常时的兜底配置)
|
||
* 20 个 Melee + 15 个 Long + 5 个 Support + 5 个 Bomber
|
||
*/
|
||
export const DefaultWaveSlot: IWaveSlot[] = [
|
||
{ type: MonType.Melee, count: 20 },
|
||
{ type: MonType.Long, count: 15 },
|
||
{ type: MonType.Support, count: 5 },
|
||
{ type: MonType.Bomber, count: 5 },
|
||
]
|
||
|
||
/**
|
||
* 向后兼容:波次配置映射
|
||
* 旧代码通过 `WaveSlotConfig[wave]` 访问时,Proxy 会拦截并调用引擎动态生成。
|
||
* 注意:每次访问同一波次都会重新随机,结果可能不同。
|
||
* 如需固定结果,请改用 `spawningEngine.generateWave()` 并缓存。
|
||
*
|
||
* @example
|
||
* WaveSlotConfig[5] // 动态生成第 5 波的 IWaveSlot[]
|
||
* WaveSlotConfig[15] // 动态生成第 15 波的 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 <= 15) {
|
||
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 <= 15
|
||
},
|
||
}
|
||
)
|
||
|
||
// ======================== 旧属性成长系统(向后兼容,新系统不再使用) ========================
|
||
|
||
/** @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],
|
||
}
|