/** * @file RogueConfig.ts * @description 无限关卡刷怪系统(关-回合两级推进) * * 游戏机制: * - 玩家单线无限推进:从第 1 关起无上限,每关 5 回合(一关一局,回合间自动推进)。 * - 第 1~4 回合普通怪(3 小怪立即全刷),属性逐回合轻量增强(SECTION_RAMP_STEP)。 * - 第 5 回合单 Boss 登场(无护卫,延迟 1s 刷出),超厚血量(BOSS_HP_MUL)。 * - Boss 加载时按攻击定位(近战/中程/远程)随机取型,并随机携带大招: * 前期 1 个,随关卡解锁扩展至多个(BOSS_ULT_UNLOCK_EVERY / BOSS_ULT_MAX_COUNT)。 * * 设计层次: * 1. StageSet - 关卡数值公式(等级/成长/回合增强/Boss 血攻倍率) * 2. BossPools - Boss 池(按攻击定位分组,动态从 HeroInfo 提取) * 3. BossUltPools - Boss 大招池(按定位分组,引用 SkillSet 6100~6204 群体大招段) * 4. StageSpawningEngine - 生成引擎(按回合配置生成怪物列表) * * 核心公式: * 小怪属性 = HeroInfo 基础属性 × MON_START_COEF × (1 + MON_GROW_SLOPE×(lv-1)) × 回合增强 * Boss 属性 = 同上 × BOSS_HP_MUL(血)/ BOSS_AP_MUL(攻),等级 = 关卡号无限成长 */ import { HeroInfo, HType, MonType, TriggerGrouped, UltCdLv, UltCdSet } from "../common/config/heroSet"; import { SkillOverrides, SkillSet } from "../common/config/SkillSet"; import { FacSet } from "../common/config/GameSet"; import { SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs, NORMAL_MON_COUNT, NORMAL_FIRST_SPAWN, BOSS_DELAY, MON_START_COEF, } from "../common/config/StageSet"; // ======================== 1. 战斗常量 ======================== /** 单场战斗时长基准(秒):供清屏速度评价(BattleBannerComp 清屏奖励)等使用 */ export const WAVE_DURATION = 30; /** 战斗超时(秒):超过后强制结算整局(残留怪销毁并扣留存分) */ 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 池:按攻击定位(近战/中程/远程)分组,动态从 HeroInfo 提取。 * 加载时随机一个定位 → 从对应池随机 Boss;定位池为空时自动回退(如暂无中程 Boss 配置)。 */ export const BossPools: Record = { [HType.Melee]: [], [HType.Mid]: [], [HType.Long]: [], }; for (const key in HeroInfo) { const info = HeroInfo[key]; if (info.fac !== FacSet.MON) continue; if (info.monType !== MonType.MeleeBoss && info.monType !== MonType.LongBoss) continue; const pool = BossPools[info.type as HType]; if (pool) pool.push(info.uuid); } // ======================== 4. Boss 大招池 ======================== /** 每解锁 1 个额外大招收紧的关卡间隔(每 5 关 +1 个大招位) */ export const BOSS_ULT_UNLOCK_EVERY = 5; /** Boss 同时携带大招数量上限 */ export const BOSS_ULT_MAX_COUNT = 3; /** * 获取指定关卡的 Boss 携带大招数量(1 个起步,随关卡解锁扩展) * @param stage 关卡号(1~∞) * @returns 大招数量(1~BOSS_ULT_MAX_COUNT) */ export function getBossUltCount(stage: number): number { const st = Math.max(1, Math.floor(stage)); return Math.min(BOSS_ULT_MAX_COUNT, 1 + Math.floor((st - 1) / BOSS_ULT_UNLOCK_EVERY)); } /** Boss 大招条目(s_uuid 引用 SkillSet 6100~6204 群体大招段,按定位分组) */ export interface BossUltEntry { s_uuid: number; name: string; /** 抽取权重 */ weight: number; /** 技能参数覆盖(伤害系数等) */ overrides?: SkillOverrides; } /** * Boss 大招池(按攻击定位分组) * 大招注入 Boss 的 skills 表后由 SCastSystem 按 CD 自动施放(含镜头聚焦 + 技能名横幅)。 */ export const BossUltPools: Record = { // 近战系:战士群体斩击(6100~6104 段) [HType.Melee]: [ { s_uuid: 6100, name: "寒冰斩", weight: 10, overrides: { ap: 180 } }, { s_uuid: 6101, name: "裂气斩", weight: 10, overrides: { ap: 180 } }, { s_uuid: 6102, name: "火焰斩", weight: 10, overrides: { ap: 180 } }, { s_uuid: 6103, name: "闪电斩", weight: 8, overrides: { ap: 180 } }, { s_uuid: 6104, name: "开天斩", weight: 6, overrides: { ap: 220 } }, ], // 中程系:射手箭雨 + 混合法术(6150~6151 / 6203~6204 段) [HType.Mid]: [ { s_uuid: 6150, name: "万箭齐发", weight: 10, overrides: { ap: 150 } }, { s_uuid: 6151, name: "火箭齐发", weight: 10, overrides: { ap: 150 } }, { s_uuid: 6203, name: "能量波", weight: 8, overrides: { ap: 150 } }, { s_uuid: 6204, name: "雷暴", weight: 8, overrides: { ap: 150 } }, ], // 远程系:法师群体法术(6200~6204 段) [HType.Long]: [ { s_uuid: 6200, name: "火球术", weight: 10, overrides: { ap: 160 } }, { s_uuid: 6201, name: "飓风", weight: 10, overrides: { ap: 140 } }, { s_uuid: 6202, name: "毒气弹", weight: 8, overrides: { ap: 150 } }, { s_uuid: 6203, name: "能量波", weight: 8, overrides: { ap: 150 } }, { s_uuid: 6204, name: "雷暴", weight: 8, overrides: { ap: 150 } }, ], }; // ======================== 5. 配置校验 ======================== /** * 校验 Boss 池 / Boss 大招池引用一致性 * 建议在游戏启动时调用一次,便于发现配置错误 * @returns 错误信息数组,空数组表示校验通过 */ export function validateRogueConfig(): string[] { const errors: string[] = []; // 校验至少一个定位有可用 Boss const hasBoss = (Object.keys(BossPools) as unknown as HType[]).some(t => BossPools[t].length > 0); if (!hasBoss) { errors.push("BossPools 中无任何可用 Boss uuid(MeleeBoss/LongBoss 均缺失)"); } // 校验大招池条目均存在于 SkillSet for (const key in BossUltPools) { const pool = BossPools[key as unknown as HType]; const ults = BossUltPools[key as unknown as HType]; if (!pool || pool.length === 0) continue; // 空定位池跳过(大招池无消费方) for (const ult of ults) { if (!SkillSet[ult.s_uuid]) { errors.push(`BossUltPools[${key}] 大招 ${ult.s_uuid}(${ult.name}) 未在 SkillSet 中定义`); } } } return errors; } // ======================== 6. 生成结果接口 ======================== /** 生成结果(传递给 MissionMonComp 用于实例化怪物) */ export interface GeneratedMonster { uuid: number; type: MonType; hp: number; ap: number; isBoss: boolean; /** 是否为精英(对象池复用实体需显式重置的标记位) */ isElite?: boolean; /** 怪物等级(驱动技能等级增量,Mon.load 第 6 参) */ lv: number; spawnIndex: number; /** 本怪所属批次(0=首批立即刷,1=后续定时刷) */ batch: number; /** 延迟刷出时间(秒),0 表示立即 */ delay?: number; /** * 怪物技能套装(技能注入通道,与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp) * - skill:替换普攻(会清空原技能表) * - ults:追加 Boss 大招(保留普攻,多个大招共存,SCastSystem 按 CD 轮转施放) * - 其余为触发技能组覆盖 */ skills?: { skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides }; /** Boss 大招列表(追加注入,不替换普攻) */ ults?: { s_uuid: number; cd?: number; overrides?: SkillOverrides }[]; call?: TriggerGrouped; atking?: TriggerGrouped; atked?: TriggerGrouped; dead?: TriggerGrouped; fstart?: TriggerGrouped; revive?: TriggerGrouped; }; } // ======================== 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, /** 测试模式注入的大招列表(追加,不替换普攻) */ ults: 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. 生成引擎 ======================== /** * 无限关卡刷怪生成引擎 * 按关-回合配置生成怪物列表,属性走 StageSet 数值公式 */ export class StageSpawningEngine { /** * 生成指定回合的怪物列表 * @param chapter 章节号(无限关模式已废弃章节概念,兼容参数恒忽略) * @param stage 关卡号(1~∞) * @param section 回合号(1~SECTIONS_PER_STAGE) * @returns 怪物列表(按刷出顺序排列) */ generateSectionMonsters(chapter: number, stage: number, section: number): GeneratedMonster[] { // 测试模式拦截:完全绕过生成引擎,直接返回硬编码测试怪 if (TestModeConfig.enable) { return this.generateTestWave(); } const cfg = getSectionConfig(chapter, stage, section); if (!cfg) { // 配置缺失兜底:生成 3 只 Lv1 近战小怪 return this.makeFallbackMonsters(); } return cfg.type === SectionType.Boss ? this.generateBossSection(cfg, stage) : this.generateNormalSection(cfg); } /** 重置引擎状态(每局开始时调用,预留) */ reset(): void { } /** * 普通回合(1~4):NORMAL_MON_COUNT 只小怪全部立即刷出(batch=0), * 属性乘回合增强系数(cfg.ramp)实现逐回合轻量增强 */ private generateNormalSection(cfg: SectionConfig): GeneratedMonster[] { const monsters: GeneratedMonster[] = []; const types = cfg.monTypes ?? [MonType.Melee]; for (let i = 0; i < NORMAL_MON_COUNT; i++) { const type = types[i % types.length]; const mon = this.makeMonster(type, cfg.monLv, i); mon.batch = i < NORMAL_FIRST_SPAWN ? 0 : 1; // 回合内轻量增强:第 n 回合 × (1 + SECTION_RAMP_STEP×(n-1)) mon.hp = Math.max(1, Math.round(mon.hp * cfg.ramp)); mon.ap = Math.max(1, Math.round(mon.ap * cfg.ramp)); monsters.push(mon); } return monsters; } /** * Boss 回合(5):单 Boss 登场(无护卫) * Boss 延迟 BOSS_DELAY 秒刷出,属性由 makeBoss 按血/攻分离倍率计算(超厚血量) */ private generateBossSection(cfg: SectionConfig, stage: number): GeneratedMonster[] { const boss = this.makeBoss(cfg.monLv, stage, 0); boss.batch = 0; boss.delay = BOSS_DELAY; return [boss]; } /** 生成普通怪物(属性按 StageSet 公式计算) */ private makeMonster(type: MonType, monLv: 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; const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false); return { uuid, type, hp: attrs.hp, ap: attrs.ap, isBoss: false, lv: monLv, spawnIndex, batch: 0, }; } /** 配置缺失兜底:3 只 Lv1 近战小怪 */ private makeFallbackMonsters(): GeneratedMonster[] { const monsters: GeneratedMonster[] = []; for (let i = 0; i < 3; i++) { monsters.push({ uuid: 6001, type: MonType.Melee, hp: Math.round(560 * MON_START_COEF), ap: Math.round(22 * MON_START_COEF), isBoss: false, lv: 1, spawnIndex: i, batch: 0, }); } return monsters; } /** 测试模式:完全绕过引擎逻辑 */ 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, lv: 1, spawnIndex: i, batch: 0, skills: { skill: TestModeConfig.skill, ults: TestModeConfig.ults, atking: TestModeConfig.atking, atked: TestModeConfig.atked, dead: TestModeConfig.dead, fstart: TestModeConfig.fstart, }, }); } return monsters; } /** * 生成 Boss:加载时随机攻击定位 → 从对应池随机 Boss 个体 → 随机携带大招 * 属性走 calcMonsterAttrs(isBoss=true):HP × BOSS_HP_MUL(超厚血)/ AP × BOSS_AP_MUL */ private makeBoss(monLv: number, stage: number, spawnIndex: number): GeneratedMonster { const range = this.pickBossRange(); const uuid = this.pickBossUuid(range); const baseInfo = HeroInfo[uuid]; const baseHp = baseInfo ? baseInfo.hp : 600; const baseAp = baseInfo ? baseInfo.ap : 30; const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, true); return { uuid, type: baseInfo?.monType ?? MonType.MeleeBoss, hp: attrs.hp, ap: attrs.ap, isBoss: true, lv: monLv, spawnIndex, batch: 0, skills: { ults: this.pickBossUlts(range, stage), }, }; } /** * 随机 Boss 攻击定位(近战/中程/远程) * 空池自动回退:中程暂无 Boss 配置时落到远程/近战,后续补配即自动生效 */ private pickBossRange(): HType { const all: HType[] = [HType.Melee, HType.Mid, HType.Long]; const first = all[Math.floor(Math.random() * all.length)]; if (BossPools[first].length > 0) return first; // 回退链:随机定位空池时按剩余定位找非空池 const fallback = all.find(t => BossPools[t].length > 0); return fallback ?? HType.Melee; } /** 从指定定位池随机取 Boss uuid(含全局兜底) */ private pickBossUuid(range: HType): number { let uuids = BossPools[range]; if (!uuids || uuids.length === 0) { uuids = MonList[MonType.MeleeBoss] || MonList[MonType.Melee] || [6001]; } return uuids[Math.floor(Math.random() * uuids.length)]; } /** * 按定位抽取 Boss 大招(不重复),数量由关卡进度决定(getBossUltCount) * @returns 大招配置列表;池空时返回 undefined */ private pickBossUlts(range: HType, stage: number): GeneratedMonster["skills"]["ults"] { const pool = [...(BossUltPools[range] ?? [])]; const count = Math.min(getBossUltCount(stage), pool.length); if (count <= 0) return undefined; const ults: { s_uuid: number; cd?: number; overrides?: SkillOverrides }[] = []; for (let i = 0; i < count; i++) { const picked = this.pickWeighted(pool); if (!picked) break; pool.splice(pool.indexOf(picked), 1); // 大招 CD 取英雄同档标准档(8s),与英雄大招节奏对齐 ults.push({ s_uuid: picked.s_uuid, cd: UltCdSet[UltCdLv.Normal].cd, overrides: picked.overrides }); } return ults.length > 0 ? ults : undefined; } /** 通用加权随机抽取 */ 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 StageSpawningEngine();