diff --git a/assets/script/game/map/RogueConfig.ts b/assets/script/game/map/RogueConfig.ts index 5d17154a..8805eb9e 100644 --- a/assets/script/game/map/RogueConfig.ts +++ b/assets/script/game/map/RogueConfig.ts @@ -9,12 +9,16 @@ * 4. MonSkillSet - 怪物技能池(atking / atked / dead 等全触发类型) * 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度) * - * 核心公式(最终强度 = heroPower × typeRatio × power_adjust × DDA × hp/ap_mul): + * 核心公式: * heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄) * targetPower = heroPower × 回合类型系数 × wave.power_adjust × DynamicTuner.factor * hpScale = targetPower × wave.hp_mul ÷ Σ 怪物基础强度 - * apScale = targetPower × wave.ap_mul ÷ Σ 怪物基础强度 - * 每只怪: hp ×= hpScale, ap ×= apScale + * 怪物 hp = base_hp × hpScale (走总强度框架,自动匹配玩家) + * + * 怪物 ap 独立挂钩玩家输出(不走总强度,避免被 hp 挤占导致"涨不动"): + * 怪物 ap = 玩家平均攻击 × getApWaveGrowth(wave) × AP_RELATIVE × 类型系数 + * 回合递进取线性 1.0 → 3.0(wave 1 → 20) + * 说明:ap_mul 字段已废弃(保留字段仅为兼容配置表,不再参与计算) * * 回合节奏: * - 最大 20 回合,第 20 回合通关 @@ -87,6 +91,19 @@ export const SPAWN_INTERVAL_BY_TYPE: Record = { [WaveType.Relax]: 0.12, }; +/** + * 怪物 ap 回合递进曲线(挂钩玩家输出,独立于总强度框架)。 + * 怪物 ap = 玩家平均攻击 × AP_WAVE_GROWTH(wave) × AP_RELATIVE × 怪物类型系数。 + * 线性 1.0 → 3.0(wave 1 → 20),保证后期伤害有可感知的爬升。 + */ +export function getApWaveGrowth(wave: number): number { + const t = Math.min(Math.max(wave, 1), MAX_WAVE); + return 1.0 + (t - 1) / (MAX_WAVE - 1) * 2.0; // wave1=1.0 → wave20=3.0 +} + +/** 怪物 ap 相对玩家平均攻击的基础系数(<1 保证单怪不至于一击致命,由 Boss/精英类型再上浮) */ +export const AP_RELATIVE = 0.6; + /** 回合战斗超时(秒):超过后强制结束回合(残留怪销毁并扣留存分,DynamicTuner 因 clearTime 过大自动放水) */ export const WAVE_TIMEOUT = 75; @@ -360,7 +377,7 @@ export interface WaveConfig { squad_pool: string[]; /** HP 成长乘区(并入强度缩放分子,终值 = heroPower × 系数 × hp_mul ÷ Σ基础强度) */ hp_mul: number; - /** AP 成长乘区(同 hp_mul,独立控制怪物肉度与输出的成长比例) */ + /** @deprecated 已废弃:ap 改为挂钩玩家输出 × getApWaveGrowth,本字段不再参与计算(保留仅为兼容配置表结构) */ ap_mul: number; /** 强度微调(放水 / 加压,默认 1.0) */ power_adjust?: number; @@ -594,8 +611,10 @@ export class RogueSpawningEngine { monsters.push(boss); } - // 5. 按英雄强度反推缩放系数(hp_mul/ap_mul 并入目标强度乘区,而非预先乘到怪物上, - // 保证配置表语义单一:最终强度 = heroPower × typeRatio × power_adjust × DDA × hp/ap_mul) + // 5. 属性计算: + // - hp 走总强度缩放框架(自动匹配玩家战力):hp = base_hp × hpScale + // - ap 与总强度脱钩,挂钩玩家输出 × 回合递进(解决"ap 随回合涨不动": + // 原方案 ap 被总强度封顶 + hp/ap 共用分母,增长被熨平到每回合 5~8%) const totalBasePower = monsters.reduce((sum, m) => { const info = HeroInfo[m.uuid]; return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap); @@ -603,10 +622,12 @@ export class RogueSpawningEngine { if (totalBasePower > 0 && targetPower > 0) { const hpScale = (targetPower * cfg.hp_mul) / totalBasePower; - const apScale = (targetPower * cfg.ap_mul) / totalBasePower; + const heroAvgAp = this.getCurrentHeroAttack(); + const apGrowth = getApWaveGrowth(wave); for (const m of monsters) { m.hp = Math.max(1, Math.round(m.hp * hpScale)); - m.ap = Math.max(1, Math.round(m.ap * apScale)); + // ap = 玩家平均攻击 × 回合递进 × 基础系数 × 类型系数(Boss/护卫/精英在上游已上浮 base_ap) + m.ap = Math.max(1, Math.round(heroAvgAp * apGrowth * AP_RELATIVE * this.getApTypeFactor(m))); } } @@ -681,6 +702,25 @@ export class RogueSpawningEngine { return Math.max(total, 100); } + /** + * 计算场上存活英雄的输出强度(用于怪物 ap 挂钩玩家,独立于总强度框架)。 + * 取玩家英雄的"实时 ap × 数量",代表玩家当前能打出/承受的攻击量级。 + * 怪物 ap 以此为锚,保证玩家输出越高、被反击越痛(威胁感跟随成长)。 + */ + private getCurrentHeroAttack(): number { + let totalAp = 0; + let count = 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; + totalAp += Math.max(1, attrs.ap || 1); + count++; + }); + // 兜底:无英雄时返回基准攻击,避免除零 + if (count === 0) return 30; + return totalAp / count; // 平均单英雄攻击 + } + /** 测试模式:完全绕过引擎逻辑 */ private generateTestWave(waveNumber: number): GeneratedMonster[] { const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave; @@ -764,6 +804,21 @@ export class RogueSpawningEngine { }; } + /** + * 怪物 ap 类型系数:Boss/护卫上浮,远程/召唤稍降(避免玻璃大炮互秒),近战/重甲基准。 + * 配合 getApWaveGrowth 使用,让"谁更疼"有明确的类型区分。 + */ + private getApTypeFactor(m: GeneratedMonster): number { + if (m.isBoss) return 2.2; // Boss 单发重,符合"压场"定位 + if (m.isBossGuard) return 1.2; // 护卫略高于杂鱼 + switch (m.type) { + case MonType.Long: return 0.9; + case MonType.Summoner: return 0.7; + case MonType.Assassin: return 1.1; + default: return 1.0; // Melee / Heavy + } + } + /** 生成 Boss 护卫队(复用 boss_guard 小队模板),与 Boss 同批压轴进场 */ private makeBossGuards(wave: number): GeneratedMonster[] { const squad = SquadLibrary["boss_guard"];