/** * @file RogueConfig.ts * @description 关卡刷怪系统(章节-关-节三级推进) * * 游戏机制: * - 玩家线性推进 1 章 10 关 × 每关 10 小节,进度精确到小节并持久化。 * - 普通关(1~4, 6~9):9 只小怪,前 3 只立即刷,后续每 5s 刷 1 只。 * - 精英关(5):3 只精英同时刷出。 * - Boss 关(10):2 精英先刷,Boss 延迟 1s 刷出。 * - 怪物属性 = 基础 × 0.8 × (1 + 0.05×(lv-1)) × 类型倍率(精英1.5 / Boss3.0)。 * * 设计层次: * 1. StageSet - 关卡配置(章节/关/节 + 数值公式) * 2. MonSkillPool/BossSkillPool - 怪物技能池(atking / atked / dead 等全触发类型) * 3. StageSpawningEngine - 生成引擎(按小节配置生成怪物列表) * * 核心公式: * 最终属性 = HeroInfo 基础属性 × MON_START_COEF × (1 + MON_GROW_SLOPE×(lv-1)) × 类型倍率 * 1 级小怪 ≈ 英雄 × 0.8,10 级小怪 ≈ 英雄 × 1.16,保持平缓成长。 */ import { HeroInfo, MonType, MonTypeName, TriggerGrouped } from "../common/config/heroSet"; import { SkillOverrides } from "../common/config/SkillSet"; import { FacSet } from "../common/config/GameSet"; import { SectionType, SectionConfig, getSectionConfig, calcMonsterAttrs, NORMAL_MON_COUNT, NORMAL_FIRST_SPAWN, ELITE_COUNT, BOSS_ELITE_COUNT, BOSS_DELAY, MON_START_COEF, MON_GROW_SLOPE, ELITE_MUL, BOSS_MUL, } 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. 怪物技能池 ======================== /** * 怪物技能套装(一套完整的触发技能配置) * 覆盖触发时机: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?: TriggerGrouped; /** 在技能池中的抽取权重 */ 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 } }], }, }, }; // ======================== 4. 配置校验 ======================== /** * 校验 MonSkillPool / BossSkillPool 引用一致性 * 建议在游戏启动时调用一次,便于发现配置错误 * @returns 错误信息数组,空数组表示校验通过 */ export function validateRogueConfig(): string[] { const errors: string[] = []; // 校验 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; } // ======================== 5. 生成结果接口 ======================== /** 生成结果(传递给 MissionMonComp 用于实例化怪物) */ export interface GeneratedMonster { uuid: number; type: MonType; hp: number; ap: number; isBoss: boolean; /** 是否为精英(精英关/Boss 关的精英) */ isElite?: boolean; /** 是否为 Boss 护卫队成员(供 UI/统计识别) */ isBossGuard?: boolean; /** 怪物等级(驱动技能等级增量,Mon.load 第 6 参) */ lv: number; spawnIndex: number; /** 本怪所属批次(0=首批立即刷,1=后续定时刷) */ batch: number; /** 延迟刷出时间(秒),0 表示立即 */ delay?: 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?: TriggerGrouped; }; } // ======================== 6. 测试模式配置 ======================== 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, }; // ======================== 7. 生成引擎 ======================== /** * 关卡刷怪生成引擎 * 按章节-关-节配置生成怪物列表,属性走 StageSet 数值公式 */ export class StageSpawningEngine { /** * 生成指定小节的怪物列表 * @param chapter 章节号 * @param stage 关卡号 * @param section 小节号(1~10) * @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(); } switch (cfg.type) { case SectionType.Elite: return this.generateEliteSection(cfg); case SectionType.Boss: return this.generateBossSection(cfg); case SectionType.Normal: default: return this.generateNormalSection(cfg); } } /** 重置引擎状态(每局开始时调用,预留) */ reset(): void { } /** * 普通关:9 只小怪 * 前 3 只 batch=0(立即刷),后 6 只 batch=1(定时刷) */ 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; monsters.push(mon); } return monsters; } /** * 精英关:3 只精英同时刷出 */ private generateEliteSection(cfg: SectionConfig): GeneratedMonster[] { const monsters: GeneratedMonster[] = []; const count = cfg.eliteCount ?? ELITE_COUNT; // 精英池:近战/重型/远程/刺客 const eliteTypes = [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Assassin]; for (let i = 0; i < count; i++) { const type = eliteTypes[i % eliteTypes.length]; const mon = this.makeMonster(type, cfg.monLv, i); mon.isElite = true; mon.batch = 0; // 精英属性 = 普通 × ELITE_MUL mon.hp = Math.max(1, Math.round(mon.hp * ELITE_MUL)); mon.ap = Math.max(1, Math.round(mon.ap * ELITE_MUL)); monsters.push(mon); } return monsters; } /** * Boss 关:2 精英 + 1 Boss * 精英先刷(batch=0),Boss 延迟刷出(batch=0, delay=BOSS_DELAY) */ private generateBossSection(cfg: SectionConfig): GeneratedMonster[] { const monsters: GeneratedMonster[] = []; const eliteCount = cfg.eliteCount ?? BOSS_ELITE_COUNT; const eliteTypes = [MonType.Melee, MonType.Heavy]; // 先刷精英 for (let i = 0; i < eliteCount; i++) { const type = eliteTypes[i % eliteTypes.length]; const mon = this.makeMonster(type, cfg.monLv, i); mon.isElite = true; mon.isBossGuard = true; mon.batch = 0; mon.hp = Math.max(1, Math.round(mon.hp * ELITE_MUL)); mon.ap = Math.max(1, Math.round(mon.ap * ELITE_MUL)); monsters.push(mon); } // Boss 延迟刷出 const boss = this.makeBoss(cfg.monLv, monsters.length); boss.batch = 0; boss.delay = BOSS_DELAY; // Boss 属性 = 普通 × BOSS_MUL boss.hp = Math.max(1, Math.round(boss.hp * BOSS_MUL)); boss.ap = Math.max(1, Math.round(boss.ap * BOSS_MUL)); monsters.push(boss); return monsters; } /** 配置缺失兜底: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, atking: TestModeConfig.atking, atked: TestModeConfig.atked, dead: TestModeConfig.dead, fstart: TestModeConfig.fstart, }, }); } return monsters; } /** 生成 Boss(基础面板按 StageSet 公式计算) */ private makeBoss(monLv: number, spawnIndex: 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)]; // Boss 基础面板取 HeroInfo 基础值,属性由调用方乘 BOSS_MUL const baseInfo = HeroInfo[uuid]; const baseHp = baseInfo ? baseInfo.hp : 600; const baseAp = baseInfo ? baseInfo.ap : 30; const attrs = calcMonsterAttrs(baseHp, baseAp, monLv, false, false); return { uuid, type, hp: attrs.hp, ap: attrs.ap, isBoss: true, lv: monLv, spawnIndex, batch: 0, }; } /** 生成普通怪物(属性按 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, false); return { uuid, type, hp: attrs.hp, ap: attrs.ap, isBoss: false, lv: monLv, 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]; } } // ======================== 8. 全局单例 ======================== export const spawningEngine = new StageSpawningEngine(); // 保留旧接口兼容(逐步迁移期使用) /** @deprecated 请使用 StageSpawningEngine.generateSectionMonsters */ export class RogueSpawningEngine extends StageSpawningEngine { /** @deprecated 请使用 generateSectionMonsters */ generateBossCombo(): GeneratedMonster[] { return this.generateSectionMonsters(1, 1, 1); } } /** @deprecated 请使用 spawningEngine */ export const rogueSpawningEngine = new RogueSpawningEngine();