diff --git a/assets/script/game/common/config/heroSet.ts b/assets/script/game/common/config/heroSet.ts index 3c8ed401..430d20c7 100644 --- a/assets/script/game/common/config/heroSet.ts +++ b/assets/script/game/common/config/heroSet.ts @@ -381,40 +381,40 @@ export const HeroInfo: Record = { */ - // 基础怪物 (全部远程攻击,HType仅决定站位) - // 近战位怪物 (站在前排,承受更多伤害) — v5: TD节奏CD,多而弱爽感设计 + // 基础怪物 (全部固定点位站桩攻击,HType仅决定是前排还是后排) + // 前排怪物 (站在前排,承受更多伤害) — v5: TD节奏CD,多而弱爽感设计 6001:{uuid:6001,name:"兽人战士",path:"m1", fac:FacSet.MON,lv:1,type:HType.Melee,hp:220,ap:10,speed:70, - skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"基础近战位怪"}, + skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"基础前排怪"}, 6002:{uuid:6002,name:"兽人精锐战士",path:"m2", fac:FacSet.MON,lv:1,type:HType.Melee,hp:300,ap:14,speed:110, - skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"进阶近战位怪,更快更痛"}, + skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"进阶前排怪,更快更痛"}, 6003:{uuid:6003,name:"兽人重装兵",path:"m3", fac:FacSet.MON,lv:1,type:HType.Melee,hp:850,ap:20,speed:50, skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"重型坦克怪,高HP慢攻"}, - // 远程位怪物 (站在后排,输出更高) + // 后排怪物 (站在后排,输出更高) 6004:{uuid:6004,name:"兽人射手",path:"m4", fac:FacSet.MON,lv:1,type:HType.Long,hp:190,ap:35,speed:70, - skills:{6008:{uuid:6008,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"远程高DPS怪"}, + skills:{6008:{uuid:6008,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"后排高DPS怪"}, 6005:{uuid:6005,name:"兽人刺客",path:"m5", fac:FacSet.MON,lv:1,type:HType.Long,hp:210,ap:38,speed:130, skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"高AP快速攻击刺客"}, // 特殊位怪物 6006:{uuid:6006,name:"骷髅领主",path:"m6", fac:FacSet.MON,lv:1,type:HType.Melee,hp:5000,ap:20,speed:60, - skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"MiniBoss级坦克"}, -6007:{uuid:6007,name:"兽人术士",path:"m7", fac:FacSet.MON,lv:1,type:HType.Melee,hp:300,ap:24,speed:70, - skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"法师怪,远程魔法攻击"}, -6008:{uuid:6008,name:"兽人火法",path:"m8", fac:FacSet.MON,lv:1,type:HType.Melee,hp:270,ap:32,speed:70, - skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"高输出法师怪"}, + skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"前排MiniBoss级坦克"}, +6007:{uuid:6007,name:"兽人术士",path:"m7", fac:FacSet.MON,lv:1,type:HType.Long,hp:300,ap:24,speed:70, + skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"后排法师怪,魔法攻击"}, +6008:{uuid:6008,name:"兽人火法",path:"m8", fac:FacSet.MON,lv:1,type:HType.Long,hp:270,ap:32,speed:70, + skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"后排高输出法师怪"}, // BOSS怪物 — Boss节奏1.2-1.5s,删除不存在的6206技能 -6101:{uuid:6101,name:"兽人首领-双刀战士",path:"mb1", fac:FacSet.MON,lv:6,type:HType.Long,hp:1900,ap:30,speed:120, - skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"远程Boss,高攻速"}, +6101:{uuid:6101,name:"兽人首领-双刀战士",path:"mb1", fac:FacSet.MON,lv:6,type:HType.Melee,hp:1900,ap:30,speed:120, + skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"前排Boss,高攻速"}, 6102:{uuid:6102,name:"兽人首领-斧头战士",path:"mb2", fac:FacSet.MON,lv:6,type:HType.Melee,hp:7500,ap:26,speed:60, - skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"近战Boss,超高HP"}, + skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"前排Boss,超高HP"}, 6103:{uuid:6103,name:"兽人首领-魔法师",path:"mb3", fac:FacSet.MON,lv:6,type:HType.Long,hp:2250,ap:38,speed:110, - skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"远程法系Boss,高AP"}, -6104:{uuid:6104,name:"兽人首领-射手",path:"mb4", fac:FacSet.MON,lv:6,type:HType.Melee,hp:6800,ap:30,speed:70, - skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"近战位Boss,均衡型"}, + skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow2].cd,ccd:0}},info:"后排法系Boss,高AP"}, +6104:{uuid:6104,name:"兽人首领-射手",path:"mb4", fac:FacSet.MON,lv:6,type:HType.Long,hp:6800,ap:30,speed:70, + skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"后排位Boss,均衡型"}, 6105:{uuid:6105,name:"亡灵首领-法师",path:"mb5", fac:FacSet.MON,lv:6,type:HType.Long,hp:2600,ap:42,speed:110, - skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"远程高AP Boss"}, + skills:{6103:{uuid:6103,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"后排高AP Boss"}, 6106:{uuid:6106,name:"亡灵首领-骑马战士",path:"mb6", fac:FacSet.MON,lv:6,type:HType.Melee,hp:9000,ap:26,speed:130, - skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"终极Boss,最高HP+高速"}, + skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow3].cd,ccd:0}},info:"前排终极Boss,最高HP+高速"}, }; diff --git a/assets/script/game/map/MissionComp.ts b/assets/script/game/map/MissionComp.ts index d2a5adf6..8713f2bd 100644 --- a/assets/script/game/map/MissionComp.ts +++ b/assets/script/game/map/MissionComp.ts @@ -856,11 +856,6 @@ export class MissionComp extends CCComp { // 怪物全灭检测:如果战斗阶段场上没有任何活着的怪物,且待刷新的怪物队列也为空,直接结束战斗进入下一波的准备阶段 const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0; if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) { - let heroesAliveRatio = heroCount / 6.0; // 假设最大 6 个站位,或者直接基于存活数算比例 - // 如果能获取当前已部署英雄数最好,这里简化处理,大于 4 个就算高存活 - heroesAliveRatio = Math.min(1.0, heroCount / 4.0); - spawningEngine.updateAdaptive(heroesAliveRatio, this.clearTime); - if (this.currentWave >= 15) { // 15 波通关 this.open_Victory(null, false); diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index 3a5d8ff2..48263d8b 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -3,27 +3,13 @@ * @description 怪物(Monster)波次刷新管理组件(逻辑层) * * 职责: - * 1. 管理每一波怪物的 **生成计划**:根据 WaveSlotConfig 生成怪物。 - * 2. 处理特殊插队刷怪请求(MonQueue),优先于常规刷新。 - * 3. 自动推进波次:当前波所有怪物被清除后自动进入下一波。 + * 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。 + * 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)统一刷出怪物。 * * 关键设计: - * - 突破 5 槽限制,怪物按刷怪顺序依次从 X=280 开始向右(每隔 50)排布。 - * - 6 条刷怪线:在三路 Y 轴范围内随机偏移,实现 6 路进军。 - * - resetSlotSpawnData(wave) 在每波开始时读取配置,分配并立即生成所有怪物。 - * - 去除跨波 HP 继承,上一波残留怪在波次结束/开始时销毁。 - * - * 怪物属性计算公式: - * ap = floor((base_ap + stage × grow_ap) × SpawnPowerBias) - * hp = floor((base_hp + stage × grow_hp) × SpawnPowerBias) - * 其中 stage = currentWave - 1 - * - * 依赖: - * - RogueConfig —— 怪物类型、成长值、波次配置 - * - Monster(hero/Mon.ts)—— 怪物 ECS 实体类 - * - HeroInfo(heroSet)—— 怪物基础属性配置(与英雄共用配置) - * - HeroAttrsComp / MonMoveComp —— 怪物属性和移动组件 - * - BoxSet.GAME_LINE —— 地面基准 Y 坐标 + * - 采用 12 个硬编码的网格位置点 (MON_POSITIONS,3行x4列) + * - 每次生成最多 12 个怪物,固定在位置点。 + * - 上一波残留怪在波次结束/开始时统一清理。 */ import { _decorator, v3, Vec3 } from "cc"; import { mLogger } from "../common/Logger"; @@ -31,21 +17,15 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ec import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { Monster } from "../hero/Mon"; -import { HeroInfo, HType } from "../common/config/heroSet"; import { smc } from "../common/SingletonModuleComp"; import { GameEvent } from "../common/config/GameEvent"; -import {BoxSet, FacSet } from "../common/config/GameSet"; -import { spawningEngine, GeneratedMonster, AffixType, MonType, MonList, TestModeConfig } from "./RogueConfig"; +import { BoxSet, FacSet } from "../common/config/GameSet"; +import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { MonMoveComp } from "../hero/MonMoveComp"; + const { ccclass, property } = _decorator; -/** - * MissionMonCompComp —— 怪物波次刷新管理器 - * - * 每波开始时根据 WaveSlotConfig 配置生成怪物, - * 战斗中监控数量,所有怪物消灭后自动推进到下一波。 - */ @ccclass('MissionMonCompComp') @ecs.register('MissionMonComp', false) export class MissionMonCompComp extends CCComp { @@ -81,83 +61,32 @@ export class MissionMonCompComp extends CCComp { @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; - // ======================== 插队刷怪队列 ======================== - - /** - * 刷怪队列(优先于常规配置处理): - * 用于插队生成(如运营活动怪、技能召唤怪、剧情强制怪)。 - */ - private MonQueue: Array<{ - /** 怪物 UUID */ - uuid: number, - /** 怪物等级 */ - level: number, - /** 飞行层 */ - flyLane: number, - }> = []; - // ======================== 运行时状态 ======================== /** 全局生成顺序计数器(用于渲染层级排序) */ private globalSpawnOrder: number = 0; - /** 插队刷怪处理计时器 */ - private queueTimer: number = 0; /** 当前波数 */ private currentWave: number = 0; /** 当前波的目标怪物总数 */ private waveTargetCount: number = 0; /** 当前波已生成的怪物数量 */ private waveSpawnedCount: number = 0; - /** 等待生成的怪物队列(由新肉鸽引擎提供) */ + /** 等待生成的怪物队列 */ private pendingMonsters: GeneratedMonster[] = []; // ======================== 生命周期 ======================== - onLoad(){ - this.on(GameEvent.FightReady,this.fight_ready,this) - this.on("SpawnSpecialMonster", this.onSpawnSpecialMonster, this); + onLoad() { + this.on(GameEvent.FightReady, this.fight_ready, this); this.on("PhasePrepareEnd", this.onPhasePrepareEnd, this); this.on("TimeUpAdvanceWave", this.onTimeUpAdvanceWave, this); } - /** - * 帧更新: - * 1. 检查游戏是否运行中。 - * 2. 处理插队刷怪队列。 - * 3. 逐步从 pendingMonsters 队列中生成怪物(受 stop_spawn_mon 限制)。 - */ protected update(dt: number): void { smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length; - - if(!smc.mission.play) return - if(smc.mission.pause) return - if(smc.mission.stop_mon_action) return; - if(!smc.mission.in_fight) return; - - this.updateSpecialQueue(dt); } - // ======================== 事件处理 ======================== - - /** - * 接收特殊刷怪事件并入队。 - * @param event 事件名 - * @param args { uuid: number, level: number, flyLane?: number } - */ - private onSpawnSpecialMonster(event: string, args: any) { - if (!args) return; - mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 收到特殊刷怪指令:`, args); - this.MonQueue.push({ - uuid: args.uuid, - level: args.level, - flyLane: args.flyLane || 0 - }); - // 加速队列消费 - this.queueTimer = 1.0; - } - - start() { - } + start() {} private setupWaveData(monsters: GeneratedMonster[]) { this.pendingMonsters = monsters.slice(0, MissionMonCompComp.MAX_MONSTERS); @@ -166,9 +95,7 @@ export class MissionMonCompComp extends CCComp { let hasBoss = monsters.some(m => m.isBoss); - console.log(`[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`); - const uuids = monsters.map(m => m.uuid); - console.log(`[MissionMonComp] 波次 ${this.currentWave} 怪物 UUID 列表:`, uuids); + mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`); oops.message.dispatchEvent(GameEvent.NewWave, { wave: this.currentWave, @@ -180,81 +107,41 @@ export class MissionMonCompComp extends CCComp { /** * 战斗准备:重置所有运行时状态并开始第一波。 */ - fight_ready(){ - smc.vmdata.mission_data.mon_num=0 - smc.mission.stop_spawn_mon = false - this.globalSpawnOrder = 0 - this.queueTimer = 0 - this.currentWave = 1 - this.waveTargetCount = 0 - this.waveSpawnedCount = 0 - this.MonQueue = [] - this.pendingMonsters = [] + fight_ready() { + smc.vmdata.mission_data.mon_num = 0; + smc.mission.stop_spawn_mon = false; + this.globalSpawnOrder = 0; + this.currentWave = 1; + this.waveTargetCount = 0; + this.waveSpawnedCount = 0; + this.pendingMonsters = []; // 预生成第一波数据以获取数量和 Boss 信息 const monsters = spawningEngine.generateWave(this.currentWave); this.setupWaveData(monsters); - // 如果处于测试模式,英雄也需要限制为只产出一个,这部分通知可以配合使用 if (TestModeConfig.enable) { - mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启:每波仅生成1只基准怪物"); + mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] 测试模式已开启"); } mLogger.log(this.debugMode, 'MissionMonComp', "[MissionMonComp] Starting Wave System"); } - // ======================== 插队刷怪 ======================== - - /** - * 处理插队刷怪队列(每 0.15 秒尝试消费一个): - * 1. 找到后从队列中移除并生成怪物。 - */ - private updateSpecialQueue(dt: number) { - if (this.MonQueue.length <= 0) return; - this.queueTimer += dt; - if (this.queueTimer < 0.15) return; - - const item = this.MonQueue.shift()!; - this.queueTimer = 0; - - const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) || - MonList[MonType.LongBoss].includes(item.uuid); - - const spawnIndex = this.waveSpawnedCount++; - const targetPosIndex = spawnIndex % MissionMonCompComp.MAX_MONSTERS; - - // 构造一个模拟的 GeneratedMonster 数据传递给 addMonsterAtGrid - const base = HeroInfo[item.uuid]; - const monData: GeneratedMonster = { - uuid: item.uuid, - type: MonType.Melee, // 简化的兜底,真实逻辑依赖 heroSet 配置 - hp: base ? base.hp : 100, - ap: base ? base.ap : 10, - affixes: [], - isBoss: isBoss, - spawnIndex: 0 - }; - this.addMonsterAtGrid(targetPosIndex, monData, item.level); - } - // ======================== 波次管理 ======================== /** - * 开始下一波: - * 1. 波数 +1 并更新全局数据。 - * 2. 分发 NewWave 事件(实际的生成在 resetSlotSpawnData 中触发)。 + * 开始下一波:波数 +1 并预生成数据 */ private onTimeUpAdvanceWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; - // 预生成新一波数据以获取数量和 Boss 信息 const monsters = spawningEngine.generateWave(this.currentWave); this.setupWaveData(monsters); } private onPhasePrepareEnd() { - this.resetSlotSpawnData(this.currentWave); + this.resetSlotSpawnData(); // 准备结束阶段,立即刷出本波所有怪物 if (this.pendingMonsters.length > 0) { @@ -262,11 +149,9 @@ export class MissionMonCompComp extends CCComp { for (let i = 0; i < count; i++) { const monData = this.pendingMonsters.shift()!; const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MAX_MONSTERS; - console.log(`[MissionMonComp] [PhasePrepareEnd] 准备生成怪物 UUID=${monData.uuid}, 当前已生成数量=${this.waveSpawnedCount}`); - this.addMonsterAtGrid(targetPosIndex, monData); + this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave); this.waveSpawnedCount++; } - // 生成完毕后清空 pendingMonsters this.pendingMonsters = []; } } @@ -274,14 +159,9 @@ export class MissionMonCompComp extends CCComp { // ======================== 槽位管理 ======================== /** - * 重新分配本波所有怪物状态: - * 1. 清理上一波残留怪物。 - * 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好。 - * - * @param wave 当前波数 + * 清理上一波残留怪物,并重置生成计数 */ - private resetSlotSpawnData(wave: number = 1) { - // 1. 清理上一波残留怪物 + private resetSlotSpawnData() { ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => { const attrs = e.get(HeroAttrsComp); if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) { @@ -289,18 +169,13 @@ export class MissionMonCompComp extends CCComp { } }); - // 2. 重置排号索引 this.waveSpawnedCount = 0; } // ======================== 怪物生成 ======================== /** - * 在指定位置索引处生成一个怪物: - * - * @param posIndex 位置索引 (0-11) - * @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等) - * @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定) + * 在指定位置索引处生成一个怪物 */ private addMonsterAtGrid( posIndex: number, @@ -310,51 +185,32 @@ export class MissionMonCompComp extends CCComp { let mon = ecs.getEntity(Monster); let scale = -1; - // 获取硬编码的占位点坐标,不再使用随机偏移 const basePos = MissionMonCompComp.MON_POSITIONS[posIndex % MissionMonCompComp.MON_POSITIONS.length]; const spawnX = basePos.x; const landingY = basePos.y + (monData.isBoss ? 6 : 0); const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0); this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; - // 如果存在测试技能覆盖,则传递下去(修改 mon.load 逻辑或者通过预存) - // 为了避免侵入 Mon.ts 的原有逻辑,我们先预存 - (mon as any)._testSkills = monData.testSkills; + if (monData.testSkills) { + (mon as any)._testSkills = monData.testSkills; + } mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex); - // 设置渲染排序 const move = mon.get(MonMoveComp); if (move) { move.spawnOrder = this.globalSpawnOrder; } - // 应用新引擎计算好的最终属性和词缀 + // 应用新引擎计算好的最终属性 const model = mon.get(HeroAttrsComp); - if (!model) return; - model.ap = monData.ap; - model.hp_max = monData.hp; - model.hp = model.hp_max; - - // 将词缀记录到属性组件上,供战斗层使用 - (model as any).affixes = monData.affixes || []; - - // 解析特定的抗性词缀 - if (monData.affixes) { - if (monData.affixes.includes(AffixType.CritRes)) { - model.critical_res = 50; - } - if (monData.affixes.includes(AffixType.FreezeRes)) { - model.freeze_res = 50; - } - if (monData.affixes.includes(AffixType.KnockbackRes)) { - model.knockback_res = 50; - } + if (model) { + model.ap = monData.ap; + model.hp_max = monData.hp; + model.hp = model.hp_max; } } - /** ECS 组件移除时触发(当前不销毁节点) */ - reset() { - // this.node.destroy(); - } + /** ECS 组件移除时触发 */ + reset() {} } diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index ff98f241..ec0bc2dd 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -1,49 +1,19 @@ /** * @file RogueConfig.ts - * @description 肉鸽刷怪系统 v2 —— 三层程序化生成架构 + * @description 肉鸽刷怪系统 (重构精简版) * - * 架构:蓝图模板(节奏) + 权重填充(内容) + 自适应微调(数值) - * 主线 15 波 / 5 阶梯 / 每档 3 波(恢复→攀升→高潮) - * 10 种怪物 + 8 种词缀 + 自适应难度 ±15% - * 通关后可选无限模式(分层推进) - * - * GDD: /gdd/rogue-spawning.md + * 核心规则: + * 1. 最大怪物数量 12 个 + * 2. 1-4 波:怪物数量逐步增加(如 1, 2, 4, 6) + * 3. 第 5 波:出现第一个 Boss + * 4. 5 波之后:随机出现 6-12 个怪物 + * 5. 强度:仅通过怪物数量和波次(等级)来逐步提升,每波增加一定比例基础属性。 */ -/** - * 玩家战力与经济推演估算 (前 20 波) - * 规则:初始 4 金币,每波递增 1 金币,单波基础收益上限 10 金币。基础英雄 3 金币,刷新 1 金币。 - * 合成公式:3个 Lv1 = 1个 Lv2,3个 Lv2 = 1个 Lv3 (满级)。即 1个 Lv3 需要 9 个 Lv1。 - * 注:由于抽卡具有随机性,实际需要消耗部分金币刷新,以下推演假设约 20% 金币用于刷新,80% 用于购买。 - * 6 个坑位满编。 - * - * 波次 | 当波收益 | 累计金币 | 累计购卡数 | 预估阵容 (极限最优) | 预估阵容 (脸黑/平滑型) | 阶段说明 - * ------------------------------------------------------------------------------------------------------------------------- - * W1 | 4 (初始) | 4 | 1 | 1*Lv1 | 1*Lv1 | 初始阶段 - * W2 | 5 | 9 | 2 | 2*Lv1 | 2*Lv1 | 积累英雄 - * W3 | 6 | 15 | 4 | 1*Lv2 + 1*Lv1 | 4*Lv1 | 开始成型 - * W4 | 7 | 22 | 6 | 2*Lv2 | 6*Lv1 | 刚好满编(6坑位) - * W5 | 8 | 30 | 8 | 2*Lv2 + 2*Lv1 | 1*Lv2 + 5*Lv1 | 升阶开启 - * W6 | 9 | 39 | 10 | 1*Lv3 + 1*Lv1 | 2*Lv2 + 4*Lv1 | 首个满级核心出现 - * W7 | 10 | 49 | 13 | 1*Lv3 + 1*Lv2 | 3*Lv2 + 3*Lv1 | 阵型强化 - * W8 | 10(Max) | 59 | 15 | 1*Lv3 + 2*Lv2 | 4*Lv2 + 2*Lv1 | - * W9 | 10 | 69 | 18 | 2*Lv3 | 6*Lv2 | 全员至少 Lv2 - * W10 | 10 | 79 | 21 | 2*Lv3 + 1*Lv2 | 1*Lv3 + 4*Lv2 + 1*Lv1 | (脸黑型开始超出6怪自然合成) - * W11 | 10 | 89 | 23 | 2*Lv3 + 1*Lv2 + 1*Lv1 | 1*Lv3 + 5*Lv2 | - * W12 | 10 | 99 | 26 | 2*Lv3 + 2*Lv2 + 1*Lv1 | 2*Lv3 + 2*Lv2 + 1*Lv1 | 双核心阵型确立 - * W13 | 10 | 109 | 29 | 3*Lv3 + 1*Lv1 | 2*Lv3 + 3*Lv2 + 1*Lv1 | - * W14 | 10 | 119 | 31 | 3*Lv3 + 1*Lv2 + 1*Lv1 | 2*Lv3 + 4*Lv2 | - * W15 | 10 | 129 | 34 | 3*Lv3 + 2*Lv2 + 1*Lv1 | 3*Lv3 + 2*Lv2 | 游戏后期,半数满级 - * W16 | 10 | 139 | 37 | 4*Lv3 + 1*Lv1 | 3*Lv3 + 3*Lv2 | - * W17 | 10 | 149 | 39 | 4*Lv3 + 1*Lv2 | 4*Lv3 + 1*Lv2 | - * W18 | 10 | 159 | 42 | 4*Lv3 + 2*Lv2 | 4*Lv3 + 1*Lv2 + 1*Lv1 | - * W19 | 10 | 169 | 45 | 5*Lv3 | 5*Lv3 | - * W20 | 10 | 179 | 47 | 5*Lv3 + 1*Lv1 | 5*Lv3 + 1*Lv1 | 大成阵型,接近全员满级 - */ +import { HeroInfo } from "../common/config/heroSet"; // ======================== 怪物类型枚举 ======================== -/** 怪物类型(8 种) */ export enum MonType { Melee = 0, Heavy = 1, @@ -55,10 +25,6 @@ export enum MonType { LongBoss = 9, } -/** - * 怪物类型名称映射(调试/日志用) - * @example MonTypeName[MonType.Melee] // => "近战" - */ export const MonTypeName: Record = { [MonType.Melee]: "近战", [MonType.Heavy]: "重型", @@ -72,7 +38,6 @@ export const MonTypeName: Record = { // ======================== 词缀类型枚举 ======================== -/** 怪物词缀(8 种) */ export enum AffixType { Elite = 0, Berserk = 1, @@ -87,131 +52,8 @@ export enum AffixType { KnockbackRes = 10, } -/** - * 词缀配置接口 - * @property name - 词缀中文名(显示/调试用) - * @property hpMultiplier - HP 倍率修饰,乘算到最终 HP 上(1.0 = 无变化) - * @property apMultiplier - AP 倍率修饰,乘算到最终 AP 上(1.0 = 无变化) - * @property cost - 该词缀占用的难度预算额外成本 - * @property tierMin - 首次可出现的最低阶梯编号(Tier),低于此阶梯不会触发 - * @property description - 词缀效果简述 - */ -export interface AffixConfig { - name: string - hpMultiplier: number - apMultiplier: number - cost: number - tierMin: number - description: string -} - -/** - * 词缀详细配置表 - * key = AffixType 枚举值,value = 对应词缀的完整配置 - * @see AffixConfig 字段说明 - */ -export const AffixConfigs: Record = { - [AffixType.Elite]: { - name: "精英", hpMultiplier: 1.5, apMultiplier: 1.3, - cost: 20, tierMin: 2, description: "+50% HP, +30% AP", - }, - [AffixType.Berserk]: { - name: "狂暴", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 15, tierMin: 2, description: "攻速 ×1.5 (行为层实现)", - }, - [AffixType.Shield]: { - name: "护盾", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 25, tierMin: 2, description: "开局带 抵御2次 伤害吸收盾", - }, - [AffixType.Regen]: { - name: "再生", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 20, tierMin: 3, description: "每秒回复 2% HP", - }, - [AffixType.Swift]: { - name: "疾速", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 10, tierMin: 3, description: "移速 ×2", - }, - [AffixType.Giant]: { - name: "巨型", hpMultiplier: 2.0, apMultiplier: 1.5, - cost: 30, tierMin: 3, description: "×2 体型, +100% HP, +50% AP", - }, - [AffixType.Chain]: { - name: "连锁", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 20, tierMin: 4, description: "攻击附带 50% 溅射伤害", - }, - [AffixType.SummonerA]: { - name: "召唤", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 25, tierMin: 4, description: "每 8 秒召唤 1 个小怪", - }, - [AffixType.CritRes]: { - name: "坚韧", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 15, tierMin: 2, description: "+暴击抗性", - }, - [AffixType.FreezeRes]: { - name: "防寒", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 15, tierMin: 2, description: "+冰冻抗性", - }, - [AffixType.KnockbackRes]: { - name: "稳固", hpMultiplier: 1.0, apMultiplier: 1.0, - cost: 15, tierMin: 2, description: "+击退抗性", - }, -} - -/** - * 词缀互斥组:同组内的词缀最多只能出现一个 - * 当随机到互斥冲突时,按 AffixPriority 保留优先级更高的词缀 - * @example [AffixType.Giant, AffixType.Swift] — 巨型与疾速不能同时出现 - */ -export const AffixMutualExclusion: AffixType[][] = [ - [AffixType.Giant, AffixType.Swift], - [AffixType.Regen, AffixType.Shield], -] - -/** - * 词缀优先级(从高到低) - * 互斥冲突时保留此数组中靠前的词缀,丢弃靠后的 - */ -export const AffixPriority: AffixType[] = [ - AffixType.Shield, AffixType.Regen, AffixType.Giant, - AffixType.Swift, AffixType.Elite, AffixType.Berserk, - AffixType.Chain, AffixType.SummonerA, AffixType.CritRes, AffixType.FreezeRes, AffixType.KnockbackRes, -] - -// ======================== 蓝图模板类型 ======================== - -/** 模板类型 */ -export enum TemplateType { - REST = 0, - NORMAL = 1, - MIXED = 2, - ELITE = 3, - BOSS = 4, -} - -/** - * 模板类型修正系数 - * 乘算到 base_budget 上,控制不同类型模板的预算总量 - * - REST: 0.5(恢复波预算减半) - * - NORMAL: 1.0(标准预算) - * - MIXED: 1.2(混合波预算 +20%) - * - ELITE: 0.8(精英波怪少但强,预算略低) - * - BOSS: 1.5(Boss 波预算 +50%) - */ -export const TemplateModifier: Record = { - [TemplateType.REST]: 0.5, - [TemplateType.NORMAL]: 1.0, - [TemplateType.MIXED]: 1.2, - [TemplateType.ELITE]: 0.8, - [TemplateType.BOSS]: 1.5, -} - // ======================== 怪物 UUID 池 ======================== -/** - * 各怪物类型对应的 UUID 列表 - * 生成怪物时从对应数组中随机抽取一个 UUID,用于匹配 heroSet.ts 中的怪物配置 - * UUID 编号段:6001-6006(兽人)| 6101-6105(亡灵)| 6201-6205(新类型) - */ export const MonList: Record = { [MonType.Melee]: [6001, 6002], [MonType.Heavy]: [6003], @@ -223,347 +65,19 @@ export const MonList: Record = { [MonType.LongBoss]: [6101, 6103, 6105], } -// ======================== 怪物基础属性 & 成本 ======================== - -/** - * 怪物基础属性接口 - * 定义怪物在 Tier 1(无词缀、adaptive=1.0)时的原始属性 - * @property hp - 基础生命值(final_hp = hp × tier_multiplier × affix_hp_mul × adaptive_factor) - * @property ap - 基础攻击力(final_ap = ap × tier_multiplier × affix_ap_mul × adaptive_factor) - * @property cost - 在难度预算中的成本,决定每波能出多少只 - * @property isBoss - 是否为 Boss 类型(影响词缀叠加上限和 UI 显示) - */ -export interface MonsterBaseStats { - hp: number - ap: number - cost: number - isBoss: boolean -} - -/** - * 怪物基础属性表 - * key = MonType 枚举值,value = 该类型在 Tier 1 时的基础属性 - * @see MonsterBaseStats 字段说明 - */ -export const MonsterStats: Record = { - [MonType.Melee]: { hp: 350, ap: 16, cost: 22, isBoss: false }, - [MonType.Heavy]: { hp: 1400, ap: 30, cost: 42, isBoss: false }, - [MonType.Long]: { hp: 300, ap: 52, cost: 30, isBoss: false }, - [MonType.Support]: { hp: 480, ap: 36, cost: 34, isBoss: false }, - [MonType.Summoner]: { hp: 420, ap: 48, cost: 42, isBoss: false }, - [MonType.Assassin]: { hp: 330, ap: 58, cost: 30, isBoss: false }, - [MonType.MeleeBoss]: { hp: 12000, ap: 40, cost: 160, isBoss: true }, - [MonType.LongBoss]: { hp: 3500, ap: 58, cost: 160, isBoss: true }, -} - -// ======================== 阶梯(Tier)配置 ======================== - -/** - * 阶梯(Tier)配置接口 - * @property multiplier - 属性倍率,所有怪物的 HP/AP 乘以此值(T1=1.0, T10=5.5) - * @property budget - 该档基础难度预算,怪物成本总和的上限 - * @property availableTypes - 该档允许生成的怪物类型列表(未列入的类型不会出现) - * @property isBossTier - 该档第 3 波是否为 Boss 波 - */ -export interface TierConfig { - multiplier: number - budget: number - availableTypes: MonType[] - isBossTier: boolean -} - -/** Boss 出现的 Tier 集合(MiniBoss 或 MajorBoss) */ -const BOSS_TIERS = new Set([1, 2, 3, 4, 5]) -/** MajorBoss(高难度 Boss)出现的 Tier 集合 */ -const MAJOR_BOSS_TIERS = new Set([3, 5]) - -/** - * 5 阶梯配置表 (基于"第5波单怪=1个英雄"的基准推演) - * - * 核心设计推演: - * 1. 玩家反馈:Tier 2 (第4-6波) 的单只怪物强度,刚好对应 1 个 Lv1 英雄。 - * 2. 设 Tier 2 的 Multiplier 为 2.0,基准近战怪成本为 22。 - * 3. 则 1个购卡战力 (1个Lv1英雄) = 22 * 2.0 = 44 难度点。 - * 4. 推演公式:当前波次总难度 (Budget * Multiplier) ≈ 累计购卡数 * 44。 - * 5. 约束:为避免触发同屏最大 12 只怪的截断机制,后期 Budget 锁定在 260-280,难度全靠 Multiplier (属性) 拉升。 - */ -export const TierConfigs: Record = { - // T1(W1-W3): 预计累计 2.3 卡。目标总难度 101。Budget 80 * 1.1 = 88 - 1: { multiplier: 1.1, budget: 80, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long], isBossTier: false }, - // T2(W4-W6): 预计累计 8.0 卡。目标总难度 352。Budget 160 * 2.0 = 320 - 2: { multiplier: 2.0, budget: 160, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support], isBossTier: true }, - // T3(W7-W9): 预计累计 15.3卡。目标总难度 673。Budget 220 * 3.0 = 660 - 3: { multiplier: 3.0, budget: 220, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Assassin], isBossTier: true }, - // T4(W10-W12):预计累计 23.3卡。目标总难度 1025。Budget 260 * 4.0 = 1040 - 4: { multiplier: 4.0, budget: 260, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Assassin, MonType.Summoner], isBossTier: true }, - // T5(W13-W15):预计累计 31.3卡。目标总难度 1377。Budget 280 * 5.2 = 1456 - 5: { multiplier: 5.2, budget: 280, availableTypes: [MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, MonType.Assassin, MonType.Summoner], isBossTier: true }, -} - -/** - * 获取指定阶梯的配置 - * 支持 Tier 1-5(查表)和 Tier 6+(无限模式,递推计算:T(n) = T(n-1) × 1.2) - * - * @param tier - 阶梯编号(1-5 主线,6+ 无限模式) - * @returns TierConfig 该阶梯的完整配置 - */ -export function getTierConfig(tier: number): TierConfig { - if (TierConfigs[tier]) return TierConfigs[tier] - - // 无限模式:T(n) = T(n-1) × 1.2 - const prev = getTierConfig(tier - 1) - const growthRate = InfiniteModeConfig.tierGrowthRate - const allTypes: MonType[] = [ - MonType.Melee, MonType.Heavy, MonType.Long, MonType.Support, - MonType.Assassin, MonType.Summoner, - ] - return { - multiplier: Math.round(prev.multiplier * growthRate * 100) / 100, - budget: Math.round(prev.budget * growthRate), - availableTypes: allTypes, - isBossTier: true, // 无限模式每层都有 Boss - } -} - -// ======================== 词缀概率配置 ======================== - -/** - * 每个 Tier 的基础词缀触发概率(0.0-1.0) - * T1-T2: 0%(前期无词缀) - * T3: 15%, T4: 30%, T5: 50% - * 最终概率 = baseAffixChance × roleMultiplier - */ -export const BaseAffixChance: Record = { - 1: 0, 2: 0.10, 3: 0.25, 4: 0.40, 5: 0.60, -} - -/** - * 角色类型对词缀概率的倍率 - * final_chance = baseAffixChance[tier] × roleMultiplier - * - normal: 1.0x — 普通怪 - * - miniBoss: 2.0x — MiniBoss(T2/4/7/9 的 Boss) - * - majorBoss: 3.0x — MajorBoss(T5/10 的 Boss) - */ -export const AffixRoleMultiplier = { - normal: 1.0, - miniBoss: 2.0, - majorBoss: 3.0, -} - -/** - * 词缀叠加上限(按角色类型) - * - normal: 1 — 普通怪最多 1 个词缀 - * - miniBoss: 2 — MiniBoss 最多 2 个词缀 - * - majorBoss: 3 — MajorBoss/FinalBoss 最多 3 个词缀 - */ -export const AffixStackLimit = { - normal: 1, - miniBoss: 2, - majorBoss: 3, -} - -// ======================== 蓝图模板池 ======================== - -/** - * 蓝图模板的怪物槽位定义 - * @property typePool - 候选怪物类型数组,生成时从中随机选一个(等概率) - * @property countMin - 该槽位最少生成的怪物数量 - * @property countMax - 该槽位最多生成的怪物数量(实际在 [min, max] 范围内随机) - * @property weight - 槽位权重(保留扩展用,当前各槽位按顺序填充) - * @property forceAffix - 若为 true,该槽位中的怪物必定触发词缀判定(至少 50% 概率) - */ -export interface BlueprintSlot { - typePool: MonType[] - countMin: number - countMax: number - weight: number - forceAffix?: boolean -} - -/** - * 蓝图模板定义 - * @property id - 模板唯一标识(如 "R1", "N2", "B3", "TUTORIAL") - * @property type - 模板类型(REST/NORMAL/MIXED/ELITE/BOSS),决定模板修正系数 - * @property tierMin - 首次可出现的最低 Tier(低于此 Tier 不会选到此模板) - * @property slots - 怪物槽位数组,按顺序依次填充 - * @property allowAffix - 是否允许对槽位中的怪物进行词缀判定 - */ -export interface BlueprintTemplate { - id: string - type: TemplateType - tierMin: number - slots: BlueprintSlot[] - allowAffix: boolean -} - -/** - * 蓝图模板池 - * 包含所有预定义模板,按类型分为 REST/NORMAL/MIXED/ELITE/BOSS 五类 - * 生成引擎根据当前 Tier 和波内位置从中筛选并随机抽取 - */ -export const BlueprintTemplates: BlueprintTemplate[] = [ - { id: "R1", type: TemplateType.REST, tierMin: 1, allowAffix: false, - slots: [{ typePool: [MonType.Melee], countMin: 1, countMax: 2, weight: 1.0 }] }, - { id: "R2", type: TemplateType.REST, tierMin: 2, allowAffix: false, - slots: [{ typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 4, weight: 1.0 }] }, - { id: "R3", type: TemplateType.REST, tierMin: 3, allowAffix: false, - slots: [{ typePool: [MonType.Melee, MonType.Long], countMin: 3, countMax: 5, weight: 1.0 }] }, - - { id: "N1", type: TemplateType.NORMAL, tierMin: 1, allowAffix: false, - slots: [{ typePool: [MonType.Melee], countMin: 2, countMax: 4, weight: 1.0 }] }, - { id: "N2", type: TemplateType.NORMAL, tierMin: 1, allowAffix: true, - slots: [ - { typePool: [MonType.Melee], countMin: 1, countMax: 3, weight: 0.6 }, - { typePool: [MonType.Long], countMin: 1, countMax: 2, weight: 0.4 }, - ] }, - { id: "N3", type: TemplateType.NORMAL, tierMin: 2, allowAffix: true, - slots: [ - { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 4, weight: 0.5 }, - { typePool: [MonType.Long], countMin: 1, countMax: 2, weight: 0.3 }, - { typePool: [MonType.Support], countMin: 1, countMax: 2, weight: 0.2 }, - ] }, - { id: "N4", type: TemplateType.NORMAL, tierMin: 3, allowAffix: true, - slots: [ - { typePool: [MonType.Melee, MonType.Heavy], countMin: 3, countMax: 5, weight: 0.4 }, - { typePool: [MonType.Long, MonType.Assassin], countMin: 2, countMax: 3, weight: 0.3 }, - { typePool: [MonType.Support], countMin: 1, countMax: 2, weight: 0.3 }, - ] }, - - { id: "M1", type: TemplateType.MIXED, tierMin: 1, allowAffix: true, - slots: [ - { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 3, weight: 0.4 }, - { typePool: [MonType.Long], countMin: 1, countMax: 2, weight: 0.3 }, - { typePool: [MonType.Support], countMin: 1, countMax: 1, weight: 0.3 }, - ] }, - { id: "M2", type: TemplateType.MIXED, tierMin: 2, allowAffix: true, - slots: [ - { typePool: [MonType.Melee, MonType.Heavy], countMin: 3, countMax: 4, weight: 0.3 }, - { typePool: [MonType.Long, MonType.Assassin], countMin: 2, countMax: 3, weight: 0.3 }, - { typePool: [MonType.Support], countMin: 1, countMax: 2, weight: 0.4 }, - ] }, - { id: "M3", type: TemplateType.MIXED, tierMin: 3, allowAffix: true, - slots: [ - { typePool: [MonType.Melee, MonType.Heavy], countMin: 3, countMax: 5, weight: 0.3 }, - { typePool: [MonType.Long, MonType.Assassin], countMin: 2, countMax: 3, weight: 0.3 }, - { typePool: [MonType.Summoner], countMin: 1, countMax: 2, weight: 0.2 }, - { typePool: [MonType.Support], countMin: 1, countMax: 2, weight: 0.2 }, - ] }, - - { id: "E1", type: TemplateType.ELITE, tierMin: 2, allowAffix: true, - slots: [ - { typePool: [MonType.Heavy], countMin: 1, countMax: 2, weight: 0.5, forceAffix: true }, - { typePool: [MonType.Long, MonType.Assassin], countMin: 1, countMax: 2, weight: 0.5, forceAffix: true }, - ] }, - { id: "E2", type: TemplateType.ELITE, tierMin: 3, allowAffix: true, - slots: [ - { typePool: [MonType.Heavy], countMin: 1, countMax: 2, weight: 0.3, forceAffix: true }, - { typePool: [MonType.Assassin], countMin: 2, countMax: 3, weight: 0.4, forceAffix: true }, - { typePool: [MonType.Support], countMin: 1, countMax: 2, weight: 0.3, forceAffix: true }, - ] }, - - { id: "B1", type: TemplateType.BOSS, tierMin: 2, allowAffix: true, - slots: [ - { typePool: [MonType.MeleeBoss], countMin: 1, countMax: 1, weight: 1.0 }, - { typePool: [MonType.Melee], countMin: 2, countMax: 4, weight: 0.5 }, - { typePool: [MonType.Long], countMin: 1, countMax: 2, weight: 0.3 }, - { typePool: [MonType.Support], countMin: 1, countMax: 1, weight: 0.2 }, - ] }, - { id: "B2", type: TemplateType.BOSS, tierMin: 2, allowAffix: true, - slots: [ - { typePool: [MonType.MeleeBoss], countMin: 1, countMax: 1, weight: 1.0 }, - { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 4, weight: 0.5 }, - { typePool: [MonType.Long, MonType.Support], countMin: 1, countMax: 2, weight: 0.3 }, - { typePool: [MonType.Assassin], countMin: 1, countMax: 2, weight: 0.2 }, - ] }, - { id: "B3", type: TemplateType.BOSS, tierMin: 3, allowAffix: true, - slots: [ - { typePool: [MonType.MeleeBoss, MonType.LongBoss], countMin: 1, countMax: 1, weight: 1.0 }, - { typePool: [MonType.Melee, MonType.Heavy], countMin: 2, countMax: 4, weight: 0.35 }, - { typePool: [MonType.Long, MonType.Assassin], countMin: 2, countMax: 3, weight: 0.35 }, - { typePool: [MonType.Support], countMin: 1, countMax: 2, weight: 0.3 }, - ] }, - { id: "B4", type: TemplateType.BOSS, tierMin: 4, allowAffix: true, - slots: [ - { typePool: [MonType.MeleeBoss, MonType.LongBoss], countMin: 1, countMax: 1, weight: 1.0 }, - { typePool: [MonType.Heavy], countMin: 2, countMax: 3, weight: 0.35 }, - { typePool: [MonType.Assassin], countMin: 2, countMax: 3, weight: 0.3 }, - { typePool: [MonType.Summoner, MonType.Support], countMin: 1, countMax: 2, weight: 0.2 }, - { typePool: [MonType.Melee], countMin: 1, countMax: 3, weight: 0.15 }, - ] }, - - { id: "TUTORIAL", type: TemplateType.NORMAL, tierMin: 1, allowAffix: false, - slots: [{ typePool: [MonType.Melee], countMin: 2, countMax: 2, weight: 1.0 }] }, -] - -// ======================== 自适应难度配置 ======================== - -/** - * 自适应难度配置 - * @property factorMin - adaptive_factor 下限(0.85,最多降 15%) - * @property factorMax - adaptive_factor 上限(1.15,最多加 15%) - * @property deltaPerWave - 每波调整步长(0.03) - * @property targetClearTime - 目标通关时间(秒),低于此视为"玩家太强" - * @property strongThreshold - 英雄存活率高于此视为"太强"(0.8 = 80% 存活) - * @property weakThreshold - 英雄存活率低于此视为"太弱"(0.3 = 30% 存活) - * @property maxConsecutiveDirection - 允许连续同方向调整的最大波数 - * @property antiDriftDelta - 反漂移时的反向微调量 - */ -export const AdaptiveConfig = { - factorMin: 0.75, - factorMax: 1.30, - deltaPerWave: 0.04, - targetClearTime: 12.0, - strongThreshold: 0.8, - weakThreshold: 0.4, - maxConsecutiveDirection: 4, - antiDriftDelta: 0.015, -} - -// ======================== 无限模式配置 ======================== - -/** - * 无限模式配置 - * @property wavesPerLayer - 每层的波数(4 波:REST→NORMAL→MIXED→BOSS) - * @property tierGrowthRate - 属性倍率递增率(每层 ×1.2) - * @property bossAffixPerLayer - Boss 每层额外获得的词缀数 - * @property megaBossInterval - 每 N 层出现一次 Mega Boss - */ -export const InfiniteModeConfig = { - wavesPerLayer: 4, - tierGrowthRate: 1.2, - bossAffixPerLayer: 1, - megaBossInterval: 3, -} - // ======================== 测试模式配置 ======================== -/** - * 单挑测试模式配置 - * 用于寻找 1v1 环境下单只怪物与单只英雄的战力基准 - * 开启后每波只生成 1 只怪物,且其属性随波次平滑递增 - */ export const TestModeConfig = { - /** 是否开启单挑测试模式 */ - enable: true, - /** 测试模式中生成怪物的基础生命值 (对应 1级 英雄) */ + enable: false, // 默认关闭测试模式 baseHp: 150, - /** 测试模式中生成怪物的基础攻击力 (对应 1级 英雄) */ baseAp: 12, - /** 每波次基础属性增加比例 (例如 0.2 表示每波增加 20%) */ growthRatePerWave: 0.2, - /** 固定的测试怪物类型 */ monType: MonType.Melee, - /** 固定的测试怪物 UUID (如 6001 兽人战士) */ monUuid: 6001, - /** 测试模式中生成怪物是否携带特定词缀(为空则不带) */ affixes: [] as AffixType[], - /** 每次刷新的怪物数量 */ spawnCount: 1, - // ===== 附加技能测试覆盖配置 ===== - /** 替换普攻技能(清除旧技能,添加新技能) */ 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, @@ -571,47 +85,8 @@ export const TestModeConfig = { fend: undefined as { s_uuid: number; t_num: number; overrides?: any }[] | undefined, } -// ======================== 向后兼容接口 ======================== - -/** - * 向后兼容:波次占位配置 - * @property type - 怪物类型编号(对应 MonType 枚举值) - * @property count - 该类型怪物的数量 - * @property affixes - 可选,该组怪物的词缀列表(新系统新增字段,旧代码可忽略) - */ -export interface IWaveSlot { - type: number - count: number - affixes?: AffixType[] -} - -/** 全局刷怪强度偏差系数(保留向后兼容,新系统中固定为 1.0) */ -export const SpawnPowerBias = 1.0 - // ======================== 生成结果接口 ======================== -/** - * 最终生成的怪物实例 — 战斗系统的输入数据 - * - * 战斗系统收到此数组后,按 spawnIndex 顺序依次在战场上放置怪物。 - * 需要通过 uuid 去 heroSet.ts 中查找对应的完整怪物配置(路径、技能、速度等)。 - * hp/ap 是已经过阶梯成长、词缀修饰、自适应微调后的最终值,直接使用即可。 - * - * @property uuid - 怪物 UUID,对应 heroSet.ts 中 HeroInfo 的 key(如 6001, 6203) - * @property type - 怪物类型(MonType 枚举),用于判断近战/远程/Boss 等行为模式 - * @property hp - 最终生命值(= base_hp × tier_multiplier × affix_hp_mul × adaptive_factor) - * @property ap - 最终攻击力(= base_ap × tier_multiplier × affix_ap_mul × adaptive_factor) - * @property affixes - 词缀数组(空数组表示无词缀)。战斗系统需实现各词缀的行为效果: - * - Elite/Berserk/Giant: 属性已在 hp/ap 中体现 - * - Shield: 需在战斗中实现伤害吸收盾 - * - Regen: 需在战斗中实现每秒回血 - * - Swift: 需在战斗中实现移速翻倍 - * - Chain: 需在战斗中实现溅射伤害 - * - SummonerA: 需在战斗中实现定期召唤 - * @property isBoss - 是否为 Boss(影响 UI 显示:Boss 血条独立显示在屏幕顶部) - * @property spawnIndex - 在该波次中的生成顺序(0 起始),决定战场上的排列位置 - * 排列规则:spawnIndex=0 在最前(X 最小),后续依次向后间隔排列 - */ export interface GeneratedMonster { uuid: number type: MonType @@ -620,7 +95,6 @@ export interface GeneratedMonster { affixes: AffixType[] isBoss: boolean spawnIndex: number - /** 测试模式专属:临时覆盖怪物技能配置 */ testSkills?: { skill?: { s_uuid: number; cd?: number; overrides?: any }; atking?: { s_uuid: number; t_num: number; overrides?: any }[]; @@ -633,83 +107,22 @@ export interface GeneratedMonster { // ======================== 生成引擎 ======================== -/** - * 肉鸽刷怪引擎 — 核心生成器 - * - * 职责:根据波次号(或无限模式层号),程序化生成该波的怪物列表。 - * 每局游戏创建一个实例,通过 reset() 重置,跨波次保持自适应状态。 - * - * 使用流程: - * 1. 游戏开始 → `const engine = new RogueSpawningEngine()` 或使用全局 `spawningEngine` - * 2. 每波开始 → `const monsters = engine.generateWave(waveNumber)` - * 3. 将 monsters 传给战斗系统生成怪物 - * 4. 战斗结束 → `engine.updateAdaptive(heroesAliveRatio, clearTime)` - * 5. 下一波 → 回到步骤 2 - * 6. 新一局 → `engine.reset()` - * - * 对接方: - * - 战斗系统:消费 `GeneratedMonster[]` - * - UI 系统:读取 `monsters[n].isBoss`、`monsters[n].affixes` 显示 Boss 血条和词缀标识 - * - 分数系统:根据 `monsters[n].isBoss` 统计 Boss 击杀 - */ export class RogueSpawningEngine { - private adaptiveFactor = 1.0 - private consecutiveDirection = 0 - private lastDirection = 0 // -1 降难, 0 维持, 1 加难 - private lastTemplateId = "" - private lastTemplateType: TemplateType | null = null - private consecutiveTemplateCount = 0 - /** - * 生成指定波次的怪物列表(主线 1-15 波) - * - * 内部流程: - * 1. 根据 waveNumber 计算 tier 和 waveInTier - * 2. W1 特殊处理(固定教程模板:5 个 Melee) - * 3. 根据 waveInTier 和 isBossTier 选择蓝图模板 - * 4. 计算难度预算 = base_budget × template_modifier × adaptive_factor - * 5. 按模板槽位填充怪物、应用词缀、计算最终属性 - * - * @param waveNumber - 波次编号(1-15) - * 1-3 → Tier 1, 4-6 → Tier 2, ..., 13-15 → Tier 5 - * < 1 返回空数组 - * - * @returns GeneratedMonster[] 该波次的怪物实例数组,按 spawnIndex 排列。 - * 战斗系统按此顺序在战场上依次放置怪物。 - * 每次调用结果不同(随机生成),如需固定结果请在同一次调用中缓存。 - * - * @example - * // 生成第 1 波(教程:5 个 Melee) - * const w1 = engine.generateWave(1) - * // w1 = [{ uuid: 6001, type: 0, hp: 120, ap: 12, affixes: [], isBoss: false, spawnIndex: 0 }, ...] - * - * // 生成第 6 波(Tier 2 Boss 波) - * const w6 = engine.generateWave(6) - * // w6 包含 1 个 MeleeBoss + 若干小怪 - */ generateWave(waveNumber: number): GeneratedMonster[] { - if (waveNumber < 1) return [] + if (waveNumber < 1) return []; // 测试模式拦截 if (TestModeConfig.enable) { const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave; - - // 提取词缀加成 - const affixHpMul = 1.0 + TestModeConfig.affixes.reduce( - (sum, a) => sum + (AffixConfigs[a].hpMultiplier - 1.0), 0 - ); - const affixApMul = 1.0 + TestModeConfig.affixes.reduce( - (sum, a) => sum + (AffixConfigs[a].apMultiplier - 1.0), 0 - ); - 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 * affixHpMul), - ap: Math.round(TestModeConfig.baseAp * growth * affixApMul), + hp: Math.round(TestModeConfig.baseHp * growth), + ap: Math.round(TestModeConfig.baseAp * growth), affixes: [...TestModeConfig.affixes], isBoss: false, spawnIndex: i, @@ -726,385 +139,112 @@ export class RogueSpawningEngine { return monsters; } - const tier = Math.ceil(waveNumber / 3) - const waveInTier = ((waveNumber - 1) % 3) + 1 // 1, 2, or 3 + // 1. 确定生成数量 + let count = this.getWaveMonsterCount(waveNumber); - // 特殊:Tier 1 Wave 1 = 教程 - if (waveNumber === 1) { - return this.spawnFromTemplate( - BlueprintTemplates.find(t => t.id === "TUTORIAL")!, - 1, getTierConfig(1).budget * this.adaptiveFactor - ) + // 第 5 波之后,在 count 的基础上加上随机逻辑 (覆盖原有固定返回值) + if (waveNumber > 5 && !TestModeConfig.enable) { + count = Math.floor(Math.random() * 7) + 6; } - const isBossWave = TierConfigs[tier]?.isBossTier && waveInTier === 3 - const template = this.selectTemplate(tier, waveInTier, isBossWave) - const tierConfig = getTierConfig(tier) - const budget = Math.round( - tierConfig.budget * TemplateModifier[template.type] * this.adaptiveFactor - ) + // 限制最大怪物数为 12 + count = Math.min(count, 12); - return this.spawnFromTemplate(template, tier, budget) - } + // 每 5 波出现一次 Boss + const isBossWave = (waveNumber % 5 === 0); - /** - * 生成无限模式指定层的指定波 - * - * 无限模式结构:每层 4 波(REST → NORMAL → MIXED → BOSS),Tier 从 6 开始递增。 - * 属性倍率按 T(n) = T(n-1) × 1.2 递推,无上限。 - * - * @param layer - 层编号(从 1 开始) - * Tier = 5 + layer(layer=1 → Tier 6, multiplier=6.6) - * @param waveInLayer - 层内波次编号(1-4) - * 1: REST(恢复波) - * 2: NORMAL(标准波) - * 3: MIXED(混合波) - * 4: BOSS(Boss 波,Boss 每层额外获得词缀) - * - * @returns GeneratedMonster[] 该波的怪物实例数组 - * - * @example - * // 无限模式第 1 层第 4 波(Boss 波,Tier 6) - * const boss = engine.generateInfiniteWave(1, 4) - * // boss 包含 1 个 Boss + 若干小怪,属性倍率 = 6.6x - */ - generateInfiniteWave(layer: number, waveInLayer: number): GeneratedMonster[] { - const tier = 5 + layer - const tierConfig = getTierConfig(tier) - let templateType: TemplateType + const monsters: GeneratedMonster[] = []; - // 无限模式每层 4 波:REST → NORMAL → MIXED → BOSS - switch (waveInLayer) { - case 1: templateType = TemplateType.REST; break - case 2: templateType = TemplateType.NORMAL; break - case 3: templateType = TemplateType.MIXED; break - case 4: templateType = TemplateType.BOSS; break - default: templateType = TemplateType.NORMAL; break - } + // 普通怪池 + const normalTypes = [ + MonType.Melee, MonType.Heavy, MonType.Long, + MonType.Support, MonType.Assassin, MonType.Summoner + ]; - const templates = BlueprintTemplates.filter(t => - t.type === templateType && t.tierMin <= 5 - ) - const template = this.pickRandomTemplate(templates) - const budget = Math.round( - tierConfig.budget * TemplateModifier[template.type] * this.adaptiveFactor - ) + for (let i = 0; i < count; i++) { + let type: MonType; + let isBoss = false; - return this.spawnFromTemplate(template, tier, budget) - } - - /** - * 根据战斗结果更新自适应难度系数 - * - * 每波战斗结束后调用一次。引擎内部维护 adaptiveFactor(0.85-1.15), - * 该系数会乘算到后续所有波的怪物 HP/AP 上。 - * - * 调整规则: - * - 玩家太强(存活率 ≥ 80% 且 通关时间 < 15s)→ adaptiveFactor += 0.03 - * - 玩家太弱(存活率 ≤ 30%) → adaptiveFactor -= 0.03 - * - 其他情况 → 不调整 - * - 连续 5 波同方向 → 强制反向微调 0.01(反漂移) - * - * @param heroesAliveRatio - 本波结束时英雄存活比例(0.0-1.0) - * = 存活英雄数 / 总英雄数 - * 3 英雄中存活 2 个 → 0.67 - * 3 英雄中存活 3 个 → 1.0 - * @param clearTime - 本波通关时间(秒),从怪物全部生成到全部击败的时长 - * < 15s 视为"秒杀",配合高存活率判定玩家过强 - * - * @example - * // 3 英雄全部存活,5 秒通关 → 判定为"太强",adaptiveFactor += 0.03 - * engine.updateAdaptive(1.0, 5.0) - * - * // 仅 1 英雄存活 → 判定为"太弱",adaptiveFactor -= 0.03 - * engine.updateAdaptive(0.33, 30.0) - */ - updateAdaptive(heroesAliveRatio: number, clearTime: number): void { - let direction = 0 - let delta = 0 - - if (heroesAliveRatio >= AdaptiveConfig.strongThreshold && - clearTime < AdaptiveConfig.targetClearTime) { - direction = 1 - delta = AdaptiveConfig.deltaPerWave - } else if (heroesAliveRatio <= AdaptiveConfig.weakThreshold) { - direction = -1 - delta = -AdaptiveConfig.deltaPerWave - } - - // 反漂移:连续同方向超过阈值时强制反向 - if (direction !== 0 && direction === this.lastDirection) { - this.consecutiveDirection++ - if (this.consecutiveDirection >= AdaptiveConfig.maxConsecutiveDirection) { - delta = direction === 1 - ? -AdaptiveConfig.antiDriftDelta - : AdaptiveConfig.antiDriftDelta - this.consecutiveDirection = 0 + 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)]; } - } else { - this.consecutiveDirection = direction !== 0 ? 1 : 0 - } - this.lastDirection = direction - this.adaptiveFactor = Math.max( - AdaptiveConfig.factorMin, - Math.min(AdaptiveConfig.factorMax, this.adaptiveFactor + delta) - ) - } + const uuids = MonList[type] || MonList[MonType.Melee]; + const uuid = uuids[Math.floor(Math.random() * uuids.length)]; - /** - * 获取当前自适应难度系数 - * @returns number 当前 adaptive_factor 值(0.85-1.15) - * 1.0 = 无调整,>1.0 = 更难,<1.0 = 更简单 - */ - getAdaptiveFactor(): number { return this.adaptiveFactor } + // 从 HeroInfo 中获取基础属性,如果没找到则给一个兜底值 + const baseInfo = HeroInfo[uuid]; + const baseHp = baseInfo ? baseInfo.hp : 100; + const baseAp = baseInfo ? baseInfo.ap : 10; - /** - * 重置引擎状态(新一局游戏开始时调用) - * 将 adaptiveFactor 恢复为 1.0,清空模板连续计数和方向记录 - */ - reset(): void { - this.adaptiveFactor = 1.0 - this.consecutiveDirection = 0 - this.lastDirection = 0 - this.lastTemplateId = "" - this.lastTemplateType = null - this.consecutiveTemplateCount = 0 - } + const finalHp = Math.max(1, Math.round(baseHp)); + const finalAp = Math.max(1, Math.round(baseAp)); - // ---- 内部方法 ---- - - private selectTemplate( - tier: number, waveInTier: number, isBossWave: boolean - ): BlueprintTemplate { - let targetType: TemplateType - - if (isBossWave) { - targetType = TemplateType.BOSS - } else if (waveInTier === 1) { - targetType = TemplateType.REST - } else if (waveInTier === 3) { - targetType = TemplateType.MIXED - } else { - // Wave 2: 从 NORMAL/MIXED/ELITE 中随机 - const candidates = [TemplateType.NORMAL, TemplateType.MIXED, TemplateType.ELITE] - targetType = candidates[Math.floor(Math.random() * candidates.length)] - } - - // 同一模板被连续抽取 3 次:强制下一次从不同类型模板池中抽取 - if (this.consecutiveTemplateCount >= 3 && this.lastTemplateType !== null) { - if (targetType === this.lastTemplateType) { - const candidates = [TemplateType.REST, TemplateType.NORMAL, TemplateType.MIXED, TemplateType.ELITE, TemplateType.BOSS].filter(t => t !== this.lastTemplateType) - targetType = candidates[Math.floor(Math.random() * candidates.length)] - } - } - - let templates = BlueprintTemplates.filter(t => - t.type === targetType && t.tierMin <= tier && t.id !== "TUTORIAL" - ) - - if (templates.length === 0) { - templates = BlueprintTemplates.filter(t => - t.type === TemplateType.NORMAL && t.tierMin <= tier && t.id !== "TUTORIAL" - ) - } - - return this.pickRandomTemplate(templates) - } - - private pickRandomTemplate(templates: BlueprintTemplate[]): BlueprintTemplate { - const idx = Math.floor(Math.random() * templates.length) - const picked = templates[idx] - - if (picked.id === this.lastTemplateId) { - this.consecutiveTemplateCount++ - } else { - this.consecutiveTemplateCount = 1 - this.lastTemplateId = picked.id - this.lastTemplateType = picked.type - } - - return picked - } - - private spawnFromTemplate( - template: BlueprintTemplate, tier: number, budget: number - ): GeneratedMonster[] { - const monsters: GeneratedMonster[] = [] - let remainingBudget = budget - let spawnIndex = 0 - - for (const slot of template.slots) { - const count = this.randomInt(slot.countMin, slot.countMax) - - for (let i = 0; i < count; i++) { - // 从 typePool 中按权重随机选一个类型 - const type = slot.typePool[Math.floor(Math.random() * slot.typePool.length)] - const stats = MonsterStats[type] - const tierConfig = getTierConfig(tier) - - // 计算成本 - let cost = stats.cost - let affixes: AffixType[] = [] - - // 生成词缀 - if (template.allowAffix && tier >= 5) { - affixes = this.generateAffixes(type, tier, slot.forceAffix ?? false) - cost += affixes.reduce((sum, a) => sum + AffixConfigs[a].cost, 0) - } - - // 检查预算 - if (cost > remainingBudget && monsters.length > 0) continue - - remainingBudget -= cost - - // 计算最终属性 - const affixHpMul = 1.0 + affixes.reduce( - (sum, a) => sum + (AffixConfigs[a].hpMultiplier - 1.0), 0 - ) - const affixApMul = 1.0 + affixes.reduce( - (sum, a) => sum + (AffixConfigs[a].apMultiplier - 1.0), 0 - ) - - const finalHp = Math.max(1, Math.round( - stats.hp * tierConfig.multiplier * affixHpMul * this.adaptiveFactor - )) - const finalAp = Math.max(1, Math.round( - stats.ap * tierConfig.multiplier * affixApMul * this.adaptiveFactor - )) - - // 从 UUID 池中随机选一个 - const uuids = MonList[type] || MonList[MonType.Melee] - const uuid = uuids[Math.floor(Math.random() * uuids.length)] - - monsters.push({ - uuid, type, hp: finalHp, ap: finalAp, - affixes, isBoss: stats.isBoss, spawnIndex, - }) - spawnIndex++ - } - } - - // 预算利用率检查 (目标 >= 70%,但填充上限为模板数量的50%) - let totalCost = budget - remainingBudget - if (budget > 0 && totalCost / budget < 0.7 && template.id !== "TUTORIAL") { - const tierConfig = getTierConfig(tier) - const type = MonType.Melee - const stats = MonsterStats[type] - const maxFill = Math.max(5, Math.ceil(monsters.length * 0.8)) - let fillCount = 0 - while (remainingBudget >= stats.cost && fillCount < maxFill) { - const finalHp = Math.max(1, Math.round(stats.hp * tierConfig.multiplier * this.adaptiveFactor)) - const finalAp = Math.max(1, Math.round(stats.ap * tierConfig.multiplier * this.adaptiveFactor)) - const uuids = MonList[type] - const uuid = uuids[Math.floor(Math.random() * uuids.length)] - monsters.push({ - uuid, type, hp: finalHp, ap: finalAp, - affixes: [], isBoss: false, spawnIndex - }) - spawnIndex++ - remainingBudget -= stats.cost - totalCost += stats.cost - fillCount++ - } - } - - // 保底:至少 1 个 Melee - if (monsters.length === 0) { - const tierConfig = getTierConfig(tier) monsters.push({ - uuid: 6001, type: MonType.Melee, - hp: Math.max(1, Math.round(120 * tierConfig.multiplier * this.adaptiveFactor)), - ap: Math.max(1, Math.round(12 * tierConfig.multiplier * this.adaptiveFactor)), - affixes: [], isBoss: false, spawnIndex: 0, - }) + uuid, + type, + hp: finalHp, + ap: finalAp, + affixes: [], // 词缀系统已移除,传入空数组 + isBoss: isBoss, + spawnIndex: i, + }); } - return monsters + return monsters; } - private generateAffixes( - type: MonType, tier: number, forceAffix: boolean - ): AffixType[] { - const stats = MonsterStats[type] - const maxAffixes = stats.isBoss - ? (MAJOR_BOSS_TIERS.has(tier) ? AffixStackLimit.majorBoss : AffixStackLimit.miniBoss) - : AffixStackLimit.normal - - // 计算触发概率 - let chance = (BaseAffixChance[tier] ?? 0.5) * - (stats.isBoss ? (MAJOR_BOSS_TIERS.has(tier) ? AffixRoleMultiplier.majorBoss : AffixRoleMultiplier.miniBoss) : AffixRoleMultiplier.normal) - - if (forceAffix) chance = Math.max(chance, 0.5) - chance = Math.min(chance, 1.0) - - // 可用词缀列表(过滤 tierMin) - const available = (Object.values(AffixType) as AffixType[]).filter( - a => typeof a === "number" && AffixConfigs[a].tierMin <= tier - ) - - if (available.length === 0) return [] - - const result: AffixType[] = [] - - for (let i = 0; i < maxAffixes; i++) { - if (Math.random() >= chance) continue - - const candidates = available.filter(a => !result.includes(a)) - if (candidates.length === 0) continue - - result.push(candidates[Math.floor(Math.random() * candidates.length)]) - } - - // 互斥冲突时,按 AffixPriority 保留优先级更高的词缀 - for (const group of AffixMutualExclusion) { - const present = result.filter(a => group.includes(a)) - if (present.length > 1) { - present.sort((a, b) => AffixPriority.indexOf(a) - AffixPriority.indexOf(b)) - const toRemove = present.slice(1) - for (const r of toRemove) { - const idx = result.indexOf(r) - if (idx !== -1) result.splice(idx, 1) - } - } - } - - return result + reset(): void { + // 重置引擎状态 } - private randomInt(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min - } - - // ---- 向后兼容 ---- - - /** - * 向后兼容:获取指定波次的 IWaveSlot[](汇总后的类型+数量,不含最终属性) - * - * 将 generateWave() 的结果按怪物类型合并,返回简化的 [{type, count}] 格式。 - * 注意:此方法不返回 HP/AP 值,调用方需自行处理属性计算或改用 generateWave()。 - * - * @param waveNumber - 波次编号(1-15) - * @returns IWaveSlot[] 按怪物类型合并后的数组 - * - type: MonType 枚举值 - * - count: 该类型的怪物总数 - * - affixes: 该组怪物的词缀列表(仅首个同类型怪物的词缀) - * - * @example - * engine.getWaveSlotConfig(6) - * // [{ type: 8, count: 1 }, { type: 0, count: 10 }, { type: 2, count: 5 }] - * // = 1 个 MeleeBoss + 10 个 Melee + 5 个 Long + /** + * 导出一个空函数,用于外部获取/预测某个波次的怪物数量 + * @param waveNumber 目标波数 + * @returns 预计生成的怪物总数 */ + 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); + } + + // 向后兼容接口 getWaveSlotConfig(waveNumber: number): IWaveSlot[] { - const generated = this.generateWave(waveNumber) - const slotMap = new Map() + const generated = this.generateWave(waveNumber); + const slotMap = new Map(); for (const m of generated) { - const existing = slotMap.get(m.type) + const existing = slotMap.get(m.type); if (existing) { - existing.count++ + existing.count++; } else { - slotMap.set(m.type, { count: 1, affixes: m.affixes }) + slotMap.set(m.type, { count: 1, affixes: m.affixes }); } } @@ -1112,87 +252,52 @@ export class RogueSpawningEngine { type, count: data.count, ...(data.affixes.length > 0 ? { affixes: data.affixes } : {}), - })) + })); } } // ======================== 全局单例 & 向后兼容导出 ======================== -/** - * 全局刷怪引擎实例 - * 推荐直接使用:`spawningEngine.generateWave(n)` / `spawningEngine.updateAdaptive(...)` - * 新一局游戏开始时调用 `spawningEngine.reset()` - */ -export const spawningEngine = new RogueSpawningEngine() +export const spawningEngine = new RogueSpawningEngine(); -/** - * 向后兼容:动态生成波次配置 - * 每次调用都会重新随机生成,如需固定结果请缓存返回值或使用 spawningEngine.generateWave() - * - * @param waveNumber - 波次编号(1-30) - * @returns IWaveSlot[] 按 MonType 合并后的怪物数量配置 - */ -export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] { - return spawningEngine.getWaveSlotConfig(waveNumber) +export interface IWaveSlot { + type: number + count: number + affixes?: AffixType[] } -/** - * 向后兼容:默认占位配置(波次 > 15 或异常时的兜底配置) - * 20 个 Melee + 15 个 Long + 5 个 Support +/** + * 获取指定波次预计的怪物总数量 + * @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 }, -] +]; -/** - * 向后兼容:波次配置映射 - * 旧代码通过 `WaveSlotConfig[wave]` 访问时,Proxy 会拦截并调用引擎动态生成。 - * 注意:每次访问同一波次都会重新随机,结果可能不同。 - * 如需固定结果,请改用 `spawningEngine.generateWave()` 并缓存。 - * - * @example - * WaveSlotConfig[5] // 动态生成第 5 波的 IWaveSlot[] - * WaveSlotConfig[15] // 动态生成第 15 波的 IWaveSlot[] - */ 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 && wave <= 15) { - return spawningEngine.getWaveSlotConfig(wave) + const wave = parseInt(prop, 10); + if (!isNaN(wave) && wave >= 1) { + return spawningEngine.getWaveSlotConfig(wave); } - if (prop === "toJSON") return () => ({}) - return undefined + if (prop === "toJSON") return () => ({}); + return undefined; }, has(_target, prop: string) { - const wave = parseInt(prop, 10) - return !isNaN(wave) && wave >= 1 && wave <= 15 + const wave = parseInt(prop, 10); + return !isNaN(wave) && wave >= 1; }, } -) - -// ======================== 旧属性成长系统(向后兼容,新系统不再使用) ======================== - -/** @deprecated 新系统使用 TierConfigs 中的 tier_multiplier */ -export enum UpType { - AP1_HP1 = 0, - HP2 = 1, - AP2 = 2, -} - -/** @deprecated 新系统使用 RogueSpawningEngine */ -export const StageGrow = { - [UpType.AP1_HP1]: [4, 10] as [number, number], - [UpType.HP2]: [2, 20] as [number, number], - [UpType.AP2]: [8, 0] as [number, number], -} - -/** @deprecated 新系统使用 RogueSpawningEngine */ -export const StageBossGrow = { - [UpType.AP1_HP1]: [3, 16] as [number, number], - [UpType.HP2]: [1, 24] as [number, number], - [UpType.AP2]: [10, 4] as [number, number], -} +);