From 988c669bba8287631fd4eafc1689a9f0648b4c6f Mon Sep 17 00:00:00 2001 From: panFD Date: Tue, 4 Aug 2026 20:53:59 +0800 Subject: [PATCH] =?UTF-8?q?refactor(rogue):=20=E9=87=8D=E6=9E=84=E8=82=89?= =?UTF-8?q?=E9=B8=BD=E5=88=B7=E6=80=AA=E7=B3=BB=E7=BB=9F=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=8A=A8=E6=80=81=E9=9A=BE=E5=BA=A6=E4=B8=8E=E5=88=86?= =?UTF-8?q?=E6=89=B9=E5=88=B7=E6=80=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重构RogueConfig:替换旧强化系统为动态难度调节器,调整波次配置为20波通关,新增技能池与阵营适配 2. 优化刷怪逻辑:实现每波3批10秒间隔的分批刷怪,调整怪物上限为18只 3. 新增驻场技能支持:添加resolveFieldByLv处理等级化驻场技能,扩展FieldSkillHelper支持多阵营统计 4. 调整关卡判定:将最大波次从15改为20,新增英雄死亡计数与难度动态调节 5. 优化代码结构:移除废弃字段,统一技能注入方式,修复槽位复用逻辑 --- assets/script/game/hero/FieldSkillHelper.ts | 56 +- assets/script/game/hero/Mon.ts | 2 + assets/script/game/map/MissionComp.ts | 20 +- assets/script/game/map/MissionMonComp.ts | 138 +++- assets/script/game/map/RogueConfig.ts | 718 ++++++++++++-------- 5 files changed, 572 insertions(+), 362 deletions(-) diff --git a/assets/script/game/hero/FieldSkillHelper.ts b/assets/script/game/hero/FieldSkillHelper.ts index 766074c6..3e835403 100644 --- a/assets/script/game/hero/FieldSkillHelper.ts +++ b/assets/script/game/hero/FieldSkillHelper.ts @@ -15,13 +15,23 @@ import { EquipBoxComp } from "../map/EquipBoxComp"; export class FieldSkillHelper { /** 获取指定驻场技能类型的总加成值(计算存活的友方英雄 + 场上的驻场技能卡) */ public static getFieldSkillTotalValue(type: FieldSkillType): number { + return FieldSkillHelper.getFieldSkillTotalValueForFac(type, FacSet.HERO); + } + + /** + * 获取指定阵营的驻场技能总加成值 + * @param type 驻场技能类型 + * @param fac 阵营(FacSet.HERO / FacSet.MON),默认 HERO + * @returns 总加成值 + */ + public static getFieldSkillTotalValueForFac(type: FieldSkillType, fac: number = FacSet.HERO): number { let total = 0; - // 1. 统计英雄带来的驻场技能加成 + // 1. 统计英雄/怪物带来的驻场技能加成 // 读 model.runtime_field(已按当前等级 resolve 的 uuid 列表) ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => { const model = entity.get(HeroAttrsComp); - if (!model || model.is_dead || model.fac !== FacSet.HERO) return; + if (!model || model.is_dead || model.fac !== fac) return; const fields = model.runtime_field; if (fields) { for (const skillUuid of fields) { @@ -33,31 +43,33 @@ export class FieldSkillHelper { } }); - // 2. 统计技能盒子(技能卡)带来的驻场技能加成 - ecs.query(ecs.allOf(SkillBoxComp)).forEach((entity: ecs.Entity) => { - const skillBox = entity.get(SkillBoxComp); - if (!skillBox || !skillBox.field || skillBox.field.length === 0) return; + // 2. 统计技能盒子(技能卡)带来的驻场技能加成(仅英雄阵营) + if (fac === FacSet.HERO) { + ecs.query(ecs.allOf(SkillBoxComp)).forEach((entity: ecs.Entity) => { + const skillBox = entity.get(SkillBoxComp); + if (!skillBox || !skillBox.field || skillBox.field.length === 0) return; - for (const skillUuid of skillBox.field) { - const skillConfig = FieldSkillSet[skillUuid]; - if (skillConfig && skillConfig.type === type) { - total += skillConfig.value; + for (const skillUuid of skillBox.field) { + const skillConfig = FieldSkillSet[skillUuid]; + if (skillConfig && skillConfig.type === type) { + total += skillConfig.value; + } } - } - }); + }); - // 3. 统计装备盒(装备卡)带来的驻场技能加成 - ecs.query(ecs.allOf(EquipBoxComp)).forEach((entity: ecs.Entity) => { - const equipBox = entity.get(EquipBoxComp); - if (!equipBox || !equipBox.field || equipBox.field.length === 0) return; + // 3. 统计装备盒(装备卡)带来的驻场技能加成(仅英雄阵营) + ecs.query(ecs.allOf(EquipBoxComp)).forEach((entity: ecs.Entity) => { + const equipBox = entity.get(EquipBoxComp); + if (!equipBox || !equipBox.field || equipBox.field.length === 0) return; - for (const skillUuid of equipBox.field) { - const skillConfig = FieldSkillSet[skillUuid]; - if (skillConfig && skillConfig.type === type) { - total += skillConfig.value; + for (const skillUuid of equipBox.field) { + const skillConfig = FieldSkillSet[skillUuid]; + if (skillConfig && skillConfig.type === type) { + total += skillConfig.value; + } } - } - }); + }); + } return total; } diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index 1db2d030..38ebc243 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -225,6 +225,8 @@ export class Monster extends ecs.Entity { if (testSkills.dead !== undefined) model.dead = testSkills.dead; if (testSkills.fstart !== undefined) model.fstart = testSkills.fstart; if (testSkills.fend !== undefined) model.fend = testSkills.fend; + if (testSkills.call !== undefined) model.call = testSkills.call; + if (testSkills.revive !== undefined) model.revive = testSkills.revive; } // 按怪物等级 resolve 触发技能/复活到运行时缓存(怪物无 lv 成长,恒用 mon_lv) diff --git a/assets/script/game/map/MissionComp.ts b/assets/script/game/map/MissionComp.ts index fd9c23e6..9b345ba4 100644 --- a/assets/script/game/map/MissionComp.ts +++ b/assets/script/game/map/MissionComp.ts @@ -48,7 +48,7 @@ import { Tooltip } from "../skill/Tooltip"; import { Timer } from "db://oops-framework/core/common/timer/Timer"; import { FieldSkillType } from "../common/config/SkillSet"; import { FieldSkillHelper } from "../hero/FieldSkillHelper"; -import { spawningEngine } from "./RogueConfig"; +import { spawningEngine, MAX_WAVE, DynamicTuner } from "./RogueConfig"; const { ccclass, property } = _decorator; /** 任务(关卡)生命周期阶段 */ @@ -524,20 +524,28 @@ export class MissionComp extends CCComp { let allAlive = true; let hasHero = false; + let heroDeathCount = 0; ecs.query(this.heroAttrsMatcher).forEach(entity => { const attrs = entity.get(HeroAttrsComp); if (attrs && attrs.fac === FacSet.HERO) { hasHero = true; - if (attrs.is_dead) allAlive = false; + if (attrs.is_dead) { + allAlive = false; + heroDeathCount++; + } } }); + + // 【动态难度调节】根据本波战况自动放水 / 加压 + DynamicTuner.adjust(this.clearTime, heroDeathCount); + mLogger.log(this.debugMode, 'MissionComp', `[DynamicTuner] wave=${this.currentWave} clearTime=${this.clearTime.toFixed(1)}s deaths=${heroDeathCount} factor=${DynamicTuner.factor.toFixed(2)}`); // 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分) if (hasHero && allAlive) { smc.vmdata.scores.wave_all_alive_count++; } - // 【评分系统 - 战绩分】判断是否通过最后一关(第15回合) - if (this.currentWave === 15) { + // 【评分系统 - 战绩分】判断是否通过最后一关(第20回合) + if (this.currentWave === MAX_WAVE) { smc.vmdata.scores.passed_wave_20 = true; } } @@ -889,8 +897,8 @@ 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) { - if (this.currentWave >= 15) { - // 15 波通关 + if (this.currentWave >= MAX_WAVE) { + // 20 波通关 this.open_Victory(null, false); } else { oops.message.dispatchEvent("TimeUpAdvanceWave"); diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index f4972cc1..80a2dff0 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -4,7 +4,8 @@ * * 职责: * 1. 管理每一波怪物的生成计划:根据 RogueConfig 生成怪物。 - * 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)把怪物转入逐个刷出队列。 + * 2. 自动推进波次:在准备阶段结束时(PhasePrepareEnd)启动分批释放。 + * 3. 分批刷怪:每波固定 3 批,每 10 秒释放一批,批内按 MON_SPAWN_INTERVAL 逐个刷出。 * * 关键设计: * - 所有怪物统一从右侧 X=400 出生点逐个刷出(MON_SPAWN_INTERVAL 节奏控制)。 @@ -21,7 +22,7 @@ import { Monster } from "../hero/Mon"; import { smc } from "../common/SingletonModuleComp"; import { GameEvent } from "../common/config/GameEvent"; import { BoxSet, FacSet } from "../common/config/GameSet"; -import { spawningEngine, GeneratedMonster, TestModeConfig } from "./RogueConfig"; +import { spawningEngine, GeneratedMonster, TestModeConfig, MAX_MONSTERS, BATCH_COUNT, BATCH_INTERVAL } from "./RogueConfig"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { MonMoveComp } from "../hero/MonMoveComp"; @@ -32,8 +33,6 @@ const { ccclass, property } = _decorator; export class MissionMonCompComp extends CCComp { // ======================== 常量 ======================== - /** 怪物最多 12 个 */ - private static readonly MAX_MONSTERS = 12; /** 怪物出生掉落高度 */ private static readonly MON_DROP_HEIGHT = 0; /** 怪物统一从右侧 X=400 出生点逐个刷出,随后向左推进 */ @@ -42,9 +41,10 @@ export class MissionMonCompComp extends CCComp { private static readonly MON_SPAWN_INTERVAL = 0.3; /** - * 12 个怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标, + * 怪物占位点:所有槽位统一以右侧出生点 (X=400) 作为坐标, * 实际阵型由 MonMoveComp 在战斗中向左推进时自然拉开。 * 槽位索引仍保留 3 行 × 4 列结构,供 monGrid 寻路与 SCastSystem 索敌使用。 + * 超出 12 个槽位的怪物(放松波)按 spawnIndex % 12 复用槽位。 */ public static readonly MON_POSITIONS: Vec3[] = [ v3(MissionMonCompComp.MON_SPAWN_X, BoxSet.GAME_LINE, 0), // index 0: Col1-Top @@ -65,7 +65,7 @@ export class MissionMonCompComp extends CCComp { @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; - + // ======================== 运行时状态 ======================== /** 全局生成顺序计数器(用于渲染层级排序) */ @@ -76,9 +76,15 @@ export class MissionMonCompComp extends CCComp { private waveTargetCount: number = 0; /** 当前波已生成的怪物数量 */ private waveSpawnedCount: number = 0; - /** 等待生成的怪物队列(波次总池) */ - private pendingMonsters: GeneratedMonster[] = []; - /** 逐个刷怪队列:从 pendingMonsters 转入,按节奏在 update 中释放 */ + /** 等待生成的怪物队列(按批次分组,batch 0~2) */ + private pendingBatches: GeneratedMonster[][] = [[], [], []]; + /** 当前正在释放的批次索引 */ + private currentBatch: number = 0; + /** 批次释放计时器(秒) */ + private batchTimer: number = 0; + /** 是否正在分批释放中 */ + private isReleasing: boolean = false; + /** 逐个刷怪队列:从当前批次转入,按节奏在 update 中释放 */ private spawnQueue: GeneratedMonster[] = []; /** 逐个刷怪累计计时(秒) */ private spawnTimer: number = 0; @@ -92,8 +98,21 @@ export class MissionMonCompComp extends CCComp { } protected update(dt: number): void { - // pending_mon_num 同时统计未刷出的总池与正在释放的队列,确保 UI 与全灭判定一致 - smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length + this.spawnQueue.length; + // 统计待刷出的怪物总数(未释放的批次 + 正在释放的队列) + let pendingCount = this.spawnQueue.length; + for (let i = this.currentBatch; i < BATCH_COUNT; i++) { + pendingCount += this.pendingBatches[i].length; + } + smc.vmdata.mission_data.pending_mon_num = pendingCount; + + // 分批释放:按 BATCH_INTERVAL 节奏推进批次 + if (this.isReleasing) { + this.batchTimer += dt; + if (this.batchTimer >= BATCH_INTERVAL && this.currentBatch < BATCH_COUNT - 1) { + this.batchTimer = 0; + this.advanceBatch(); + } + } // 逐个刷怪:按 MON_SPAWN_INTERVAL 节奏从队列释放 if (this.spawnQueue.length > 0) { @@ -101,7 +120,7 @@ export class MissionMonCompComp extends CCComp { if (this.spawnTimer >= MissionMonCompComp.MON_SPAWN_INTERVAL) { this.spawnTimer = 0; const monData = this.spawnQueue.shift()!; - const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MAX_MONSTERS; + const targetPosIndex = this.waveSpawnedCount % MissionMonCompComp.MON_POSITIONS.length; this.addMonsterAtGrid(targetPosIndex, monData, this.currentWave); this.waveSpawnedCount++; } @@ -111,10 +130,16 @@ export class MissionMonCompComp extends CCComp { start() {} private setupWaveData(monsters: GeneratedMonster[]) { - this.pendingMonsters = monsters.slice(0, MissionMonCompComp.MAX_MONSTERS); - smc.vmdata.mission_data.pending_mon_num = this.pendingMonsters.length; - this.waveTargetCount = this.pendingMonsters.length; - + // 按批次分组 + this.pendingBatches = [[], [], []]; + for (const m of monsters) { + const batch = Math.min(m.batch, BATCH_COUNT - 1); + this.pendingBatches[batch].push(m); + } + + this.waveTargetCount = monsters.length; + smc.vmdata.mission_data.pending_mon_num = this.waveTargetCount; + let hasBoss = monsters.some(m => m.isBoss); mLogger.log(this.debugMode, 'MissionMonComp', `[MissionMonComp] 波次 ${this.currentWave} 生成怪物总数: ${this.waveTargetCount}`); @@ -136,10 +161,13 @@ export class MissionMonCompComp extends CCComp { this.currentWave = 1; this.waveTargetCount = 0; this.waveSpawnedCount = 0; - this.pendingMonsters = []; + this.pendingBatches = [[], [], []]; + this.currentBatch = 0; + this.batchTimer = 0; + this.isReleasing = false; this.spawnQueue = []; this.spawnTimer = 0; - + // 预生成第一波数据以获取数量和 Boss 信息 const monsters = spawningEngine.generateWave(this.currentWave); this.setupWaveData(monsters); @@ -159,7 +187,7 @@ export class MissionMonCompComp extends CCComp { private onTimeUpAdvanceWave() { this.currentWave += 1; smc.vmdata.mission_data.level = this.currentWave; - + const monsters = spawningEngine.generateWave(this.currentWave); this.setupWaveData(monsters); } @@ -167,17 +195,53 @@ export class MissionMonCompComp extends CCComp { private onPhasePrepareEnd() { this.resetSlotSpawnData(); - // 准备结束阶段:把本波待刷怪物转入逐个释放队列, - // 实际生成时机由 update() 按 MON_SPAWN_INTERVAL 节奏触发, - // 让怪物从 X=400 出生点排成纵队依次向左推进。 - if (this.pendingMonsters.length > 0) { - const count = Math.min(this.pendingMonsters.length, MissionMonCompComp.MAX_MONSTERS); - for (let i = 0; i < count; i++) { - this.spawnQueue.push(this.pendingMonsters.shift()!); - } - // 让首个怪物在下一帧立即刷出,避免额外延迟 - this.spawnTimer = MissionMonCompComp.MON_SPAWN_INTERVAL; + // 准备结束阶段:启动分批释放, + // 第一批立即转入 spawnQueue,后续批次由 update 按 BATCH_INTERVAL 推进。 + this.startBatchRelease(); + } + + // ======================== 分批释放 ======================== + + /** 启动分批释放:立即释放第一批,启动批次计时器 */ + private startBatchRelease() { + this.currentBatch = 0; + this.batchTimer = 0; + this.isReleasing = true; + this.releaseCurrentBatch(); + } + + /** 推进到下一批次 */ + private advanceBatch() { + if (this.currentBatch >= BATCH_COUNT - 1) { + this.isReleasing = false; + return; } + this.currentBatch++; + this.releaseCurrentBatch(); + } + + /** 将当前批次的怪物转入 spawnQueue,等待逐个刷出 */ + private releaseCurrentBatch() { + const batch = this.pendingBatches[this.currentBatch]; + if (batch.length === 0) { + // 当前批次为空,尝试推进到下一批 + if (this.currentBatch < BATCH_COUNT - 1) { + this.currentBatch++; + this.releaseCurrentBatch(); + } else { + this.isReleasing = false; + } + return; + } + + // 将批次怪物转入逐个刷怪队列 + for (const m of batch) { + this.spawnQueue.push(m); + } + batch.length = 0; + + // 让首个怪物在下一帧立即刷出,避免额外延迟 + this.spawnTimer = MissionMonCompComp.MON_SPAWN_INTERVAL; } // ======================== 槽位管理 ======================== @@ -197,6 +261,9 @@ export class MissionMonCompComp extends CCComp { // 同步丢弃上一波未释放的队列,避免与新一波混合 this.spawnQueue = []; this.spawnTimer = 0; + this.isReleasing = false; + this.currentBatch = 0; + this.batchTimer = 0; } // ======================== 怪物生成 ======================== @@ -211,19 +278,20 @@ 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; - - if (monData.testSkills) { - (mon as any)._testSkills = monData.testSkills; + this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999; + + // 技能套装注入:通过 _testSkills 通道传递给 Mon.load() + if (monData.skills) { + (mon as any)._testSkills = monData.skills; } mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, posIndex); - + const move = mon.get(MonMoveComp); if (move) { move.spawnOrder = this.globalSpawnOrder; diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index 905fac97..ad249062 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -1,68 +1,132 @@ /** * @file RogueConfig.ts - * @description 肉鸽刷怪系统(基于硬编码规则 + 模块化随机) + * @description 肉鸽刷怪系统(基于英雄强度的动态难度 + 心流循环) * * 设计层次: - * 1. MonsterElite - 个体强化(5 种) - * 2. WaveEnchant - 波次级强化(环境修饰) - * 3. SquadConfig - 小队模板(3-4 只怪的组合单元) - * 4. WaveConfig - 每波硬编码(基础数 + 小队池 + 强化池) - * 5. RogueSpawningEngine - 生成引擎(按规则组合上述配置) + * 1. WaveType - 波型(普通 / 压力 / 放松),5 波一个心流循环 + * 2. WaveConfig - 每波硬编码(数量 + 小队池 + HP/AP 倍率 + 强度微调) + * 3. DynamicTuner - 动态难度调节器(系统根据战况放水 / 加压) + * 4. MonSkillSet - 怪物技能池(atking / atked / dead 等全触发类型) + * 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度) * - * 详细设计见 docs/superpowers/specs/2026-07-05-rogue-config-refactor-design.md + * 核心公式: + * heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄) + * targetPower = heroPower × 波型系数 × wave.power_adjust × DynamicTuner.factor + * scale = targetPower ÷ Σ 怪物基础强度 + * 每只怪: hp ×= scale, ap ×= scale + * + * 波次节奏: + * - 最大 20 波,第 20 波通关 + * - 每波 30 秒,固定分 3 批,每 10 秒释放一批 + * - wave % 5 === 0 → 压力波(必带 Boss,强度高、数量少) + * - wave % 5 === 1 → 放松波(数量 × 1.5,强度低,爽快清屏) */ -import { HeroInfo, MonType, MonTypeName } from "../common/config/heroSet"; +import { HeroInfo, MonType, MonTypeName, calcHeroPower, TriggerGrouped, LvReviveEntry, heroInfo } from "../common/config/heroSet"; +import { SkillOverrides } from "../common/config/SkillSet"; import { FacSet } from "../common/config/GameSet"; +import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; +import { HeroAttrsComp } from "../hero/HeroAttrsComp"; -// ======================== 1. 枚举与基础类型 ======================== +// ======================== 1. 波型与节奏常量 ======================== -/** - * 个体强化类型(精选 5 种) - * 设计权衡:HeroAttrsComp 暂无 atk_cd / regen 字段,因此 Swift/Regen 简化为数值差异化。 - * 待 HeroAttrsComp 升级后可启用 cd_mul / regen 扩展点。 - */ -export enum MonsterElite { - Elite = 0, // 精英:全面强化 - Berserk = 1, // 狂暴:高攻低血 - Shield = 2, // 护盾:坦克 - Regen = 3, // 再生:高血偏弱攻 - Swift = 4, // 疾风:平衡偏输出 +/** 波型枚举 */ +export enum WaveType { + Normal = 0, // 普通波 + Pressure = 1, // 压力波(wave % 5 === 0,必带 Boss) + Relax = 2, // 放松波(wave % 5 === 1,量大强度低) } -/** 个体强化效果定义 */ -export const MonsterEliteSet: Record = { - [MonsterElite.Elite]: { name: "精英", hp_mul: 1.5, ap_mul: 1.30 }, - [MonsterElite.Berserk]: { name: "狂暴", hp_mul: 0.9, ap_mul: 1.60 }, - [MonsterElite.Shield]: { name: "护盾", hp_mul: 1.8, ap_mul: 0.80 }, - [MonsterElite.Regen]: { name: "再生", hp_mul: 1.3, ap_mul: 0.95 }, - [MonsterElite.Swift]: { name: "疾风", hp_mul: 1.0, ap_mul: 1.15 }, +/** 波型名称 */ +export const WaveTypeName: Record = { + [WaveType.Normal]: "普通", + [WaveType.Pressure]: "压力", + [WaveType.Relax]: "放松", }; +/** 波型强度系数(硬编码) */ +export const WAVE_TYPE_POWER_RATIO: Record = { + [WaveType.Normal]: 0.9, // 普通波:标准强度 + [WaveType.Pressure]: 1.2, // 压力波:强度高、数量少 + [WaveType.Relax]: 0.6, // 放松波:量大、强度低 +}; + +/** 放松波数量倍率(相对普通波) */ +export const RELAX_COUNT_MUL = 1.5; + +/** 最大波次(第 20 波通关) */ +export const MAX_WAVE = 20; + +/** 每波时长(秒) */ +export const WAVE_DURATION = 30; + +/** 每波分批次数 */ +export const BATCH_COUNT = 3; + +/** 每批间隔(秒):30 秒 / 3 批 = 10 秒 */ +export const BATCH_INTERVAL = WAVE_DURATION / BATCH_COUNT; + +/** 每波怪物硬上限(放松波 12 × 1.5 = 18) */ +export const MAX_MONSTERS = 18; + /** - * 旧版词缀类型枚举(已废弃) - * @deprecated 已被 MonsterElite 取代,仅保留导出避免破坏外部引用。 - * 新代码请使用 MonsterElite。 + * 获取指定波次的波型 + * @param wave 波次(1 起) + * @returns WaveType */ -export enum AffixType { - Elite = 0, - Berserk = 1, - Shield = 2, - Regen = 3, - Swift = 4, - Giant = 5, - Chain = 6, - SummonerA = 7, - CritRes = 8, - FreezeRes = 9, - KnockbackRes = 10, +export function getWaveType(wave: number): WaveType { + if (wave % 5 === 0) return WaveType.Pressure; + if (wave % 5 === 1) return WaveType.Relax; + return WaveType.Normal; } -// ======================== 2. 怪物 UUID 池 ======================== +// ======================== 2. 动态难度调节器 ======================== + +/** + * 动态难度调节器(系统控制放水 / 加压) + * 与硬编码系数并存,用于根据战况实时微调难度。 + * + * 用法示例(MissionComp 每波结束时调用): + * DynamicTuner.adjust(clearTime, heroDeathCount); + * + * 调节规则(内部硬编码): + * - 清场时间 < 20s 且无英雄死亡 → factor += 0.05(加压) + * - 清场时间 > 28s 或有英雄死亡 → factor -= 0.05(放水) + * - factor 范围钳制 [0.5, 2.0] + */ +export const DynamicTuner = { + /** 当前难度系数(默认 1.0,>1 加压,<1 放水) */ + factor: 1.0, + + /** 系数下限(最多放水到 50%) */ + MIN_FACTOR: 0.5, + /** 系数上限(最多加压到 200%) */ + MAX_FACTOR: 2.0, + /** 单次调节步长 */ + STEP: 0.05, + + /** + * 根据上一波战况自动调节难度 + * @param clearTime 清场耗时(秒) + * @param heroDeathCount 英雄死亡数 + */ + adjust(clearTime: number, heroDeathCount: number): void { + if (heroDeathCount > 0 || clearTime > WAVE_DURATION * 0.95) { + // 有英雄死亡或清场过慢 → 放水 + this.factor = Math.max(this.MIN_FACTOR, this.factor - this.STEP); + } else if (clearTime < WAVE_DURATION * 0.65 && heroDeathCount === 0) { + // 清场过快且无死亡 → 加压 + this.factor = Math.min(this.MAX_FACTOR, this.factor + this.STEP); + } + }, + + /** 重置调节器(每局开始时调用) */ + reset(): void { + this.factor = 1.0; + }, +}; + +// ======================== 3. 怪物 UUID 池 ======================== /** 按 MonType 分组的怪物 uuid 池,动态从 HeroInfo 提取,避免硬编码 */ export const MonList: Record = {}; @@ -76,16 +140,12 @@ for (const key in HeroInfo) { } } -// ======================== 2.5 怪物金币掉落配置 ======================== +// ======================== 3.5 怪物金币掉落配置 ======================== /** * 怪物金币掉落配置(按 MonType 分类) * - base: 普通怪物死亡时掉落的固定金币数 * - boss: Boss 怪物死亡时掉落的固定金币数(仅对 Boss 类型生效) - * - * 设计说明: - * 金币不再按波次固定发放,改为每只怪物死亡时掉落。 - * Boss 提供高额固定金币奖励,作为战斗收益的核心来源。 */ export const MonsterGoldSet: Record = { [MonType.Melee]: { base: 1, boss: 0 }, @@ -110,32 +170,6 @@ export function getMonsterGoldDrop(monType: number, isBoss: boolean): number { return Math.max(0, Math.floor(isBoss ? cfg.boss : cfg.base)); } -// ======================== 3. 波次级强化库 ======================== - -/** - * 波次级强化(环境修饰符) - * 作用于整波怪物,与 MonsterElite(个体)解耦。 - */ -export interface WaveEnchant { - id: string; - name: string; - desc: string; - hp_mul?: number; // 全员 HP 乘子 - ap_mul?: number; // 全员 AP 乘子 - cd_mul?: number; // 全员攻击间隔乘子(后续扩展点) - elite_rate_mul?: number; // 该波个体强化出现概率乘子 - weight: number; // 在强化池中的抽取权重 -} - -/** 波次强化库(硬编码) */ -export const WaveEnchantLibrary: Record = { - frenzy: { id: "frenzy", name: "狂热浪潮", desc: "全员攻击力+25%", ap_mul: 1.25, weight: 10 }, - ironhide: { id: "ironhide", name: "铁皮大军", desc: "全员生命+40%", hp_mul: 1.40, weight: 10 }, - swift: { id: "swift", name: "疾风突袭", desc: "全员攻速+20%", cd_mul: 0.80, weight: 8 }, - nightmare: { id: "nightmare", name: "梦魇来袭", desc: "个体强化率×2,HP+15%", elite_rate_mul: 2.0, hp_mul: 1.15, weight: 5 }, - fortress: { id: "fortress", name: "钢铁堡垒", desc: "全员 HP+60% AP-15%", hp_mul: 1.60, ap_mul: 0.85, weight: 6 }, -}; - // ======================== 4. 小队模板库 ======================== /** 小队内单种怪物的槽位定义 */ @@ -162,90 +196,183 @@ export const SquadLibrary: Record = { summoner_cult: { id: "summoner_cult", name: "召唤教派组", weight: 4, slots: [{ type: MonType.Summoner, count: 1 }, { type: MonType.Long, count: 2 }] }, }; -// ======================== 5. 波次配置表 ======================== +// ======================== 5. 怪物技能池 ======================== + +/** + * 怪物技能套装(一套完整的触发技能配置) + * 覆盖触发时机:call / atking / atked / dead / fstart / fend / 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; + /** 战斗结束触发 */ + fend?: TriggerGrouped; + /** 复活 */ + revive?: LvReviveEntry[]; + /** 在技能池中的抽取权重 */ + weight: number; +} + +/** + * 怪物技能池(硬编码) + * 压力波 / Boss 可配置专属技能,普通波随机挂载增加变数。 + * + * 技能 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 专属技能池(压力波 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 } }], + }, + }, +}; + +// ======================== 6. 波次配置表 ======================== /** 单波次完整配置 */ export interface WaveConfig { - /** 基础怪物数(1~12) */ + /** 基础怪物总数(普通波 12 为上限,放松波自动 × 1.5) */ base_count: number; /** 可选小队 id 池,引擎按权重抽取拼装到 base_count */ squad_pool: string[]; - /** 可选波次强化 id 池,按权重抽 0~2 个(可空表示无强化) */ - enchant_pool?: string[]; - /** 是否 Boss 波(首位放 Boss) */ + /** HP 强化倍率(硬编码,逐波递进) */ + hp_mul: number; + /** AP 强化倍率(硬编码,逐波递进) */ + ap_mul: number; + /** 强度微调(放水 / 加压,默认 1.0) */ + power_adjust?: number; + /** 是否 Boss 波(压力波必为 true) */ boss_wave?: boolean; - /** 该波个体强化基础概率(默认按波次递增) */ - elite_base_rate?: number; + /** 普通怪技能池 id(可选,随机挂载) */ + skill_pool?: string[]; + /** Boss 技能池 id(Boss 波专用,随机挂载) */ + boss_skill_pool?: string[]; } /** - * 波次配置表(硬编码 wave 1~30) - * 节奏曲线:教学期(1-4) → 第一Boss(5) → 引入强化(6-9) → 第二Boss(10) → - * 组合多样化(11-14) → 第三Boss(15) → 高压阶段(16-24) → - * 第四Boss(25) → 终极阶段(26-29) → 最终Boss(30) + * 波次配置表(硬编码 wave 1~20) + * + * 心流循环(5 波一循环): + * wave % 5 === 0 → 压力波(必带 Boss,强度高、数量少) + * wave % 5 === 1 → 放松波(数量 × 1.5,强度低) + * 其余 → 普通波(标准强度) + * + * 强度递进:hp_mul / ap_mul 每 5 波一档,压力波额外提升。 */ export const WaveConfigs: Record = { - // ===== 教学期 ===== - 1: { base_count: 3, squad_pool: ["melee_grunt"] }, - 2: { base_count: 4, squad_pool: ["melee_grunt", "mixed_balanced"] }, - 3: { base_count: 5, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"] }, - 4: { base_count: 6, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"] }, - // ===== 第一 Boss ===== - 5: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], boss_wave: true }, - // ===== 引入波次强化 ===== - 6: { base_count: 7, squad_pool: ["assassin_squad", "long_line", "mixed_balanced"], enchant_pool: ["frenzy"], elite_base_rate: 0.10 }, - 7: { base_count: 8, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.12 }, - 8: { base_count: 8, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.14 }, - 9: { base_count: 9, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.16 }, - // ===== 第二 Boss ===== - 10: { base_count: 10, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.18 }, - // ===== 组合多样化 ===== - 11: { base_count: 10, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], enchant_pool: ["swift", "frenzy"], elite_base_rate: 0.18 }, - 12: { base_count: 10, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], enchant_pool: ["ironhide"], elite_base_rate: 0.20 }, - 13: { base_count: 11, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.22 }, - 14: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], enchant_pool: ["ironhide", "swift"], elite_base_rate: 0.24 }, - // ===== 第三 Boss(中期高潮) ===== - 15: { base_count: 11, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.25 }, - // ===== 高压阶段 ===== - 16: { base_count: 11, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "swift"], elite_base_rate: 0.26 }, - 17: { base_count: 11, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], enchant_pool: ["ironhide", "frenzy"], elite_base_rate: 0.27 }, - 18: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], enchant_pool: ["nightmare", "swift"], elite_base_rate: 0.28 }, - 19: { base_count: 12, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], enchant_pool: ["fortress"], elite_base_rate: 0.28 }, - 20: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], enchant_pool: ["frenzy", "ironhide"], boss_wave: true, elite_base_rate: 0.30 }, - 21: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "summoner_cult"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.30 }, - 22: { base_count: 12, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.30 }, - 23: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "melee_grunt"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.32 }, - 24: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "long_line"], enchant_pool: ["fortress", "swift"], elite_base_rate: 0.32 }, - // ===== 第四 Boss ===== - 25: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "heavy_shield"], enchant_pool: ["nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.35 }, - // ===== 终极阶段 ===== - 26: { base_count: 12, squad_pool: ["melee_grunt", "assassin_squad", "long_line", "summoner_cult"], enchant_pool: ["frenzy", "nightmare"], elite_base_rate: 0.35 }, - 27: { base_count: 12, squad_pool: ["heavy_shield", "mixed_balanced", "summoner_cult"], enchant_pool: ["ironhide", "fortress"], elite_base_rate: 0.38 }, - 28: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "melee_grunt"], enchant_pool: ["swift", "nightmare"], elite_base_rate: 0.40 }, - 29: { base_count: 12, squad_pool: ["mixed_balanced", "heavy_shield", "summoner_cult"], enchant_pool: ["frenzy", "ironhide", "fortress"], elite_base_rate: 0.42 }, - // ===== 最终 Boss ===== - 30: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], enchant_pool: ["frenzy", "ironhide", "swift", "nightmare", "fortress"], boss_wave: true, elite_base_rate: 0.45 }, + // ===== 第一循环:教学期 ===== + 1: { base_count: 6, squad_pool: ["melee_grunt"], hp_mul: 1.00, ap_mul: 1.00 }, + 2: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced"], hp_mul: 1.00, ap_mul: 1.00 }, + 3: { base_count: 8, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.05, ap_mul: 1.00 }, + 4: { base_count: 9, squad_pool: ["mixed_balanced", "long_line", "heavy_shield"], hp_mul: 1.10, ap_mul: 1.05 }, + // 压力波:第一 Boss + 5: { base_count: 7, squad_pool: ["melee_grunt", "mixed_balanced", "heavy_shield"], hp_mul: 1.15, ap_mul: 1.10, boss_wave: true, boss_skill_pool: ["boss_rage"] }, + + // ===== 第二循环:引入技能怪 ===== + // 放松波:量大好清 + 6: { base_count: 10, squad_pool: ["melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.10, ap_mul: 1.05 }, + 7: { base_count: 10, squad_pool: ["melee_grunt", "heavy_shield", "long_line"], hp_mul: 1.15, ap_mul: 1.10, skill_pool: ["tough"] }, + 8: { base_count: 11, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.20, ap_mul: 1.15, skill_pool: ["berserk"] }, + 9: { base_count: 11, squad_pool: ["heavy_shield", "long_line", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["berserk", "tough"] }, + // 压力波:第二 Boss + 10: { base_count: 8, squad_pool: ["melee_grunt", "assassin_squad", "mixed_balanced"], hp_mul: 1.30, ap_mul: 1.25, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron"] }, + + // ===== 第三循环:组合多样化 ===== + // 放松波 + 11: { base_count: 11, squad_pool: ["assassin_squad", "long_line", "summoner_cult", "mixed_balanced"], hp_mul: 1.25, ap_mul: 1.20, skill_pool: ["leech"] }, + 12: { base_count: 11, squad_pool: ["heavy_shield", "melee_grunt", "mixed_balanced", "long_line"], hp_mul: 1.30, ap_mul: 1.25, skill_pool: ["tough", "warcry"] }, + 13: { base_count: 12, squad_pool: ["assassin_squad", "summoner_cult", "mixed_balanced"], hp_mul: 1.35, ap_mul: 1.30, skill_pool: ["berserk", "leech"] }, + 14: { base_count: 12, squad_pool: ["heavy_shield", "long_line", "melee_grunt"], hp_mul: 1.40, ap_mul: 1.35, skill_pool: ["berserk", "tough", "legacy"] }, + // 压力波:第三 Boss(中期高潮) + 15: { base_count: 9, squad_pool: ["assassin_squad", "mixed_balanced", "summoner_cult"], hp_mul: 1.50, ap_mul: 1.40, boss_wave: true, boss_skill_pool: ["boss_rage", "boss_iron", "boss_doom"] }, + + // ===== 第四循环:终极阶段 ===== + // 放松波 + 16: { base_count: 12, squad_pool: ["assassin_squad", "heavy_shield", "long_line"], hp_mul: 1.45, ap_mul: 1.40, skill_pool: ["leech", "warcry"] }, + 17: { base_count: 12, squad_pool: ["melee_grunt", "summoner_cult", "mixed_balanced"], hp_mul: 1.50, ap_mul: 1.45, skill_pool: ["berserk", "legacy"] }, + 18: { base_count: 12, squad_pool: ["assassin_squad", "long_line", "heavy_shield"], hp_mul: 1.55, ap_mul: 1.50, skill_pool: ["berserk", "tough", "leech"] }, + 19: { base_count: 12, squad_pool: ["mixed_balanced", "summoner_cult", "melee_grunt"], hp_mul: 1.60, ap_mul: 1.55, skill_pool: ["berserk", "tough", "legacy", "warcry"] }, + // 压力波:最终 Boss + 20: { base_count: 10, squad_pool: ["assassin_squad", "heavy_shield", "summoner_cult", "mixed_balanced"], hp_mul: 1.80, ap_mul: 1.70, boss_wave: true, boss_skill_pool: ["boss_doom", "boss_rage"] }, }; -// ======================== 6. 全局难度缩放 ======================== - -/** - * 根据波次返回全局 HP/AP 乘子,每 5 波递进一档 - * @param wave 当前波次 - * @returns hp_mul / ap_mul - */ -export function getGlobalScale(wave: number): { hp_mul: number; ap_mul: number } { - const step = Math.floor((wave - 1) / 5); - return { - hp_mul: 1 + 0.15 * step, // wave 1=1.0, 6=1.15, 11=1.30, 16=1.45, 21=1.60, 26=1.75 - ap_mul: 1 + 0.08 * step, // wave 1=1.0, 6=1.08, 11=1.16, 16=1.24, 21=1.32, 26=1.40 - }; -} - // ======================== 7. 配置校验 ======================== /** - * 校验 WaveConfigs / SquadLibrary / WaveEnchantLibrary 引用一致性 + * 校验 WaveConfigs / SquadLibrary / MonSkillPool 引用一致性 * 建议在游戏启动时调用一次,便于发现配置错误 * @returns 错误信息数组,空数组表示校验通过 */ @@ -260,15 +387,22 @@ export function validateRogueConfig(): string[] { errors.push(`Wave ${wave} 引用了不存在的小队: ${squadId}`); } } - if (cfg.enchant_pool) { - for (const enchId of cfg.enchant_pool) { - if (!WaveEnchantLibrary[enchId]) { - errors.push(`Wave ${wave} 引用了不存在的强化: ${enchId}`); + if (cfg.skill_pool) { + for (const skillId of cfg.skill_pool) { + if (!MonSkillPool[skillId]) { + errors.push(`Wave ${wave} 引用了不存在的技能: ${skillId}`); } } } - if (cfg.base_count < 1 || cfg.base_count > 12) { - errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~12)`); + if (cfg.boss_skill_pool) { + for (const skillId of cfg.boss_skill_pool) { + if (!BossSkillPool[skillId]) { + errors.push(`Wave ${wave} 引用了不存在的 Boss 技能: ${skillId}`); + } + } + } + if (cfg.base_count < 1 || cfg.base_count > MAX_MONSTERS) { + errors.push(`Wave ${wave} base_count=${cfg.base_count} 越界 (1~${MAX_MONSTERS})`); } } @@ -295,23 +429,22 @@ export interface GeneratedMonster { ap: number; isBoss: boolean; spawnIndex: number; + /** 本怪所属批次(0~2,由 MissionMonComp 按 BATCH_INTERVAL 释放) */ + batch: number; - /** @deprecated 已被 elite 取代,始终返回 [],仅向后兼容 */ - affixes: AffixType[]; - - /** 个体强化(无则 undefined) */ - elite?: MonsterElite; - /** 该波触发的波次强化 id 列表 */ - wave_enchants: string[]; - - /** 测试模式专用技能覆盖(按 s_uuid 分组 + lv 数组,与 heroInfo 结构一致) */ - testSkills?: { - skill?: { s_uuid: number; cd?: number; overrides?: any }; - atking?: Record; - atked?: Record; - dead?: Record; - fstart?: Record; - fend?: Record; + /** + * 怪物技能套装(覆盖全部触发时机) + * 注入方式与 _testSkills 相同,在 Mon.load() 中写入 HeroAttrsComp + */ + skills?: { + skill?: { s_uuid: number; cd?: number; overrides?: SkillOverrides }; + call?: TriggerGrouped; + atking?: TriggerGrouped; + atked?: TriggerGrouped; + dead?: TriggerGrouped; + fstart?: TriggerGrouped; + fend?: TriggerGrouped; + revive?: LvReviveEntry[]; }; } @@ -324,35 +457,28 @@ export const TestModeConfig = { growthRatePerWave: 0.2, monType: MonType.Melee, monUuid: 6001, - /** @deprecated 已被 testElite 取代 */ - affixes: [] as AffixType[], spawnCount: 1, - /** 测试个体强化 */ - testElite: undefined as MonsterElite | undefined, - /** 测试波次强化 id */ - testWaveEnchant: undefined as string | undefined, - - skill: undefined as { s_uuid: number; cd?: number; overrides?: any } | undefined, - atking: undefined as Record | undefined, - atked: undefined as Record | undefined, - dead: undefined as Record | undefined, - fstart: undefined as Record | undefined, - fend: undefined as Record | undefined, + 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, + fend: undefined as TriggerGrouped | undefined, }; // ======================== 10. 生成引擎 ======================== /** * 肉鸽刷怪生成引擎 - * 按硬编码 WaveConfig 规则,组合小队模板与双层强化系统生成怪物列表 + * 按英雄强度反推怪物强度,结合波型系数与动态调节器生成怪物列表 */ export class RogueSpawningEngine { /** * 生成指定波次的怪物列表 - * @param waveNumber 波次(1 起,>30 时复用 wave 30 配置) - * @returns 怪物列表,长度 ≤ 12 + * @param waveNumber 波次(1 起,>MAX_WAVE 时复用 wave MAX_WAVE 配置) + * @returns 怪物列表,长度 ≤ MAX_MONSTERS */ generateWave(waveNumber: number): GeneratedMonster[] { if (waveNumber < 1) return []; @@ -362,97 +488,120 @@ export class RogueSpawningEngine { return this.generateTestWave(waveNumber); } - // 1. 取硬编码 WaveConfig(>30 时复用 wave 30 配置) - const cfg = WaveConfigs[Math.min(waveNumber, 30)]; + const wave = Math.min(waveNumber, MAX_WAVE); + const cfg = WaveConfigs[wave]; + const waveType = getWaveType(wave); + const typeRatio = WAVE_TYPE_POWER_RATIO[waveType]; - // 2. 抽取波次级强化(按权重从 enchant_pool 抽 0~2 个) - const enchants = this.pickEnchants(cfg.enchant_pool); + // 1. 计算目标强度 + const heroPower = this.getCurrentHeroPower(); + const powerAdjust = cfg.power_adjust ?? 1.0; + const targetPower = heroPower * typeRatio * powerAdjust * DynamicTuner.factor; - // 3. 计算波次最终属性乘子 - const globalScale = getGlobalScale(waveNumber); - const enchantHpMul = enchants.reduce((m, e) => m * (e.hp_mul ?? 1), 1); - const enchantApMul = enchants.reduce((m, e) => m * (e.ap_mul ?? 1), 1); - const eliteRateMul = enchants.reduce((m, e) => m * (e.elite_rate_mul ?? 1), 1); + // 2. 确定怪物总数(放松波 × 1.5) + let totalCount = cfg.base_count; + if (waveType === WaveType.Relax) { + totalCount = Math.round(totalCount * RELAX_COUNT_MUL); + } + totalCount = Math.min(totalCount, MAX_MONSTERS); - // 4. Boss 位(Boss 波首位占 1 个) + // 3. Boss 位(压力波必带 Boss,占 1 个名额) const monsters: GeneratedMonster[] = []; - let remaining = cfg.base_count; + let remaining = totalCount; if (cfg.boss_wave) { - monsters.push(this.makeBoss(waveNumber)); + monsters.push(this.makeBoss(wave, cfg)); remaining -= 1; } - // 5. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining - const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, waveNumber); + // 4. 小队拼装:按权重从 squad_pool 抽小队,累计到 remaining + const squadMonsters = this.assembleSquads(cfg.squad_pool, remaining, wave); monsters.push(...squadMonsters); - // 6. 应用全局 & 波次 Enchant 乘子 - const waveEnchantIds = enchants.map(e => e.id); + // 5. 应用硬编码 HP/AP 倍率 for (const m of monsters) { - m.hp = Math.max(1, Math.round(m.hp * globalScale.hp_mul * enchantHpMul)); - m.ap = Math.max(1, Math.round(m.ap * globalScale.ap_mul * enchantApMul)); - m.wave_enchants = waveEnchantIds.slice(); + m.hp = Math.max(1, Math.round(m.hp * cfg.hp_mul)); + m.ap = Math.max(1, Math.round(m.ap * cfg.ap_mul)); } - // 7. 应用个体 Elite(非 Boss 怪,每只最多 1 个) - const eliteBaseRate = cfg.elite_base_rate ?? Math.min(0.05 + waveNumber * 0.01, 0.30); - for (const m of monsters) { - if (!m.isBoss && Math.random() < eliteBaseRate * eliteRateMul) { - const elite = this.pickElite(); - this.applyElite(m, elite); + // 6. 按英雄强度反推缩放系数 + const totalBasePower = monsters.reduce((sum, m) => { + const info = HeroInfo[m.uuid]; + return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap); + }, 0); + + if (totalBasePower > 0 && targetPower > 0) { + const scale = targetPower / totalBasePower; + for (const m of monsters) { + m.hp = Math.max(1, Math.round(m.hp * scale)); + m.ap = Math.max(1, Math.round(m.ap * scale)); } } - // 8. 硬上限保护(理论不会触发,但防止配置错误) - return monsters.slice(0, 12); + // 7. 挂载技能(普通怪随机技能池,Boss 专属技能池) + for (const m of monsters) { + if (m.isBoss && cfg.boss_skill_pool) { + m.skills = this.pickSkillSet(cfg.boss_skill_pool, BossSkillPool); + } else if (!m.isBoss && cfg.skill_pool) { + m.skills = this.pickSkillSet(cfg.skill_pool, MonSkillPool); + } + } + + // 8. 分配批次(0~2,均匀分布) + for (let i = 0; i < monsters.length; i++) { + monsters[i].batch = i % BATCH_COUNT; + monsters[i].spawnIndex = i; + } + + // 9. 硬上限保护 + return monsters.slice(0, MAX_MONSTERS); } - /** 重置引擎状态(保留接口兼容,当前无内部状态) */ + /** 重置引擎状态(每局开始时调用) */ reset(): void { - // 无可变状态 + DynamicTuner.reset(); } /** * 获取指定波次的怪物总数 * @param waveNumber 目标波数 - * @returns 预计生成的怪物总数(base_count,受 12 上限约束) + * @returns 预计生成的怪物总数 */ getWaveMonsterCount(waveNumber: number): number { if (waveNumber < 1) return 0; if (TestModeConfig.enable) { return Math.max(1, TestModeConfig.spawnCount || 1); } - const cfg = WaveConfigs[Math.min(waveNumber, 30)]; - return Math.min(cfg.base_count, 12); - } - - /** - * 获取波次槽位配置(向后兼容接口) - * 内部从 generateWave 反推,仅用于历史调用方 - * @param waveNumber 目标波数 - */ - getWaveSlotConfig(waveNumber: number): IWaveSlot[] { - const generated = this.generateWave(waveNumber); - const slotMap = new Map(); - - for (const m of generated) { - const existing = slotMap.get(m.type); - if (existing) { - existing.count++; - } else { - slotMap.set(m.type, { count: 1, affixes: [] }); - } + const wave = Math.min(waveNumber, MAX_WAVE); + const cfg = WaveConfigs[wave]; + const waveType = getWaveType(wave); + let count = cfg.base_count; + if (waveType === WaveType.Relax) { + count = Math.round(count * RELAX_COUNT_MUL); } - - return Array.from(slotMap.entries()).map(([type, data]) => ({ - type, - count: data.count, - ...(data.affixes.length > 0 ? { affixes: data.affixes } : {}), - })); + return Math.min(count, MAX_MONSTERS); } // ======================== 私有生成子算法 ======================== + /** + * 计算场上存活英雄的总强度 + * 通过 ECS 查询所有 HeroAttrsComp,累加 calcHeroPower + */ + private getCurrentHeroPower(): number { + let total = 0; + ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => { + const attrs = entity.get(HeroAttrsComp); + if (!attrs || attrs.is_dead || attrs.fac !== FacSet.HERO) return; + const info = HeroInfo[attrs.hero_uuid]; + if (!info) return; + // 使用英雄当前等级(升级卡驱动),保证强度评估与实战一致 + const lv = Math.max(1, attrs.lv || 1); + total += calcHeroPower(info, lv); + }); + // 兜底:无英雄时返回基准强度,避免除零 + return Math.max(total, 100); + } + /** 测试模式:完全绕过引擎逻辑 */ private generateTestWave(waveNumber: number): GeneratedMonster[] { const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave; @@ -465,12 +614,10 @@ export class RogueSpawningEngine { type: TestModeConfig.monType, hp: Math.round(TestModeConfig.baseHp * growth), ap: Math.round(TestModeConfig.baseAp * growth), - affixes: [...TestModeConfig.affixes], isBoss: false, spawnIndex: i, - elite: TestModeConfig.testElite, - wave_enchants: TestModeConfig.testWaveEnchant ? [TestModeConfig.testWaveEnchant] : [], - testSkills: { + batch: i % BATCH_COUNT, + skills: { skill: TestModeConfig.skill, atking: TestModeConfig.atking, atked: TestModeConfig.atked, @@ -509,7 +656,7 @@ export class RogueSpawningEngine { } /** 生成 Boss(首位) */ - private makeBoss(wave: number): GeneratedMonster { + private makeBoss(wave: number, cfg: WaveConfig): GeneratedMonster { const isMeleeBoss = Math.random() < 0.5; const type = isMeleeBoss ? MonType.MeleeBoss : MonType.LongBoss; @@ -532,10 +679,9 @@ export class RogueSpawningEngine { type, hp: Math.round(baseHp * bossBonusHpMul), ap: baseAp, - affixes: [], // 兼容字段 isBoss: true, spawnIndex: 0, - wave_enchants: [], // 后续统一填充 + batch: 0, // Boss 固定第一批 }; } @@ -557,52 +703,28 @@ export class RogueSpawningEngine { type, hp: baseHp, ap: baseAp, - affixes: [], // 兼容字段 isBoss: false, spawnIndex, - wave_enchants: [], // 后续统一填充 + batch: 0, // 后续统一分配 }; } - /** 抽取波次级强化(按权重抽 0~2 个,不重复) */ - private pickEnchants(pool?: string[]): WaveEnchant[] { - if (!pool || pool.length === 0) return []; - const enchants: WaveEnchant[] = []; - - // 第一个 Enchant: 70% 概率抽 1 个 - if (Math.random() < 0.7) { - const first = this.pickWeightedEnchant(pool); - if (first) enchants.push(first); - } - - // 第二个 Enchant: 30% 概率再抽 1 个(不重复) - if (Math.random() < 0.3 && pool.length > 1) { - const remaining = pool.filter(id => !enchants.some(e => e.id === id)); - const second = this.pickWeightedEnchant(remaining); - if (second) enchants.push(second); - } - - return enchants; - } - - /** 抽取个体强化(等概率随机 1 种) */ - private pickElite(): MonsterElite { - const candidates: MonsterElite[] = [ - MonsterElite.Elite, - MonsterElite.Berserk, - MonsterElite.Shield, - MonsterElite.Swift, - MonsterElite.Regen, - ]; - return candidates[Math.floor(Math.random() * candidates.length)]; - } - - /** 应用个体强化到怪物 */ - private applyElite(m: GeneratedMonster, elite: MonsterElite): void { - const def = MonsterEliteSet[elite]; - m.hp = Math.max(1, Math.round(m.hp * def.hp_mul)); - m.ap = Math.max(1, Math.round(m.ap * def.ap_mul)); - m.elite = elite; + /** 从技能池中按权重抽取一套技能 */ + 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, + fend: picked.fend, + revive: picked.revive, + }; } /** 按权重从小队 id 池抽 1 个小队 */ @@ -612,13 +734,6 @@ export class RogueSpawningEngine { return this.pickWeighted(valid); } - /** 按权重从强化 id 池抽 1 个强化 */ - private pickWeightedEnchant(pool: string[]): WaveEnchant | null { - const valid = pool.map(id => WaveEnchantLibrary[id]).filter(e => e); - if (valid.length === 0) return null; - return this.pickWeighted(valid); - } - /** 通用加权随机抽取 */ private pickWeighted(items: T[]): T | null { if (items.length === 0) return null; @@ -641,8 +756,6 @@ export const spawningEngine = new RogueSpawningEngine(); export interface IWaveSlot { type: number; count: number; - /** @deprecated 已废弃,新接口不再使用 */ - affixes?: AffixType[]; } /** @@ -653,9 +766,16 @@ export function getWaveMonsterCount(waveNumber: number): number { return spawningEngine.getWaveMonsterCount(waveNumber); } -/** 获取波次槽位配置(向后兼容) */ +/** 获取波次槽位配置(向后兼容,从 generateWave 反推) */ export function getWaveSlotConfig(waveNumber: number): IWaveSlot[] { - return spawningEngine.getWaveSlotConfig(waveNumber); + const generated = spawningEngine.generateWave(waveNumber); + const slotMap = new Map(); + + for (const m of generated) { + slotMap.set(m.type, (slotMap.get(m.type) || 0) + 1); + } + + return Array.from(slotMap.entries()).map(([type, count]) => ({ type, count })); } export const DefaultWaveSlot: IWaveSlot[] = [ @@ -671,7 +791,7 @@ export const WaveSlotConfig: { [wave: number]: IWaveSlot[] } = new Proxy( get(_target, prop: string) { const wave = parseInt(prop, 10); if (!isNaN(wave) && wave >= 1) { - return spawningEngine.getWaveSlotConfig(wave); + return getWaveSlotConfig(wave); } if (prop === "toJSON") return () => ({}); return undefined;