/** * @file RogueConfig.ts * @description Boss 组合刷怪系统(单场 Boss 战定胜负) * * 游戏机制: * - 整局只有一场战斗:开局英雄三次三选一后自动开战。 * - 怪物从 BossComboLibrary 按权重随机抽一组(1 Boss + 4 小怪), * 战斗开始瞬间一次全量登场(X∈[100,320] 均匀铺开),双方立即互相攻击。 * - 怪物全灭 → 胜利结算;英雄全灭 → 失败结算;无回合递进、无分批刷怪。 * * 设计层次: * 1. BossComboLibrary - Boss 组合库(小怪构成 + Boss/小怪技能池 + 抽取权重) * 2. MonSkillPool/BossSkillPool - 怪物技能池(atking / atked / dead 等全触发类型) * 3. RogueSpawningEngine - 生成引擎(基础属性 × 可调系数) * * 核心公式(基础属性 × 可调系数): * 小怪 hp = HeroInfo 基础hp × MON_HP_MUL,小怪 ap = HeroInfo 基础ap × MON_AP_MUL * Boss 基础面板固定为初始英雄 2 倍强度(BOSS_BASE_HP/BOSS_BASE_AP),与小怪共用同一套系数, * 场上 Boss 恰好为小怪 2 倍强度(如需额外压场可调 BOSS_HP_BONUS_MUL)。 * 小怪基础面板与 1 级英雄持平(近战镜像 hp300/ap22/cd1.8),默认系数校准为 * 整场战斗(3 英雄 vs 4 小怪 + 1 Boss)双方对砍 ≈ WAVE_DURATION(30s)。 * 手感微调只改 MON_HP_MUL / MON_AP_MUL / BOSS_HP_BONUS_MUL 三个系数即可。 */ import { HeroInfo, MonType, MonTypeName, TriggerGrouped, LvReviveEntry } from "../common/config/heroSet"; import { SkillOverrides } from "../common/config/SkillSet"; import { FacSet } from "../common/config/GameSet"; // ======================== 1. 战斗常量 ======================== /** 单场战斗时长基准(秒):供清屏速度评价(BattleBannerComp 清屏奖励)等使用 */ export const WAVE_DURATION = 30; /** 怪物登场固定构成:1 个 Boss + 4 个小怪(一次全量登场,打完即整局结束) */ export const BOSS_COUNT = 1; export const MINION_COUNT = 4; /** 怪物血量可调系数:最终 hp = HeroInfo 基础hp × MON_HP_MUL。 * 2.6 = 校准后怪物总血量 / 3 英雄 30s 输出(≈30 DPS),双方对砍 ≈ 30s 分胜负 */ export const MON_HP_MUL = 2.6; /** 怪物攻击可调系数:最终 ap = HeroInfo 基础ap × MON_AP_MUL。 * 0.9 使 4 小怪+1 Boss 的总 DPS ≈ 20,30s 恰好磨光 3 英雄总血量(≈600) */ export const MON_AP_MUL = 0.9; /** Boss 基础面板 = 初始英雄基准(hp300/ap22)× 2 倍强度, * 不读取 Boss HeroInfo 的差异化高基础值,避免数值膨胀 */ export const BOSS_BASE_HP = 600; export const BOSS_BASE_AP = 44; /** Boss 血量额外加成(在 MON_HP_MUL 之上乘算,默认 1.0 表示不再额外膨胀,保留作压场微调旋钮) */ export const BOSS_HP_BONUS_MUL = 1.0; /** 战斗超时(秒):超过后强制结算整局(残留怪销毁并扣留存分) */ export const BATTLE_TIMEOUT = 90; // ======================== 2. 怪物 UUID 池 ======================== /** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */ export const MonList: Record = {}; 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); } } // ======================== 2.5 怪物金币掉落配置 ======================== /** * 怪物金币掉落配置(按 MonType 分类) * - base: 普通怪物死亡时掉落的固定金币数 * - boss: Boss 怪物死亡时掉落的固定金币数(仅对 Boss 类型生效) */ export const MonsterGoldSet: Record = { [MonType.Melee]: { base: 1, boss: 0 }, [MonType.Heavy]: { base: 2, boss: 0 }, [MonType.Long]: { base: 2, boss: 0 }, [MonType.Support]: { base: 3, boss: 0 }, [MonType.Summoner]: { base: 3, boss: 0 }, [MonType.Assassin]: { base: 3, boss: 0 }, [MonType.MeleeBoss]: { base: 0, boss: 15 }, [MonType.LongBoss]: { base: 0, boss: 15 }, }; /** * 获取指定怪物类型的金币掉落数量 * @param monType 怪物类型(MonType) * @param isBoss 是否为 Boss * @returns 掉落的金币数量(≥0) */ export function getMonsterGoldDrop(monType: number, isBoss: boolean): number { const cfg = MonsterGoldSet[monType]; if (!cfg) return 0; return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base)); } // ======================== 3. Boss 组合库 ======================== /** Boss 组合中小怪的槽位定义(type + 数量,Boss 另由 BOSS_COUNT 提供) */ export interface BossComboMinionSlot { type: MonType; count: number; } /** Boss 组合配置:一场战斗随机抽一组登场(1 Boss + minions 小怪) */ export interface BossComboConfig { id: string; name: string; /** 小怪构成(合计数量建议等于 MINION_COUNT,超出截断、不足自动补近战) */ minions: BossComboMinionSlot[]; /** Boss 技能池 id(引用 BossSkillPool,可选) */ boss_skill_pool?: string[]; /** 小怪技能池 id(引用 MonSkillPool,可选) */ skill_pool?: string[]; /** 在组合库中的抽取权重 */ weight: number; } /** Boss 组合库(硬编码):每场战斗按权重随机抽一组 */ export const BossComboLibrary: Record = { rage_charge: { id: "rage_charge", name: "狂暴冲锋组", weight: 10, minions: [{ type: MonType.Melee, count: 3 }, { type: MonType.Heavy, count: 1 }], boss_skill_pool: ["boss_rage"], }, iron_wall: { id: "iron_wall", name: "铁壁推进组", weight: 8, minions: [{ type: MonType.Heavy, count: 2 }, { type: MonType.Support, count: 1 }, { type: MonType.Long, count: 1 }], boss_skill_pool: ["boss_iron"], skill_pool: ["tough"], }, doom_ritual: { id: "doom_ritual", name: "毁灭仪式组", weight: 6, minions: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }, { type: MonType.Support, count: 1 }], boss_skill_pool: ["boss_doom"], skill_pool: ["warcry"], }, assassin_raid: { id: "assassin_raid", name: "刺客突袭组", weight: 7, minions: [{ type: MonType.Assassin, count: 2 }, { type: MonType.Melee, count: 2 }], boss_skill_pool: ["boss_rage", "boss_doom"], skill_pool: ["berserk"], }, blood_legion: { id: "blood_legion", name: "吸血军团组", weight: 5, minions: [{ type: MonType.Melee, count: 2 }, { type: MonType.Long, count: 1 }, { type: MonType.Summoner, count: 1 }], boss_skill_pool: ["boss_iron", "boss_doom"], skill_pool: ["leech"], }, }; /** * 按权重随机抽取一组 Boss 组合 * @returns 组合配置,库为空时返回 null */ export function pickBossCombo(): BossComboConfig | null { const combos = Object.values(BossComboLibrary).filter(c => c && c.weight > 0); if (combos.length === 0) return null; const total = combos.reduce((s, c) => s + c.weight, 0); let r = Math.random() * total; for (const c of combos) { r -= c.weight; if (r <= 0) return c; } return combos[combos.length - 1]; } // ======================== 4. 怪物技能池 ======================== /** * 怪物技能套装(一套完整的触发技能配置) * 覆盖触发时机:call / atking / atked / dead / fstart / revive */ export interface MonSkillSet { id: string; name: string; /** 普攻技能覆盖(可选,不填则使用怪物默认普攻) */ skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides }; /** 召唤触发 */ call?: TriggerGrouped; /** 攻击触发 */ atking?: TriggerGrouped; /** 受击触发 */ atked?: TriggerGrouped; /** 死亡触发 */ dead?: TriggerGrouped; /** 战斗开始触发 */ fstart?: TriggerGrouped; /** 复活 */ revive?: LvReviveEntry[]; /** 在技能池中的抽取权重 */ weight: number; } /** * 怪物技能池(硬编码) * 技能 uuid 引用 SkillSet 中的 6000~6500 段触发技能。 */ export const MonSkillPool: Record = { /** 狂暴:攻击触发自身攻击提升 */ berserk: { id: "berserk", name: "狂暴", weight: 10, atking: { 6401: [{ lv: 1, t_num: 5, overrides: { ap: 1 } }], }, }, /** 坚韧:受击获得护盾 */ tough: { id: "tough", name: "坚韧", weight: 8, atked: { 6301: [{ lv: 1, t_num: 3, overrides: { ap: 2 } }], }, }, /** 遗志:死亡时全队攻击提升 */ legacy: { id: "legacy", name: "遗志", weight: 6, dead: { 6401: [{ lv: 1, t_num: 1, overrides: { ap: 3 } }], }, }, /** 战吼:战斗开始时全队攻击提升 */ warcry: { id: "warcry", name: "战吼", weight: 5, fstart: { 6401: [{ lv: 1, t_num: 1, overrides: { ap: 2 } }], }, }, /** 吸血:攻击恢复生命 */ leech: { id: "leech", name: "吸血", weight: 7, atking: { 6302: [{ lv: 1, t_num: 4, overrides: { ap: 150 } }], }, }, }; /** Boss 专属技能池 */ export const BossSkillPool: Record = { /** 狂暴领主:攻击触发全队攻击提升 */ boss_rage: { id: "boss_rage", name: "狂暴领主", weight: 10, atking: { 6401: [{ lv: 1, t_num: 3, overrides: { ap: 3 } }], }, dead: { 6401: [{ lv: 1, t_num: 1, overrides: { ap: 5 } }], }, }, /** 铁壁领主:受击获得高额护盾 */ boss_iron: { id: "boss_iron", name: "铁壁领主", weight: 8, atked: { 6301: [{ lv: 1, t_num: 2, overrides: { ap: 5 } }], }, }, /** 毁灭领主:战斗开始时全队攻击大幅提升 */ boss_doom: { id: "boss_doom", name: "毁灭领主", weight: 6, fstart: { 6401: [{ lv: 1, t_num: 1, overrides: { ap: 8 } }], }, atking: { 6401: [{ lv: 1, t_num: 5, overrides: { ap: 2 } }], }, }, }; // ======================== 5. 配置校验 ======================== /** * 校验 BossComboLibrary / MonSkillPool 引用一致性 * 建议在游戏启动时调用一次,便于发现配置错误 * @returns 错误信息数组,空数组表示校验通过 */ export function validateRogueConfig(): string[] { const errors: string[] = []; // 1. 校验组合引用与数量 for (const id in BossComboLibrary) { const combo = BossComboLibrary[id]; let minionTotal = 0; for (const slot of combo.minions) { minionTotal += slot.count; if (!MonList[slot.type] || MonList[slot.type].length === 0) { errors.push(`组合 ${id} 引用的怪物类型 ${slot.type}(${MonTypeName[slot.type] || "?"}) 在 MonList 中无可用 uuid`); } } if (minionTotal !== MINION_COUNT) { errors.push(`组合 ${id} 小怪数量=${minionTotal} 与 MINION_COUNT=${MINION_COUNT} 不一致`); } if (combo.skill_pool) { for (const skillId of combo.skill_pool) { if (!MonSkillPool[skillId]) { errors.push(`组合 ${id} 引用了不存在的技能: ${skillId}`); } } } if (combo.boss_skill_pool) { for (const skillId of combo.boss_skill_pool) { if (!BossSkillPool[skillId]) { errors.push(`组合 ${id} 引用了不存在的 Boss 技能: ${skillId}`); } } } } // 2. 校验 Boss 类型在 MonList 中有怪 if ((!MonList[MonType.MeleeBoss] || MonList[MonType.MeleeBoss].length === 0) && (!MonList[MonType.LongBoss] || MonList[MonType.LongBoss].length === 0)) { errors.push("MonList 中无任何 Boss 类型(MeleeBoss/LongBoss)可用 uuid"); } return errors; } // ======================== 6. 生成结果接口 ======================== /** 生成结果(传递给 MissionMonComp 用于实例化怪物) */ export interface GeneratedMonster { uuid: number; type: MonType; hp: number; ap: number; isBoss: boolean; /** 是否为 Boss 护卫队成员(供 UI/统计识别) */ isBossGuard?: boolean; spawnIndex: number; /** 本怪所属批次(保留字段,单场全量登场恒为 0) */ batch: number; /** * 怪物技能套装(覆盖全部触发时机) * 注入方式与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp */ skills?: { skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides }; call?: TriggerGrouped; atking?: TriggerGrouped; atked?: TriggerGrouped; dead?: TriggerGrouped; fstart?: TriggerGrouped; revive?: LvReviveEntry[]; }; } // ======================== 7. 测试模式配置 ======================== export const TestModeConfig = { enable: false, // 默认关闭测试模式 baseHp: 150, baseAp: 12, monType: MonType.Melee, monUuid: 6001, spawnCount: 1, skill: undefined as { s_uuid: number; cd?: number; overrides?: SkillOverrides } | undefined, atking: undefined as TriggerGrouped | undefined, atked: undefined as TriggerGrouped | undefined, dead: undefined as TriggerGrouped | undefined, fstart: undefined as TriggerGrouped | undefined, }; // ======================== 8. 生成引擎 ======================== /** * Boss 组合生成引擎 * 随机抽一组 Boss 组合,怪物最终属性 = HeroInfo 基础属性 × 可调系数 */ export class RogueSpawningEngine { /** * 生成一场战斗的怪物列表:随机一组 Boss 组合(1 Boss + 4 小怪,属性走基础×系数)。 * Boss 压队尾(登场时由 MissionMonComp 统一布位)。 * @returns 怪物列表(小怪在前、Boss 在尾) */ generateBossCombo(): GeneratedMonster[] { // 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪 if (TestModeConfig.enable) { return this.generateTestWave(); } const combo = pickBossCombo(); const monsters: GeneratedMonster[] = []; if (combo) { for (const slot of combo.minions) { for (let i = 0; i < slot.count; i++) { monsters.push(this.makeMonster(slot.type, monsters.length)); } } } // 组合库为空或数量不足兜底:补近战小怪至 MINION_COUNT while (monsters.length < MINION_COUNT) { monsters.push(this.makeMonster(MonType.Melee, monsters.length)); } // Boss 压队尾 for (let i = 0; i < BOSS_COUNT; i++) { monsters.push(this.makeBoss()); } // 属性缩放:基础属性 × 可调系数(Boss 基础面板已是英雄 2 倍,BOSS_HP_BONUS_MUL 默认 1.0), // 校准目标:Boss 场上强度恰为小怪 2 倍,双方对砍 ≈ 30s for (const m of monsters) { m.hp = Math.max(1, Math.round(m.hp * MON_HP_MUL * (m.isBoss ? BOSS_HP_BONUS_MUL : 1))); m.ap = Math.max(1, Math.round(m.ap * MON_AP_MUL)); } // 挂载组合技能(Boss 专属池 / 小怪普通池) for (const m of monsters) { if (m.isBoss && combo?.boss_skill_pool) { m.skills = this.pickSkillSet(combo.boss_skill_pool, BossSkillPool); } else if (!m.isBoss && combo?.skill_pool) { m.skills = this.pickSkillSet(combo.skill_pool, MonSkillPool); } m.batch = 0; } for (let i = 0; i < monsters.length; i++) { monsters[i].spawnIndex = i; } return monsters; } /** 重置引擎状态(每局开始时调用,预留) */ reset(): void { } /** 测试模式:完全绕过引擎逻辑 */ private generateTestWave(): GeneratedMonster[] { 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: TestModeConfig.baseHp, ap: TestModeConfig.baseAp, isBoss: false, spawnIndex: i, batch: 0, skills: { skill: TestModeConfig.skill, atking: TestModeConfig.atking, atked: TestModeConfig.atked, dead: TestModeConfig.dead, fstart: TestModeConfig.fstart, }, }); } return monsters; } /** 生成 Boss(基础面板固定为初始英雄 2 倍强度,属性随后由 generateBossCombo 统一系数缩放) */ private makeBoss(): 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)]; return { uuid, type, hp: BOSS_BASE_HP, ap: BOSS_BASE_AP, isBoss: true, spawnIndex: 0, // 占位值,由 generateBossCombo 统一分配 batch: 0, }; } /** 生成普通怪物 */ private makeMonster(type: MonType, 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, isBoss: false, spawnIndex, batch: 0, }; } /** 从技能池中按权重抽取一套技能 */ private pickSkillSet(pool: string[], library: Record): GeneratedMonster["skills"] | undefined { const valid = pool.map(id => library[id]).filter(s => s); if (valid.length === 0) return undefined; const picked = this.pickWeighted(valid); if (!picked) return undefined; return { skill: picked.skill, call: picked.call, atking: picked.atking, atked: picked.atked, dead: picked.dead, fstart: picked.fstart, revive: picked.revive, }; } /** 通用加权随机抽取 */ private pickWeighted(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]; } } // ======================== 9. 全局单例 ======================== export const spawningEngine = new RogueSpawningEngine();