Files
pixelheros/assets/script/game/map/RogueConfig.ts
panFD 279e239e3a refactor(rogue): replace random spawning with rule-based configs
Introduce hard-coded WaveConfigs (1-30), modular SquadLibrary (6 templates), and dual-layer enchant system (WaveEnchantLibrary + MonsterElite). Spawn engine now assembles squads by weight, applies global wave scaling and enchant modifiers, and rolls individual elites with bounded rate. Backward-compatible exports preserved; AffixType marked deprecated. MissionMonComp/MissionComp unchanged.
2026-07-05 22:33:42 +08:00

651 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file RogueConfig.ts
* @description 肉鸽刷怪系统(基于硬编码规则 + 模块化随机)
*
* 设计层次:
* 1. MonsterElite - 个体强化5 种)
* 2. WaveEnchant - 波次级强化(环境修饰)
* 3. SquadConfig - 小队模板3-4 只怪的组合单元)
* 4. WaveConfig - 每波硬编码(基础数 + 小队池 + 强化池)
* 5. RogueSpawningEngine - 生成引擎(按规则组合上述配置)
*
* 详细设计见 docs/superpowers/specs/2026-07-05-rogue-config-refactor-design.md
*/
import { HeroInfo, MonType, MonTypeName } from "../common/config/heroSet";
import { FacSet } from "../common/config/GameSet";
// ======================== 1. 枚举与基础类型 ========================
/**
* 个体强化类型(精选 5 种)
* 设计权衡HeroAttrsComp 暂无 atk_cd / regen 字段,因此 Swift/Regen 简化为数值差异化。
* 待 HeroAttrsComp 升级后可启用 cd_mul / regen 扩展点。
*/
export enum MonsterElite {
Elite = 0, // 精英:全面强化
Berserk = 1, // 狂暴:高攻低血
Shield = 2, // 护盾:坦克
Regen = 3, // 再生:高血偏弱攻
Swift = 4, // 疾风:平衡偏输出
}
/** 个体强化效果定义 */
export const MonsterEliteSet: Record<MonsterElite, {
name: string;
hp_mul: number;
ap_mul: number;
}> = {
[MonsterElite.Elite]: { name: "精英", hp_mul: 1.5, ap_mul: 1.30 },
[MonsterElite.Berserk]: { name: "狂暴", hp_mul: 0.9, ap_mul: 1.60 },
[MonsterElite.Shield]: { name: "护盾", hp_mul: 1.8, ap_mul: 0.80 },
[MonsterElite.Regen]: { name: "再生", hp_mul: 1.3, ap_mul: 0.95 },
[MonsterElite.Swift]: { name: "疾风", hp_mul: 1.0, ap_mul: 1.15 },
};
/**
* 旧版词缀类型枚举(已废弃)
* @deprecated 已被 MonsterElite 取代,仅保留导出避免破坏外部引用。
* 新代码请使用 MonsterElite。
*/
export enum AffixType {
Elite = 0,
Berserk = 1,
Shield = 2,
Regen = 3,
Swift = 4,
Giant = 5,
Chain = 6,
SummonerA = 7,
CritRes = 8,
FreezeRes = 9,
KnockbackRes = 10,
}
// ======================== 2. 怪物 UUID 池 ========================
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
export const MonList: Record<number, number[]> = {};
for (const key in HeroInfo) {
const info = HeroInfo[key];
if (info.fac === FacSet.MON && info.monType !== undefined) {
if (!MonList[info.monType]) {
MonList[info.monType] = [];
}
MonList[info.monType].push(info.uuid);
}
}
// ======================== 3. 波次级强化库 ========================
/**
* 波次级强化(环境修饰符)
* 作用于整波怪物,与 MonsterElite个体解耦。
*/
export interface WaveEnchant {
id: string;
name: string;
desc: string;
hp_mul?: number; // 全员 HP 乘子
ap_mul?: number; // 全员 AP 乘子
cd_mul?: number; // 全员攻击间隔乘子(后续扩展点)
elite_rate_mul?: number; // 该波个体强化出现概率乘子
weight: number; // 在强化池中的抽取权重
}
/** 波次强化库(硬编码) */
export const WaveEnchantLibrary: Record<string, WaveEnchant> = {
frenzy: { id: "frenzy", name: "狂热浪潮", desc: "全员攻击力+25%", ap_mul: 1.25, weight: 10 },
ironhide: { id: "ironhide", name: "铁皮大军", desc: "全员生命+40%", hp_mul: 1.40, weight: 10 },
swift: { id: "swift", name: "疾风突袭", desc: "全员攻速+20%", cd_mul: 0.80, weight: 8 },
nightmare: { id: "nightmare", name: "梦魇来袭", desc: "个体强化率×2HP+15%", elite_rate_mul: 2.0, hp_mul: 1.15, weight: 5 },
fortress: { id: "fortress", name: "钢铁堡垒", desc: "全员 HP+60% AP-15%", hp_mul: 1.60, ap_mul: 0.85, weight: 6 },
};
// ======================== 4. 小队模板库 ========================
/** 小队内单种怪物的槽位定义 */
export interface SquadSlot {
type: MonType;
count: number;
}
/** 小队模板3-4 只怪的组合单元) */
export interface SquadConfig {
id: string;
name: string;
slots: SquadSlot[];
weight: number; // 在小队池中的抽取权重
}
/** 小队模板库(硬编码) */
export const SquadLibrary: Record<string, SquadConfig> = {
melee_grunt: { id: "melee_grunt", name: "近战步兵组", weight: 10, slots: [{ type: MonType.Melee, count: 3 }] },
assassin_squad: { id: "assassin_squad", name: "刺客突袭组", weight: 6, slots: [{ type: MonType.Assassin, count: 2 }, { type: MonType.Support, count: 1 }] },
mixed_balanced: { id: "mixed_balanced", name: "平衡混合组", weight: 8, slots: [{ type: MonType.Melee, count: 1 }, { type: MonType.Long, count: 1 }, { type: MonType.Heavy, count: 1 }] },
long_line: { id: "long_line", name: "远程线列组", weight: 7, slots: [{ type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }] },
heavy_shield: { id: "heavy_shield", name: "重盾堡垒组", weight: 5, slots: [{ type: MonType.Heavy, count: 1 }, { type: MonType.Melee, count: 2 }] },
summoner_cult: { id: "summoner_cult", name: "召唤教派组", weight: 4, slots: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }] },
};
// ======================== 5. 波次配置表 ========================
/** 单波次完整配置 */
export interface WaveConfig {
/** 基础怪物数1~12 */
base_count: number;
/** 可选小队 id 池,引擎按权重抽取拼装到 base_count */
squad_pool: string[];
/** 可选波次强化 id 池,按权重抽 0~2 个(可空表示无强化) */
enchant_pool?: string[];
/** 是否 Boss 波(首位放 Boss */
boss_wave?: boolean;
/** 该波个体强化基础概率(默认按波次递增) */
elite_base_rate?: number;
}
/**
* 波次配置表(硬编码 wave 1~30
* 节奏曲线:教学期(1-4) → 第一Boss(5) → 引入强化(6-9) → 第二Boss(10) →
* 组合多样化(11-14) → 第三Boss(15) → 高压阶段(16-24) →
* 第四Boss(25) → 终极阶段(26-29) → 最终Boss(30)
*/
export const WaveConfigs: Record<number, WaveConfig> = {
// ===== 教学期 =====
1: { base_count: 3, squad_pool: ["melee_grunt"] },
2: { base_count: 4, squad_pool: ["melee_grunt", "mixed_balanced"] },
3: { base_count: 5, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"] },
4: { base_count: 6, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"] },
// ===== 第一 Boss =====
5: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], boss_wave: true },
// ===== 引入波次强化 =====
6: { base_count: 7, squad_pool: ["assassin_squad", "long_line", "mixed_balanced"], enchant_pool: ["frenzy"], elite_base_rate: 0.10 },
7: { base_count: 8, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.12 },
8: { base_count: 8, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.14 },
9: { base_count: 9, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"],elite_base_rate: 0.16 },
// ===== 第二 Boss =====
10: { base_count: 10, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide"],boss_wave: true, elite_base_rate: 0.18 },
// ===== 组合多样化 =====
11: { base_count: 10, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"],enchant_pool: ["swift", "frenzy"], elite_base_rate: 0.18 },
12: { base_count: 10, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.20 },
13: { base_count: 11, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.22 },
14: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], enchant_pool: ["ironhide", "swift"], elite_base_rate: 0.24 },
// ===== 第三 Boss中期高潮 =====
15: { base_count: 11, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.25 },
// ===== 高压阶段 =====
16: { base_count: 11, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.26 },
17: { base_count: 11, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.27 },
18: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], enchant_pool: ["nightmare", "swift"], elite_base_rate: 0.28 },
19: { base_count: 12, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], enchant_pool: ["fortress"], elite_base_rate: 0.28 },
20: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.30 },
21: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "summoner_cult"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.30 },
22: { base_count: 12, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.30 },
23: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "melee_grunt"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.32 },
24: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "long_line"], enchant_pool: ["fortress", "swift"], elite_base_rate: 0.32 },
// ===== 第四 Boss =====
25: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "heavy_shield"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.35 },
// ===== 终极阶段 =====
26: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "long_line", "summoner_cult"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.35 },
27: { base_count: 12, squad_pool: ["heavy_shield", "mixed_balanced", "summoner_cult"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.38 },
28: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "melee_grunt"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.40 },
29: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "summoner_cult"], enchant_pool: ["frenzy", "ironhide", "fortress"], elite_base_rate: 0.42 },
// ===== 最终 Boss =====
30: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide", "swift", "nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.45 },
};
// ======================== 6. 全局难度缩放 ========================
/**
* 根据波次返回全局 HP/AP 乘子,每 5 波递进一档
* @param wave 当前波次
* @returns hp_mul / ap_mul
*/
export function getGlobalScale(wave: number): { hp_mul: number; ap_mul: number } {
const step = Math.floor((wave - 1) / 5);
return {
hp_mul: 1 + 0.15 * step, // wave 1=1.0, 6=1.15, 11=1.30, 16=1.45, 21=1.60, 26=1.75
ap_mul: 1 + 0.08 * step, // wave 1=1.0, 6=1.08, 11=1.16, 16=1.24, 21=1.32, 26=1.40
};
}
// ======================== 7. 配置校验 ========================
/**
* 校验 WaveConfigs / SquadLibrary / WaveEnchantLibrary 引用一致性
* 建议在游戏启动时调用一次,便于发现配置错误
* @returns 错误信息数组,空数组表示校验通过
*/
export function validateRogueConfig(): string[] {
const errors: string[] = [];
// 1. 校验 WaveConfigs 引用与范围
for (const wave in WaveConfigs) {
const cfg = WaveConfigs[wave];
for (const squadId of cfg.squad_pool) {
if (!SquadLibrary[squadId]) {
errors.push(`Wave ${wave} 引用了不存在的小队: ${squadId}`);
}
}
if (cfg.enchant_pool) {
for (const enchId of cfg.enchant_pool) {
if (!WaveEnchantLibrary[enchId]) {
errors.push(`Wave ${wave} 引用了不存在的强化: ${enchId}`);
}
}
}
if (cfg.base_count < 1 || cfg.base_count > 12) {
errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~12)`);
}
}
// 2. 校验 SquadLibrary 中所有 type 在 MonList 中有怪
for (const id in SquadLibrary) {
const squad = SquadLibrary[id];
for (const slot of squad.slots) {
if (!MonList[slot.type] || MonList[slot.type].length === 0) {
errors.push(`小队 ${id} 引用的怪物类型 ${slot.type}(${MonTypeName[slot.type] || "?"}) 在 MonList 中无可用 uuid`);
}
}
}
return errors;
}
// ======================== 8. 生成结果接口 ========================
/** 生成结果(传递给 MissionMonComp 用于实例化怪物) */
export interface GeneratedMonster {
uuid: number;
type: MonType;
hp: number;
ap: number;
isBoss: boolean;
spawnIndex: number;
/** @deprecated 已被 elite 取代,始终返回 [],仅向后兼容 */
affixes: AffixType[];
/** 个体强化(无则 undefined */
elite?: MonsterElite;
/** 该波触发的波次强化 id 列表 */
wave_enchants: string[];
/** 测试模式专用技能覆盖 */
testSkills?: {
skill?: { s_uuid: number; cd?: number; overrides?: any };
atking?: { s_uuid: number; t_num: number; overrides?: any }[];
atked?: { s_uuid: number; t_num: number; overrides?: any }[];
dead?: { s_uuid: number; t_num: number; overrides?: any }[];
fstart?: { s_uuid: number; t_num: number; overrides?: any }[];
fend?: { s_uuid: number; t_num: number; overrides?: any }[];
};
}
// ======================== 9. 测试模式配置 ========================
export const TestModeConfig = {
enable: false, // 默认关闭测试模式
baseHp: 150,
baseAp: 12,
growthRatePerWave: 0.2,
monType: MonType.Melee,
monUuid: 6001,
/** @deprecated 已被 testElite 取代 */
affixes: [] as AffixType[],
spawnCount: 1,
/** 测试个体强化 */
testElite: undefined as MonsterElite | undefined,
/** 测试波次强化 id */
testWaveEnchant: undefined as string | undefined,
skill: undefined as { s_uuid: number; cd?: number; overrides?: any } | undefined,
atking: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
atked: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
dead: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
fstart: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
fend: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
};
// ======================== 10. 生成引擎 ========================
/**
* 肉鸽刷怪生成引擎
* 按硬编码 WaveConfig 规则,组合小队模板与双层强化系统生成怪物列表
*/
export class RogueSpawningEngine {
/**
* 生成指定波次的怪物列表
* @param waveNumber 波次1 起,>30 时复用 wave 30 配置)
* @returns 怪物列表,长度 ≤ 12
*/
generateWave(waveNumber: number): GeneratedMonster[] {
if (waveNumber < 1) return [];
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
if (TestModeConfig.enable) {
return this.generateTestWave(waveNumber);
}
// 1. 取硬编码 WaveConfig>30 时复用 wave 30 配置)
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
// 2. 抽取波次级强化(按权重从 enchant_pool 抽 0~2 个)
const enchants = this.pickEnchants(cfg.enchant_pool);
// 3. 计算波次最终属性乘子
const globalScale = getGlobalScale(waveNumber);
const enchantHpMul = enchants.reduce((m, e) => m * (e.hp_mul ?? 1), 1);
const enchantApMul = enchants.reduce((m, e) => m * (e.ap_mul ?? 1), 1);
const eliteRateMul = enchants.reduce((m, e) => m * (e.elite_rate_mul ?? 1), 1);
// 4. Boss 位Boss 波首位占 1 个)
const monsters: GeneratedMonster[] = [];
let remaining = cfg.base_count;
if (cfg.boss_wave) {
monsters.push(this.makeBoss(waveNumber));
remaining -= 1;
}
// 5. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, waveNumber);
monsters.push(...squadMonsters);
// 6. 应用全局 & 波次 Enchant 乘子
const waveEnchantIds = enchants.map(e => e.id);
for (const m of monsters) {
m.hp = Math.max(1, Math.round(m.hp * globalScale.hp_mul * enchantHpMul));
m.ap = Math.max(1, Math.round(m.ap * globalScale.ap_mul * enchantApMul));
m.wave_enchants = waveEnchantIds.slice();
}
// 7. 应用个体 Elite非 Boss 怪,每只最多 1 个)
const eliteBaseRate = cfg.elite_base_rate ?? Math.min(0.05 + waveNumber * 0.01, 0.30);
for (const m of monsters) {
if (!m.isBoss && Math.random() < eliteBaseRate * eliteRateMul) {
const elite = this.pickElite();
this.applyElite(m, elite);
}
}
// 8. 硬上限保护(理论不会触发,但防止配置错误)
return monsters.slice(0, 12);
}
/** 重置引擎状态(保留接口兼容,当前无内部状态) */
reset(): void {
// 无可变状态
}
/**
* 获取指定波次的怪物总数
* @param waveNumber 目标波数
* @returns 预计生成的怪物总数base_count受 12 上限约束)
*/
getWaveMonsterCount(waveNumber: number): number {
if (waveNumber < 1) return 0;
if (TestModeConfig.enable) {
return Math.max(1, TestModeConfig.spawnCount || 1);
}
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
return Math.min(cfg.base_count, 12);
}
/**
* 获取波次槽位配置(向后兼容接口)
* 内部从 generateWave 反推,仅用于历史调用方
* @param waveNumber 目标波数
*/
getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
const generated = this.generateWave(waveNumber);
const slotMap = new Map<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: [] });
}
}
return Array.from(slotMap.entries()).map(([type, data]) => ({
type,
count: data.count,
...(data.affixes.length > 0 ? { affixes: data.affixes } : {}),
}));
}
// ======================== 私有生成子算法 ========================
/** 测试模式:完全绕过引擎逻辑 */
private generateTestWave(waveNumber: number): GeneratedMonster[] {
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
const count = Math.max(1, TestModeConfig.spawnCount || 1);
const monsters: GeneratedMonster[] = [];
for (let i = 0; i < count; i++) {
monsters.push({
uuid: TestModeConfig.monUuid,
type: TestModeConfig.monType,
hp: Math.round(TestModeConfig.baseHp * growth),
ap: Math.round(TestModeConfig.baseAp * growth),
affixes: [...TestModeConfig.affixes],
isBoss: false,
spawnIndex: i,
elite: TestModeConfig.testElite,
wave_enchants: TestModeConfig.testWaveEnchant ? [TestModeConfig.testWaveEnchant] : [],
testSkills: {
skill: TestModeConfig.skill,
atking: TestModeConfig.atking,
atked: TestModeConfig.atked,
dead: TestModeConfig.dead,
fstart: TestModeConfig.fstart,
fend: TestModeConfig.fend,
},
});
}
return monsters;
}
/**
* 小队拼装:按权重从 squad_pool 抽小队,累计到 target 数量
* 超出部分按剩余位截断
*/
private assembleSquads(pool: string[], target: number, wave: number): GeneratedMonster[] {
const result: GeneratedMonster[] = [];
let filled = 0;
let safety = 20; // 防御性循环上限
while (filled < target && safety-- > 0) {
const squad = this.pickWeightedSquad(pool);
if (!squad) break;
const remaining = target - filled;
for (const slot of squad.slots) {
for (let i = 0; i < slot.count && filled < remaining; i++) {
result.push(this.makeMonster(slot.type, wave, filled));
filled++;
}
if (filled >= remaining) break;
}
}
return result;
}
/** 生成 Boss首位 */
private makeBoss(wave: number): GeneratedMonster {
const isMeleeBoss = Math.random() < 0.5;
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
// Boss 类型兜底:若 MonList 缺失则退回 MeleeBoss
let uuids = MonList[type];
if (!uuids || uuids.length === 0) {
uuids = MonList[MonType.MeleeBoss] || MonList[MonType.Melee] || [6001];
}
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
const baseInfo = HeroInfo[uuid];
const baseHp = baseInfo ? baseInfo.hp : 500;
const baseAp = baseInfo ? baseInfo.ap : 30;
// Boss 自带额外血量加成:每 5 波递增 20%wave 5=1.0, 10=1.2, 15=1.4...
const bossBonusHpMul = 1 + 0.20 * Math.floor(Math.max(0, wave - 5) / 5);
return {
uuid,
type,
hp: Math.round(baseHp * bossBonusHpMul),
ap: baseAp,
affixes: [], // 兼容字段
isBoss: true,
spawnIndex: 0,
wave_enchants: [], // 后续统一填充
};
}
/** 生成普通怪物 */
private makeMonster(type: MonType, wave: number, spawnIndex: number): GeneratedMonster {
let uuids = MonList[type];
if (!uuids || uuids.length === 0) {
// 兜底 Melee
uuids = MonList[MonType.Melee] || [6001];
}
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
const baseInfo = HeroInfo[uuid];
const baseHp = baseInfo ? baseInfo.hp : 100;
const baseAp = baseInfo ? baseInfo.ap : 10;
return {
uuid,
type,
hp: baseHp,
ap: baseAp,
affixes: [], // 兼容字段
isBoss: false,
spawnIndex,
wave_enchants: [], // 后续统一填充
};
}
/** 抽取波次级强化(按权重抽 0~2 个,不重复) */
private pickEnchants(pool?: string[]): WaveEnchant[] {
if (!pool || pool.length === 0) return [];
const enchants: WaveEnchant[] = [];
// 第一个 Enchant: 70% 概率抽 1 个
if (Math.random() < 0.7) {
const first = this.pickWeightedEnchant(pool);
if (first) enchants.push(first);
}
// 第二个 Enchant: 30% 概率再抽 1 个(不重复)
if (Math.random() < 0.3 && pool.length > 1) {
const remaining = pool.filter(id => !enchants.some(e => e.id === id));
const second = this.pickWeightedEnchant(remaining);
if (second) enchants.push(second);
}
return enchants;
}
/** 抽取个体强化(等概率随机 1 种) */
private pickElite(): MonsterElite {
const candidates: MonsterElite[] = [
MonsterElite.Elite,
MonsterElite.Berserk,
MonsterElite.Shield,
MonsterElite.Swift,
MonsterElite.Regen,
];
return candidates[Math.floor(Math.random() * candidates.length)];
}
/** 应用个体强化到怪物 */
private applyElite(m: GeneratedMonster, elite: MonsterElite): void {
const def = MonsterEliteSet[elite];
m.hp = Math.max(1, Math.round(m.hp * def.hp_mul));
m.ap = Math.max(1, Math.round(m.ap * def.ap_mul));
m.elite = elite;
}
/** 按权重从小队 id 池抽 1 个小队 */
private pickWeightedSquad(pool: string[]): SquadConfig | null {
const valid = pool.map(id => SquadLibrary[id]).filter(s => s);
if (valid.length === 0) return null;
return this.pickWeighted(valid);
}
/** 按权重从强化 id 池抽 1 个强化 */
private pickWeightedEnchant(pool: string[]): WaveEnchant | null {
const valid = pool.map(id => WaveEnchantLibrary[id]).filter(e => e);
if (valid.length === 0) return null;
return this.pickWeighted(valid);
}
/** 通用加权随机抽取 */
private pickWeighted<T extends { weight: number }>(items: T[]): T | null {
if (items.length === 0) return null;
const total = items.reduce((s, it) => s + it.weight, 0);
if (total <= 0) return items[Math.floor(Math.random() * items.length)];
let r = Math.random() * total;
for (const it of items) {
r -= it.weight;
if (r <= 0) return it;
}
return items[items.length - 1];
}
}
// ======================== 11. 全局单例 & 向后兼容导出 ========================
export const spawningEngine = new RogueSpawningEngine();
/** 历史接口:波次槽位(保留兼容) */
export interface IWaveSlot {
type: number;
count: number;
/** @deprecated 已废弃,新接口不再使用 */
affixes?: AffixType[];
}
/**
* 获取指定波次预计的怪物总数量
* @param waveNumber 目标波数
*/
export function getWaveMonsterCount(waveNumber: number): number {
return spawningEngine.getWaveMonsterCount(waveNumber);
}
/** 获取波次槽位配置(向后兼容) */
export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
return spawningEngine.getWaveSlotConfig(waveNumber);
}
export const DefaultWaveSlot: IWaveSlot[] = [
{ type: MonType.Melee, count: 20 },
{ type: MonType.Long, count: 15 },
{ type: MonType.Support, count: 5 },
];
/** 波次槽位配置代理(按需生成) */
export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = new Proxy(
{} as { [wave: number]: IWaveSlot[] },
{
get(_target, prop: string) {
const wave = parseInt(prop, 10);
if (!isNaN(wave) && wave >= 1) {
return spawningEngine.getWaveSlotConfig(wave);
}
if (prop === "toJSON") return () => ({});
return undefined;
},
has(_target, prop: string) {
const wave = parseInt(prop, 10);
return !isNaN(wave) && wave >= 1;
},
}
);