Compare commits
2 Commits
9ce8aac12f
...
279e239e3a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
279e239e3a | ||
|
|
251caf0b36 |
@@ -1,38 +1,71 @@
|
||||
/**
|
||||
* @file RogueConfig.ts
|
||||
* @description 肉鸽刷怪系统 (重构精简版)
|
||||
* @description 肉鸽刷怪系统(基于硬编码规则 + 模块化随机)
|
||||
*
|
||||
* 核心规则:
|
||||
* 1. 最大怪物数量 12 个
|
||||
* 2. 1-4 波:怪物数量逐步增加(如 1, 2, 4, 6)
|
||||
* 3. 第 5 波:出现第一个 Boss
|
||||
* 4. 5 波之后:随机出现 6-12 个怪物
|
||||
* 5. 强度:仅通过怪物数量和波次(等级)来逐步提升,每波增加一定比例基础属性。
|
||||
* 设计层次:
|
||||
* 1. MonsterElite - 个体强化(5 种)
|
||||
* 2. WaveEnchant - 波次级强化(环境修饰)
|
||||
* 3. SquadConfig - 小队模板(3-4 只怪的组合单元)
|
||||
* 4. WaveConfig - 每波硬编码(基础数 + 小队池 + 强化池)
|
||||
* 5. RogueSpawningEngine - 生成引擎(按规则组合上述配置)
|
||||
*
|
||||
* 详细设计见 docs/superpowers/specs/2026-07-05-rogue-config-refactor-design.md
|
||||
*/
|
||||
|
||||
import { HeroInfo, MonType, MonTypeName } from "../common/config/heroSet";
|
||||
import { FacSet } from "../common/config/GameSet";
|
||||
|
||||
// ======================== 词缀类型枚举 ========================
|
||||
// ======================== 1. 枚举与基础类型 ========================
|
||||
|
||||
/**
|
||||
* 个体强化类型(精选 5 种)
|
||||
* 设计权衡:HeroAttrsComp 暂无 atk_cd / regen 字段,因此 Swift/Regen 简化为数值差异化。
|
||||
* 待 HeroAttrsComp 升级后可启用 cd_mul / regen 扩展点。
|
||||
*/
|
||||
export enum MonsterElite {
|
||||
Elite = 0, // 精英:全面强化
|
||||
Berserk = 1, // 狂暴:高攻低血
|
||||
Shield = 2, // 护盾:坦克
|
||||
Regen = 3, // 再生:高血偏弱攻
|
||||
Swift = 4, // 疾风:平衡偏输出
|
||||
}
|
||||
|
||||
/** 个体强化效果定义 */
|
||||
export const MonsterEliteSet: Record<MonsterElite, {
|
||||
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,
|
||||
Elite = 0,
|
||||
Berserk = 1,
|
||||
Shield = 2,
|
||||
Regen = 3,
|
||||
Swift = 4,
|
||||
Giant = 5,
|
||||
Chain = 6,
|
||||
SummonerA = 7,
|
||||
CritRes = 8,
|
||||
FreezeRes = 9,
|
||||
KnockbackRes = 10,
|
||||
}
|
||||
|
||||
// ======================== 怪物 UUID 池 ========================
|
||||
// ======================== 2. 怪物 UUID 池 ========================
|
||||
|
||||
/** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */
|
||||
export const MonList: Record<number, number[]> = {};
|
||||
// 动态从 HeroInfo 中提取怪物分类,避免硬编码
|
||||
for (const key in HeroInfo) {
|
||||
const info = HeroInfo[key];
|
||||
if (info.fac === FacSet.MON && info.monType !== undefined) {
|
||||
@@ -43,7 +76,212 @@ for (const key in HeroInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 测试模式配置 ========================
|
||||
// ======================== 3. 波次级强化库 ========================
|
||||
|
||||
/**
|
||||
* 波次级强化(环境修饰符)
|
||||
* 作用于整波怪物,与 MonsterElite(个体)解耦。
|
||||
*/
|
||||
export interface WaveEnchant {
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
hp_mul?: number; // 全员 HP 乘子
|
||||
ap_mul?: number; // 全员 AP 乘子
|
||||
cd_mul?: number; // 全员攻击间隔乘子(后续扩展点)
|
||||
elite_rate_mul?: number; // 该波个体强化出现概率乘子
|
||||
weight: number; // 在强化池中的抽取权重
|
||||
}
|
||||
|
||||
/** 波次强化库(硬编码) */
|
||||
export const WaveEnchantLibrary: Record<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: "个体强化率×2,HP+15%", elite_rate_mul: 2.0, hp_mul: 1.15, weight: 5 },
|
||||
fortress: { id: "fortress", name: "钢铁堡垒", desc: "全员 HP+60% AP-15%", hp_mul: 1.60, ap_mul: 0.85, weight: 6 },
|
||||
};
|
||||
|
||||
// ======================== 4. 小队模板库 ========================
|
||||
|
||||
/** 小队内单种怪物的槽位定义 */
|
||||
export interface SquadSlot {
|
||||
type: MonType;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** 小队模板(3-4 只怪的组合单元) */
|
||||
export interface SquadConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
slots: SquadSlot[];
|
||||
weight: number; // 在小队池中的抽取权重
|
||||
}
|
||||
|
||||
/** 小队模板库(硬编码) */
|
||||
export const SquadLibrary: Record<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, // 默认关闭测试模式
|
||||
@@ -52,167 +290,113 @@ export const TestModeConfig = {
|
||||
growthRatePerWave: 0.2,
|
||||
monType: MonType.Melee,
|
||||
monUuid: 6001,
|
||||
/** @deprecated 已被 testElite 取代 */
|
||||
affixes: [] as AffixType[],
|
||||
spawnCount: 1,
|
||||
|
||||
/** 测试个体强化 */
|
||||
testElite: undefined as MonsterElite | undefined,
|
||||
/** 测试波次强化 id */
|
||||
testWaveEnchant: undefined as string | undefined,
|
||||
|
||||
skill: undefined as { s_uuid: number; cd?: number; overrides?: any } | undefined,
|
||||
atking: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
|
||||
atked: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
|
||||
dead: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
|
||||
fstart: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
|
||||
fend: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined,
|
||||
}
|
||||
};
|
||||
|
||||
// ======================== 生成结果接口 ========================
|
||||
|
||||
export interface GeneratedMonster {
|
||||
uuid: number
|
||||
type: MonType
|
||||
hp: number
|
||||
ap: number
|
||||
affixes: AffixType[]
|
||||
isBoss: boolean
|
||||
spawnIndex: number
|
||||
testSkills?: {
|
||||
skill?: { s_uuid: number; cd?: number; overrides?: any };
|
||||
atking?: { s_uuid: number; t_num: number; overrides?: any }[];
|
||||
atked?: { s_uuid: number; t_num: number; overrides?: any }[];
|
||||
dead?: { s_uuid: number; t_num: number; overrides?: any }[];
|
||||
fstart?: { s_uuid: number; t_num: number; overrides?: any }[];
|
||||
fend?: { s_uuid: number; t_num: number; overrides?: any }[];
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 生成引擎 ========================
|
||||
// ======================== 10. 生成引擎 ========================
|
||||
|
||||
/**
|
||||
* 肉鸽刷怪生成引擎
|
||||
* 按硬编码 WaveConfig 规则,组合小队模板与双层强化系统生成怪物列表
|
||||
*/
|
||||
export class RogueSpawningEngine {
|
||||
|
||||
/**
|
||||
* 生成指定波次的怪物列表
|
||||
* @param waveNumber 波次(1 起,>30 时复用 wave 30 配置)
|
||||
* @returns 怪物列表,长度 ≤ 12
|
||||
*/
|
||||
generateWave(waveNumber: number): GeneratedMonster[] {
|
||||
if (waveNumber < 1) return [];
|
||||
|
||||
// 测试模式拦截
|
||||
// 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪
|
||||
if (TestModeConfig.enable) {
|
||||
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
|
||||
const count = Math.max(1, TestModeConfig.spawnCount || 1);
|
||||
const monsters: GeneratedMonster[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
monsters.push({
|
||||
uuid: TestModeConfig.monUuid,
|
||||
type: TestModeConfig.monType,
|
||||
hp: Math.round(TestModeConfig.baseHp * growth),
|
||||
ap: Math.round(TestModeConfig.baseAp * growth),
|
||||
affixes: [...TestModeConfig.affixes],
|
||||
isBoss: false,
|
||||
spawnIndex: i,
|
||||
testSkills: {
|
||||
skill: TestModeConfig.skill,
|
||||
atking: TestModeConfig.atking,
|
||||
atked: TestModeConfig.atked,
|
||||
dead: TestModeConfig.dead,
|
||||
fstart: TestModeConfig.fstart,
|
||||
fend: TestModeConfig.fend
|
||||
}
|
||||
});
|
||||
}
|
||||
return monsters;
|
||||
return this.generateTestWave(waveNumber);
|
||||
}
|
||||
|
||||
// 1. 确定生成数量
|
||||
let count = this.getWaveMonsterCount(waveNumber);
|
||||
// 1. 取硬编码 WaveConfig(>30 时复用 wave 30 配置)
|
||||
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
|
||||
|
||||
// 第 5 波之后,在 count 的基础上加上随机逻辑 (覆盖原有固定返回值)
|
||||
if (waveNumber > 5 && !TestModeConfig.enable) {
|
||||
count = Math.floor(Math.random() * 7) + 6;
|
||||
}
|
||||
// 2. 抽取波次级强化(按权重从 enchant_pool 抽 0~2 个)
|
||||
const enchants = this.pickEnchants(cfg.enchant_pool);
|
||||
|
||||
// 限制最大怪物数为 12
|
||||
count = Math.min(count, 12);
|
||||
|
||||
// 每 5 波出现一次 Boss
|
||||
const isBossWave = (waveNumber % 5 === 0);
|
||||
// 3. 计算波次最终属性乘子
|
||||
const globalScale = getGlobalScale(waveNumber);
|
||||
const enchantHpMul = enchants.reduce((m, e) => m * (e.hp_mul ?? 1), 1);
|
||||
const enchantApMul = enchants.reduce((m, e) => m * (e.ap_mul ?? 1), 1);
|
||||
const eliteRateMul = enchants.reduce((m, e) => m * (e.elite_rate_mul ?? 1), 1);
|
||||
|
||||
// 4. Boss 位(Boss 波首位占 1 个)
|
||||
const monsters: GeneratedMonster[] = [];
|
||||
|
||||
// 普通怪池
|
||||
const normalTypes = [
|
||||
MonType.Melee, MonType.Heavy, MonType.Long,
|
||||
MonType.Support, MonType.Assassin, MonType.Summoner
|
||||
];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let type: MonType;
|
||||
let isBoss = false;
|
||||
|
||||
if (isBossWave && i === 0) {
|
||||
// 生成 Boss
|
||||
isBoss = true;
|
||||
type = Math.random() > 0.5 ? MonType.MeleeBoss : MonType.LongBoss;
|
||||
} else {
|
||||
// 生成普通小怪
|
||||
type = normalTypes[Math.floor(Math.random() * normalTypes.length)];
|
||||
}
|
||||
|
||||
const uuids = MonList[type] || MonList[MonType.Melee];
|
||||
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
|
||||
|
||||
// 从 HeroInfo 中获取基础属性,如果没找到则给一个兜底值
|
||||
const baseInfo = HeroInfo[uuid];
|
||||
const baseHp = baseInfo ? baseInfo.hp : 100;
|
||||
const baseAp = baseInfo ? baseInfo.ap : 10;
|
||||
|
||||
const finalHp = Math.max(1, Math.round(baseHp));
|
||||
const finalAp = Math.max(1, Math.round(baseAp));
|
||||
|
||||
monsters.push({
|
||||
uuid,
|
||||
type,
|
||||
hp: finalHp,
|
||||
ap: finalAp,
|
||||
affixes: [], // 词缀系统已移除,传入空数组
|
||||
isBoss: isBoss,
|
||||
spawnIndex: i,
|
||||
});
|
||||
let remaining = cfg.base_count;
|
||||
if (cfg.boss_wave) {
|
||||
monsters.push(this.makeBoss(waveNumber));
|
||||
remaining -= 1;
|
||||
}
|
||||
|
||||
return monsters;
|
||||
// 5. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
|
||||
const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, waveNumber);
|
||||
monsters.push(...squadMonsters);
|
||||
|
||||
// 6. 应用全局 & 波次 Enchant 乘子
|
||||
const waveEnchantIds = enchants.map(e => e.id);
|
||||
for (const m of monsters) {
|
||||
m.hp = Math.max(1, Math.round(m.hp * globalScale.hp_mul * enchantHpMul));
|
||||
m.ap = Math.max(1, Math.round(m.ap * globalScale.ap_mul * enchantApMul));
|
||||
m.wave_enchants = waveEnchantIds.slice();
|
||||
}
|
||||
|
||||
// 7. 应用个体 Elite(非 Boss 怪,每只最多 1 个)
|
||||
const eliteBaseRate = cfg.elite_base_rate ?? Math.min(0.05 + waveNumber * 0.01, 0.30);
|
||||
for (const m of monsters) {
|
||||
if (!m.isBoss && Math.random() < eliteBaseRate * eliteRateMul) {
|
||||
const elite = this.pickElite();
|
||||
this.applyElite(m, elite);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 硬上限保护(理论不会触发,但防止配置错误)
|
||||
return monsters.slice(0, 12);
|
||||
}
|
||||
|
||||
/** 重置引擎状态(保留接口兼容,当前无内部状态) */
|
||||
reset(): void {
|
||||
// 重置引擎状态
|
||||
// 无可变状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出一个空函数,用于外部获取/预测某个波次的怪物数量
|
||||
/**
|
||||
* 获取指定波次的怪物总数
|
||||
* @param waveNumber 目标波数
|
||||
* @returns 预计生成的怪物总数
|
||||
* @returns 预计生成的怪物总数(base_count,受 12 上限约束)
|
||||
*/
|
||||
getWaveMonsterCount(waveNumber: number): number {
|
||||
if (waveNumber < 1) return 0;
|
||||
|
||||
if (TestModeConfig.enable) {
|
||||
return Math.max(1, TestModeConfig.spawnCount || 1);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
if (waveNumber === 1) {
|
||||
count = 1;
|
||||
} else if (waveNumber === 2) {
|
||||
count = 2;
|
||||
} else if (waveNumber === 3) {
|
||||
count = 4;
|
||||
} else if (waveNumber === 4) {
|
||||
count = 6;
|
||||
} else if (waveNumber === 5) {
|
||||
count = 6;
|
||||
} else {
|
||||
// 注意:由于后续波次是 6-12 随机,这里可以返回一个平均值或特定的空逻辑
|
||||
// 根据需求,暂时返回固定的 6,或者保留外部自己实现逻辑的口子
|
||||
count = 6;
|
||||
}
|
||||
return Math.min(count, 12);
|
||||
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
|
||||
return Math.min(cfg.base_count, 12);
|
||||
}
|
||||
|
||||
// 向后兼容接口
|
||||
/**
|
||||
* 获取波次槽位配置(向后兼容接口)
|
||||
* 内部从 generateWave 反推,仅用于历史调用方
|
||||
* @param waveNumber 目标波数
|
||||
*/
|
||||
getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
||||
const generated = this.generateWave(waveNumber);
|
||||
const slotMap = new Map<number, { count: number; affixes: AffixType[] }>();
|
||||
@@ -222,7 +406,7 @@ export class RogueSpawningEngine {
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
} else {
|
||||
slotMap.set(m.type, { count: 1, affixes: m.affixes });
|
||||
slotMap.set(m.type, { count: 1, affixes: [] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,19 +416,202 @@ export class RogueSpawningEngine {
|
||||
...(data.affixes.length > 0 ? { affixes: data.affixes } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
// ======================== 私有生成子算法 ========================
|
||||
|
||||
/** 测试模式:完全绕过引擎逻辑 */
|
||||
private generateTestWave(waveNumber: number): GeneratedMonster[] {
|
||||
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave;
|
||||
const count = Math.max(1, TestModeConfig.spawnCount || 1);
|
||||
const monsters: GeneratedMonster[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
monsters.push({
|
||||
uuid: TestModeConfig.monUuid,
|
||||
type: TestModeConfig.monType,
|
||||
hp: Math.round(TestModeConfig.baseHp * growth),
|
||||
ap: Math.round(TestModeConfig.baseAp * growth),
|
||||
affixes: [...TestModeConfig.affixes],
|
||||
isBoss: false,
|
||||
spawnIndex: i,
|
||||
elite: TestModeConfig.testElite,
|
||||
wave_enchants: TestModeConfig.testWaveEnchant ? [TestModeConfig.testWaveEnchant] : [],
|
||||
testSkills: {
|
||||
skill: TestModeConfig.skill,
|
||||
atking: TestModeConfig.atking,
|
||||
atked: TestModeConfig.atked,
|
||||
dead: TestModeConfig.dead,
|
||||
fstart: TestModeConfig.fstart,
|
||||
fend: TestModeConfig.fend,
|
||||
},
|
||||
});
|
||||
}
|
||||
return monsters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小队拼装:按权重从 squad_pool 抽小队,累计到 target 数量
|
||||
* 超出部分按剩余位截断
|
||||
*/
|
||||
private assembleSquads(pool: string[], target: number, wave: number): GeneratedMonster[] {
|
||||
const result: GeneratedMonster[] = [];
|
||||
let filled = 0;
|
||||
let safety = 20; // 防御性循环上限
|
||||
|
||||
while (filled < target && safety-- > 0) {
|
||||
const squad = this.pickWeightedSquad(pool);
|
||||
if (!squad) break;
|
||||
|
||||
const remaining = target - filled;
|
||||
for (const slot of squad.slots) {
|
||||
for (let i = 0; i < slot.count && filled < remaining; i++) {
|
||||
result.push(this.makeMonster(slot.type, wave, filled));
|
||||
filled++;
|
||||
}
|
||||
if (filled >= remaining) break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 生成 Boss(首位) */
|
||||
private makeBoss(wave: number): GeneratedMonster {
|
||||
const isMeleeBoss = Math.random() < 0.5;
|
||||
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
||||
|
||||
// Boss 类型兜底:若 MonList 缺失则退回 MeleeBoss
|
||||
let uuids = MonList[type];
|
||||
if (!uuids || uuids.length === 0) {
|
||||
uuids = MonList[MonType.MeleeBoss] || MonList[MonType.Melee] || [6001];
|
||||
}
|
||||
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
|
||||
|
||||
const baseInfo = HeroInfo[uuid];
|
||||
const baseHp = baseInfo ? baseInfo.hp : 500;
|
||||
const baseAp = baseInfo ? baseInfo.ap : 30;
|
||||
|
||||
// Boss 自带额外血量加成:每 5 波递增 20%(wave 5=1.0, 10=1.2, 15=1.4...)
|
||||
const bossBonusHpMul = 1 + 0.20 * Math.floor(Math.max(0, wave - 5) / 5);
|
||||
|
||||
return {
|
||||
uuid,
|
||||
type,
|
||||
hp: Math.round(baseHp * bossBonusHpMul),
|
||||
ap: baseAp,
|
||||
affixes: [], // 兼容字段
|
||||
isBoss: true,
|
||||
spawnIndex: 0,
|
||||
wave_enchants: [], // 后续统一填充
|
||||
};
|
||||
}
|
||||
|
||||
/** 生成普通怪物 */
|
||||
private makeMonster(type: MonType, wave: number, spawnIndex: number): GeneratedMonster {
|
||||
let uuids = MonList[type];
|
||||
if (!uuids || uuids.length === 0) {
|
||||
// 兜底 Melee
|
||||
uuids = MonList[MonType.Melee] || [6001];
|
||||
}
|
||||
const uuid = uuids[Math.floor(Math.random() * uuids.length)];
|
||||
|
||||
const baseInfo = HeroInfo[uuid];
|
||||
const baseHp = baseInfo ? baseInfo.hp : 100;
|
||||
const baseAp = baseInfo ? baseInfo.ap : 10;
|
||||
|
||||
return {
|
||||
uuid,
|
||||
type,
|
||||
hp: baseHp,
|
||||
ap: baseAp,
|
||||
affixes: [], // 兼容字段
|
||||
isBoss: false,
|
||||
spawnIndex,
|
||||
wave_enchants: [], // 后续统一填充
|
||||
};
|
||||
}
|
||||
|
||||
/** 抽取波次级强化(按权重抽 0~2 个,不重复) */
|
||||
private pickEnchants(pool?: string[]): WaveEnchant[] {
|
||||
if (!pool || pool.length === 0) return [];
|
||||
const enchants: WaveEnchant[] = [];
|
||||
|
||||
// 第一个 Enchant: 70% 概率抽 1 个
|
||||
if (Math.random() < 0.7) {
|
||||
const first = this.pickWeightedEnchant(pool);
|
||||
if (first) enchants.push(first);
|
||||
}
|
||||
|
||||
// 第二个 Enchant: 30% 概率再抽 1 个(不重复)
|
||||
if (Math.random() < 0.3 && pool.length > 1) {
|
||||
const remaining = pool.filter(id => !enchants.some(e => e.id === id));
|
||||
const second = this.pickWeightedEnchant(remaining);
|
||||
if (second) enchants.push(second);
|
||||
}
|
||||
|
||||
return enchants;
|
||||
}
|
||||
|
||||
/** 抽取个体强化(等概率随机 1 种) */
|
||||
private pickElite(): MonsterElite {
|
||||
const candidates: MonsterElite[] = [
|
||||
MonsterElite.Elite,
|
||||
MonsterElite.Berserk,
|
||||
MonsterElite.Shield,
|
||||
MonsterElite.Swift,
|
||||
MonsterElite.Regen,
|
||||
];
|
||||
return candidates[Math.floor(Math.random() * candidates.length)];
|
||||
}
|
||||
|
||||
/** 应用个体强化到怪物 */
|
||||
private applyElite(m: GeneratedMonster, elite: MonsterElite): void {
|
||||
const def = MonsterEliteSet[elite];
|
||||
m.hp = Math.max(1, Math.round(m.hp * def.hp_mul));
|
||||
m.ap = Math.max(1, Math.round(m.ap * def.ap_mul));
|
||||
m.elite = elite;
|
||||
}
|
||||
|
||||
/** 按权重从小队 id 池抽 1 个小队 */
|
||||
private pickWeightedSquad(pool: string[]): SquadConfig | null {
|
||||
const valid = pool.map(id => SquadLibrary[id]).filter(s => s);
|
||||
if (valid.length === 0) return null;
|
||||
return this.pickWeighted(valid);
|
||||
}
|
||||
|
||||
/** 按权重从强化 id 池抽 1 个强化 */
|
||||
private pickWeightedEnchant(pool: string[]): WaveEnchant | null {
|
||||
const valid = pool.map(id => WaveEnchantLibrary[id]).filter(e => e);
|
||||
if (valid.length === 0) return null;
|
||||
return this.pickWeighted(valid);
|
||||
}
|
||||
|
||||
/** 通用加权随机抽取 */
|
||||
private pickWeighted<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
|
||||
affixes?: AffixType[]
|
||||
type: number;
|
||||
count: number;
|
||||
/** @deprecated 已废弃,新接口不再使用 */
|
||||
affixes?: AffixType[];
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 获取指定波次预计的怪物总数量
|
||||
* @param waveNumber 目标波数
|
||||
*/
|
||||
@@ -252,6 +619,7 @@ export function getWaveMonsterCount(waveNumber: number): number {
|
||||
return spawningEngine.getWaveMonsterCount(waveNumber);
|
||||
}
|
||||
|
||||
/** 获取波次槽位配置(向后兼容) */
|
||||
export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] {
|
||||
return spawningEngine.getWaveSlotConfig(waveNumber);
|
||||
}
|
||||
@@ -262,6 +630,7 @@ export const DefaultWaveSlot: IWaveSlot[] = [
|
||||
{ type: MonType.Support, count: 5 },
|
||||
];
|
||||
|
||||
/** 波次槽位配置代理(按需生成) */
|
||||
export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = new Proxy(
|
||||
{} as { [wave: number]: IWaveSlot[] },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
# RogueConfig 重构设计:从纯随机到规则化随机组合
|
||||
|
||||
- **创建日期**: 2026-07-05
|
||||
- **状态**: 已批准(待实施)
|
||||
- **作用域**: 重构 `assets/script/game/map/RogueConfig.ts` 及配套调用方
|
||||
- **目标**: 把肉鸽刷怪系统从「每只怪独立随机」改造为「在硬编码规则内的随机组合变动」
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
当前 [RogueConfig.ts](../../../assets/script/game/map/RogueConfig.ts) 的 `RogueSpawningEngine.generateWave()` 存在以下问题:
|
||||
|
||||
1. **纯随机生成**:每只怪独立从 6 种普通类型中 `Math.random()` 抽取,缺乏组合设计感。
|
||||
2. **波次无节奏**:5 波之后纯粹 6-12 随机数,难度曲线只靠数量与 `growthRatePerWave` 撑场。
|
||||
3. **强化系统空转**:`AffixType` 枚举定义 11 种词缀,但生成时 `affixes: []` 始终为空。
|
||||
4. **可配置性差**:策划无法通过修改配置数据调整某一波的怪物组合与强化。
|
||||
|
||||
## 2. 重构目标
|
||||
|
||||
1. **新增多个怪物组合配置**:以「小队(Squad)」为单位硬编码多种组合模板。
|
||||
2. **硬编码多个强化配置**:分为「波次级环境强化(WaveEnchant)」+「个体精英强化(MonsterElite)」双层。
|
||||
3. **硬编码每波次配置**:每波包含 `基础怪物数 + 可选小队池 + 可选强化池` 三字段,组合方式可控。
|
||||
4. **保持接口兼容**:调用方 [MissionMonComp.ts](../../../assets/script/game/map/MissionMonComp.ts) 改动最小化。
|
||||
|
||||
## 3. 设计决策
|
||||
|
||||
| 决策点 | 选择 | 理由 |
|
||||
|---|---|---|
|
||||
| 组合颗粒度 | **小队模块化**(3-4 只怪/小队) | 每波拼装 2-4 个小队,变化丰富且可控 |
|
||||
| 强化层级 | **双层混合**(波次级 + 个体级) | 既保留波次"环境"主题感,又有单体惊喜 |
|
||||
| 波次配置组织 | **按波次逐条硬编码** | 每波字段完整独立,调参最直接 |
|
||||
| 硬编码范围 | **wave 1~30 + 超出复用 wave 30** | 长线体验,超出后由全局乘子维持难度增长 |
|
||||
| 个体强化种类 | **精选 5 种**(Elite/Berserk/Shield/Swift/Regen) | 数量适中,调参简单 |
|
||||
| Boss 节奏 | **每 5 波一个 Boss**(5/10/15/20/25/30) | 沿用现有仪式感 |
|
||||
| 接口策略 | **同时重构接口** | 数据更干净,调用方改动可控 |
|
||||
| 难度缩放 | **叠加波次级 HP/AP 乘子**(每 5 波 +15% HP / +8% AP) | 与数量、强化叠加形成明显难度曲线 |
|
||||
|
||||
## 4. 整体架构与数据流
|
||||
|
||||
```
|
||||
WaveConfig (硬编码 30 波)
|
||||
├─ base_count 基础怪物数
|
||||
├─ squad_pool 小队池 (引用 SquadConfig.id)
|
||||
└─ enchant_pool 波次强化池 (引用 WaveEnchant.id)
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ RogueSpawningEngine.generateWave(n) │
|
||||
│ 1. 取 WaveConfig │
|
||||
│ 2. 计算全局 hp/ap 乘子 (波次递进 + Enchant) │
|
||||
│ 3. 按权重从 squad_pool 抽小队拼装到 base_count │
|
||||
│ 4. Boss 波首位放 Boss │
|
||||
│ 5. 按 enchant_pool 抽 0~2 个 WaveEnchant │
|
||||
│ 6. 每只小怪按概率抽个体 MonsterElite │
|
||||
└───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
GeneratedMonster[] (扩展接口)
|
||||
│
|
||||
▼
|
||||
MissionMonComp.addMonsterAtGrid
|
||||
(读取 hp/ap 应用到 HeroAttrsComp)
|
||||
```
|
||||
|
||||
**核心约束**:
|
||||
- 单波总怪数 ≤ 12(网格上限,3 行 4 列)
|
||||
- Boss 占首位
|
||||
- 超过 wave 30 用第 30 波配置 + 持续递增的全局乘子
|
||||
|
||||
## 5. 数据结构定义
|
||||
|
||||
### 5.1 个体强化(MonsterElite)
|
||||
|
||||
> **HeroAttrsComp 字段适配发现**:经检查 [HeroAttrsComp.ts](../../../assets/script/game/hero/HeroAttrsComp.ts) 无 `atk_cd` / `regen_per_sec` 字段。本次先不改造 HeroAttrsComp,Swift/Regen 简化为「数值差异化」,cd_mul 与 regen 作为后续扩展点留口子。
|
||||
|
||||
```typescript
|
||||
export enum MonsterElite {
|
||||
Elite = 0, // 精英:全面强化
|
||||
Berserk = 1, // 狂暴:高攻低血
|
||||
Shield = 2, // 护盾:坦克
|
||||
Swift = 4, // 疾风:平衡偏输出
|
||||
Regen = 3, // 再生:高血偏弱攻
|
||||
}
|
||||
|
||||
export const MonsterEliteSet: Record<MonsterElite, {
|
||||
name: string;
|
||||
hp_mul: number;
|
||||
ap_mul: number;
|
||||
}> = {
|
||||
[MonsterElite.Elite]: { name: "精英", hp_mul: 1.5, ap_mul: 1.3 },
|
||||
[MonsterElite.Berserk]: { name: "狂暴", hp_mul: 0.9, ap_mul: 1.6 },
|
||||
[MonsterElite.Shield]: { name: "护盾", hp_mul: 1.8, ap_mul: 0.8 },
|
||||
[MonsterElite.Swift]: { name: "疾风", hp_mul: 1.0, ap_mul: 1.15 },
|
||||
[MonsterElite.Regen]: { name: "再生", hp_mul: 1.3, ap_mul: 0.95 },
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 波次级强化(WaveEnchant)
|
||||
|
||||
```typescript
|
||||
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: "个体强化率×2,HP+15%", elite_rate_mul: 2.0, hp_mul: 1.15, weight: 5 },
|
||||
fortress: { id: "fortress", name: "钢铁堡垒", desc: "全员 HP+60% AP-15%", hp_mul: 1.60, ap_mul: 0.85, weight: 6 },
|
||||
};
|
||||
```
|
||||
|
||||
### 5.3 小队模板(SquadConfig)
|
||||
|
||||
```typescript
|
||||
export interface SquadSlot {
|
||||
type: MonType;
|
||||
count: number;
|
||||
elite_chance?: number; // 0~1,小队成员出现个体强化的概率(默认 0)
|
||||
elite_pool?: MonsterElite[]; // 可选的个体强化池
|
||||
}
|
||||
|
||||
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.4 波次配置(WaveConfig)
|
||||
|
||||
```typescript
|
||||
export interface WaveConfig {
|
||||
base_count: number; // 该波基础怪物数 (1~12)
|
||||
squad_pool: string[]; // 可选小队 id 池
|
||||
enchant_pool?: string[]; // 可选波次强化 id 池(可空表示无强化)
|
||||
boss_wave?: boolean; // 是否 Boss 波
|
||||
elite_base_rate?: number; // 该波个体强化基础概率(默认按波次递增)
|
||||
}
|
||||
|
||||
export const WaveConfigs: Record<number, WaveConfig> = {
|
||||
// 1-4: 教学期
|
||||
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"] },
|
||||
// 5: 第一 Boss
|
||||
5: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], boss_wave: true },
|
||||
// 6-9: 引入波次强化
|
||||
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 },
|
||||
// 10: 第二 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-14: 组合多样化
|
||||
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 },
|
||||
// 15: 第三 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-24: 高压阶段
|
||||
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 },
|
||||
// 25: 第四 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-29: 终极阶段
|
||||
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 },
|
||||
// 30: 最终 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 },
|
||||
};
|
||||
```
|
||||
|
||||
### 5.5 全局难度缩放
|
||||
|
||||
```typescript
|
||||
export function getGlobalScale(wave: number): { hp_mul: number; ap_mul: number } {
|
||||
const step = Math.floor((wave - 1) / 5); // 每 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
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**最终 HP 公式**:`HeroInfo[uuid].hp × 全局乘子 × 波次Enchant乘子 × 个体Elite乘子`
|
||||
|
||||
## 6. 生成引擎算法
|
||||
|
||||
### 6.1 主流程伪代码
|
||||
|
||||
```typescript
|
||||
generateWave(waveNumber: number): GeneratedMonster[] {
|
||||
// 1. 取硬编码 WaveConfig (wave > 30 时使用第 30 波配置)
|
||||
const cfg = WaveConfigs[Math.min(waveNumber, 30)];
|
||||
|
||||
// 2. 抽取波次级强化(按权重从 enchant_pool 抽 0~2 个)
|
||||
const enchants = pickEnchants(cfg.enchant_pool);
|
||||
|
||||
// 3. 计算波次最终属性乘子
|
||||
const globalScale = getGlobalScale(waveNumber);
|
||||
const enchantHpMul = product(enchants.map(e => e.hp_mul ?? 1));
|
||||
const enchantApMul = product(enchants.map(e => e.ap_mul ?? 1));
|
||||
const eliteRateMul = product(enchants.map(e => e.elite_rate_mul ?? 1));
|
||||
|
||||
// 4. Boss 位(Boss 波首位占 1 个)
|
||||
let monsters: GeneratedMonster[] = [];
|
||||
let remaining = cfg.base_count;
|
||||
if (cfg.boss_wave) {
|
||||
monsters.push(makeBoss(waveNumber));
|
||||
remaining -= 1;
|
||||
}
|
||||
|
||||
// 5. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining
|
||||
monsters.push(...assembleSquads(cfg.squad_pool, remaining, waveNumber));
|
||||
|
||||
// 6. 应用全局 & 波次 Enchant 乘子
|
||||
for (const m of monsters) {
|
||||
m.hp = Math.round(m.hp * globalScale.hp_mul * enchantHpMul);
|
||||
m.ap = Math.round(m.ap * globalScale.ap_mul * enchantApMul);
|
||||
m.wave_enchants = enchants.map(e => e.id);
|
||||
}
|
||||
|
||||
// 7. 应用个体 Elite(非 Boss 怪)
|
||||
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 = pickElite(m);
|
||||
applyElite(m, elite);
|
||||
m.elite = elite;
|
||||
}
|
||||
}
|
||||
|
||||
return monsters.slice(0, 12); // 硬上限保护
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 关键子算法
|
||||
|
||||
#### assembleSquads(小队拼装)
|
||||
|
||||
```typescript
|
||||
function assembleSquads(pool: string[], target: number, wave: number): GeneratedMonster[] {
|
||||
const result: GeneratedMonster[] = [];
|
||||
let filled = 0;
|
||||
let safety = 20; // 防止死循环
|
||||
|
||||
while (filled < target && safety-- > 0) {
|
||||
const squad = pickWeighted(pool.map(id => SquadLibrary[id]));
|
||||
const remaining = target - filled;
|
||||
|
||||
for (const slot of squad.slots) {
|
||||
for (let i = 0; i < slot.count && filled < remaining; i++) {
|
||||
result.push(makeMonster(slot.type, wave));
|
||||
filled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
- 小队塞不下时按剩余位截断
|
||||
- `safety` 防御性循环上限
|
||||
- 同一波可抽到重复小队(多样性来自权重 + 多次循环)
|
||||
|
||||
#### makeBoss(Boss 生成)
|
||||
|
||||
```typescript
|
||||
function makeBoss(wave: number): GeneratedMonster {
|
||||
const isMeleeBoss = Math.random() < 0.5;
|
||||
const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss;
|
||||
const uuid = pickUuidFromMonList(type);
|
||||
|
||||
// Boss 自带额外血量(基于波次的固定加成)
|
||||
const baseHp = HeroInfo[uuid].hp;
|
||||
const bossBonusHpMul = 1 + 0.20 * Math.floor((wave - 5) / 5); // wave 5=1.0, 10=1.2, 15=1.4...
|
||||
|
||||
return {
|
||||
uuid, type,
|
||||
hp: baseHp * bossBonusHpMul,
|
||||
ap: HeroInfo[uuid].ap,
|
||||
isBoss: true,
|
||||
elite: undefined, // Boss 不带个体强化
|
||||
affixes: [],
|
||||
wave_enchants: [],
|
||||
spawnIndex: 0,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### pickEnchants(波次强化抽取)
|
||||
|
||||
```typescript
|
||||
function pickEnchants(pool?: string[]): WaveEnchant[] {
|
||||
if (!pool || pool.length === 0) return [];
|
||||
const enchants: WaveEnchant[] = [];
|
||||
|
||||
// 第一个 Enchant: 70% 概率抽 1 个
|
||||
if (Math.random() < 0.7) {
|
||||
enchants.push(pickWeighted(pool.map(id => WaveEnchantLibrary[id])));
|
||||
}
|
||||
|
||||
// 第二个 Enchant: 30% 概率再抽 1 个(不重复)
|
||||
if (Math.random() < 0.3 && pool.length > 1) {
|
||||
const second = pickWeighted(
|
||||
pool.filter(id => !enchants.some(e => e.id === id))
|
||||
.map(id => WaveEnchantLibrary[id])
|
||||
);
|
||||
if (second) enchants.push(second);
|
||||
}
|
||||
|
||||
return enchants;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 GeneratedMonster 扩展接口
|
||||
|
||||
```typescript
|
||||
export interface GeneratedMonster {
|
||||
uuid: number;
|
||||
type: MonType;
|
||||
hp: number;
|
||||
ap: number;
|
||||
isBoss: boolean;
|
||||
spawnIndex: number;
|
||||
|
||||
/** @deprecated 已被 elite 取代,始终返回 [],仅向后兼容 */
|
||||
affixes: AffixType[];
|
||||
|
||||
// 新增字段
|
||||
elite?: MonsterElite; // 个体强化(无则 undefined)
|
||||
wave_enchants: string[]; // 该波触发的波次强化 id 列表
|
||||
|
||||
// 测试模式专用(保留原逻辑)
|
||||
testSkills?: { ... };
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 关键设计权衡
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|---|---|---|
|
||||
| Boss 是否可带个体强化 | 否 | Boss 已经有额外血量+波次加成 |
|
||||
| Enchant 是否会叠加同种 | 否 | 第二个 Enchant 过滤已抽过的 id,避免数值爆炸 |
|
||||
| 小队截断 | 按剩余位截断 | 保证总怪数 = `base_count` |
|
||||
| 个体 Elite 是否可叠加 | 否(每只最多 1 个) | 简化逻辑、避免叠加 bug |
|
||||
| wave > 30 行为 | 复用 wave 30 配置 + 全局乘子继续增长 | 满足"长线"需求 |
|
||||
|
||||
## 7. 错误处理与配置校验
|
||||
|
||||
### 7.1 启动校验
|
||||
|
||||
```typescript
|
||||
export function validateRogueConfig(): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 1. 校验 WaveConfigs 中所有 squad_pool / enchant_pool 引用存在
|
||||
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} 在 MonList 中无可用 uuid`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 防御性兜底
|
||||
|
||||
| 异常情况 | 兜底行为 |
|
||||
|---|---|
|
||||
| `WaveConfigs[wave]` 不存在 | 使用 `WaveConfigs[30]` |
|
||||
| `SquadLibrary[id]` 不存在 | 跳过该小队,记录 warning |
|
||||
| `MonList[type]` 为空 | 使用 `MonList[MonType.Melee]` 兜底 |
|
||||
| 单次 uuid 抽取失败 | 使用 `uuids[0]` 兜底 |
|
||||
| 小队拼装超过 12 只 | 在主流程末尾统一 `slice(0, 12)` |
|
||||
|
||||
## 8. 向后兼容性
|
||||
|
||||
### 8.1 AffixType 枚举处理
|
||||
|
||||
```typescript
|
||||
/** @deprecated 已被 MonsterElite 取代,仅保留导出避免破坏引用 */
|
||||
export enum AffixType { ... }
|
||||
```
|
||||
|
||||
经 Grep 确认 `AffixType` 仅在 RogueConfig.ts 内部使用。
|
||||
|
||||
### 8.2 接口保留
|
||||
|
||||
以下导出保留,调用方不受影响:
|
||||
- `spawningEngine` 单例
|
||||
- `getWaveMonsterCount(wave)` / `getWaveSlotConfig(wave)`
|
||||
- `DefaultWaveSlot` / `WaveSlotConfig` (Proxy)
|
||||
- `IWaveSlot` 接口
|
||||
|
||||
### 8.3 调用方影响评估
|
||||
|
||||
| 文件 | 引用情况 | 改造点 |
|
||||
|---|---|---|
|
||||
| [MissionMonComp.ts](../../../assets/script/game/map/MissionMonComp.ts) | `spawningEngine.generateWave()` / `GeneratedMonster` | 仅读 hp/ap/isBoss,无字段依赖变更,**零改动** |
|
||||
| [MissionComp.ts](../../../assets/script/game/map/MissionComp.ts) | `spawningEngine.reset()` | **零改动** |
|
||||
|
||||
## 9. 测试模式适配
|
||||
|
||||
`TestModeConfig` 保留原拦截逻辑,仅扩展 2 个可选字段:
|
||||
|
||||
```typescript
|
||||
export const TestModeConfig = {
|
||||
enable: false,
|
||||
// ... 原字段保留
|
||||
affixes: [] as AffixType[],
|
||||
|
||||
// 新增:测试个体/波次强化的快速开关
|
||||
testElite?: MonsterElite;
|
||||
testWaveEnchant?: string;
|
||||
};
|
||||
```
|
||||
|
||||
测试模式生成的怪物补充新字段(`elite` / `wave_enchants`),保持接口一致性。
|
||||
|
||||
## 10. 文件结构(重构后骨架)
|
||||
|
||||
```
|
||||
RogueConfig.ts
|
||||
├─ 1. 文件头注释
|
||||
├─ 2. 枚举与基础类型
|
||||
│ ├─ MonsterElite + MonsterEliteSet
|
||||
│ ├─ AffixType(废弃保留)
|
||||
│ ├─ GeneratedMonster(扩展)
|
||||
│ └─ IWaveSlot(兼容保留)
|
||||
├─ 3. 怪物池(保留原逻辑)
|
||||
│ └─ MonList(从 HeroInfo 动态提取)
|
||||
├─ 4. 波次强化库
|
||||
│ ├─ WaveEnchant 接口
|
||||
│ └─ WaveEnchantLibrary(硬编码 5 条)
|
||||
├─ 5. 小队模板库
|
||||
│ ├─ SquadSlot / SquadConfig 接口
|
||||
│ └─ SquadLibrary(硬编码 6 条)
|
||||
├─ 6. 波次配置表
|
||||
│ ├─ WaveConfig 接口
|
||||
│ └─ WaveConfigs(硬编码 wave 1~30)
|
||||
├─ 7. 全局难度缩放
|
||||
│ └─ getGlobalScale(wave)
|
||||
├─ 8. 配置校验
|
||||
│ └─ validateRogueConfig()
|
||||
├─ 9. 生成引擎
|
||||
│ ├─ RogueSpawningEngine(重写)
|
||||
│ │ ├─ generateWave(wave)
|
||||
│ │ ├─ assembleSquads()
|
||||
│ │ ├─ makeBoss()
|
||||
│ │ ├─ makeMonster()
|
||||
│ │ ├─ applyElite()
|
||||
│ │ ├─ pickElite()
|
||||
│ │ ├─ pickEnchants()
|
||||
│ │ └─ pickWeighted()
|
||||
│ ├─ getWaveMonsterCount()(重写,返回 base_count)
|
||||
│ └─ getWaveSlotConfig()(保留,从生成结果反推)
|
||||
├─ 10. 测试模式(保留)
|
||||
│ └─ TestModeConfig(扩展 testElite / testWaveEnchant)
|
||||
└─ 11. 全局单例 & 向后兼容导出
|
||||
├─ spawningEngine
|
||||
├─ DefaultWaveSlot
|
||||
└─ WaveSlotConfig (Proxy)
|
||||
```
|
||||
|
||||
## 11. 验证方式
|
||||
|
||||
1. **静态校验**:游戏启动时调用 `validateRogueConfig()`,控制台无 error。
|
||||
2. **运行时验证**:
|
||||
- 测试模式分别设置 `testElite` / `testWaveEnchant`,验证 hp/ap 计算正确。
|
||||
- 正常模式连续生成 wave 1~30,检查每波怪物数、Boss 出现、Enchant 命中。
|
||||
- wave 35 验证全局乘子继续递增。
|
||||
3. **回归验证**:现有战斗流程不中断,[MissionMonComp](../../../assets/script/game/map/MissionMonComp.ts) 无报错。
|
||||
|
||||
## 12. 风险与缓解
|
||||
|
||||
| 风险 | 概率 | 缓解措施 |
|
||||
|---|---|---|
|
||||
| SquadLibrary 中 type 在 MonList 中无怪 | 中 | `validateRogueConfig()` 启动校验 + 兜底 Melee |
|
||||
| 30 波数据量大,手写易出错 | 高 | 设计文档已给出完整 30 波数据,避免实施时现想 |
|
||||
| MissionMonComp 隐式依赖 affixes | 低 | 已检查仅读 hp/ap/isBoss,安全 |
|
||||
| AffixType 在其他文件被引用 | 低 | Grep 已确认仅 RogueConfig.ts 内部使用 |
|
||||
| 配置错误导致死循环 | 低 | assembleSquads 有 safety 计数器 |
|
||||
|
||||
## 13. 后续扩展点(本次不实施)
|
||||
|
||||
- **HeroAttrsComp 升级**:新增 `regen_per_sec` 字段后,启用 `MonsterElite.Regen` 的回血效果。
|
||||
- **攻速字段统一**:把怪物 CD 也纳入 `getEffectiveSkillCd` 后,启用 `cd_mul`。
|
||||
- **数据驱动化**:把 WaveConfigs / SquadLibrary / WaveEnchantLibrary 抽到 JSON 配置文件,进一步解耦。
|
||||
- **强化组合扩充**:WaveEnchantLibrary 可继续扩充(如「元素浪潮」「血月」等主题强化)。
|
||||
Reference in New Issue
Block a user