refactor(roguemap): 重构怪物ap计算逻辑,脱离总强度框架

1. 新增怪物ap独立计算体系,挂钩玩家平均攻击与回合递进
2. 废弃原ap_mul配置字段,保留仅用于兼容旧配置
3. 新增类型系数、成长曲线函数与英雄平均攻击统计逻辑
4. 修复原ap成长被总强度熨平的问题,提升后期战斗威胁感
This commit is contained in:
pan
2026-08-11 19:15:55 +08:00
parent 3c9496d753
commit f251e57c3c

View File

@@ -9,12 +9,16 @@
* 4. MonSkillSet - 怪物技能池atking / atked / dead 等全触发类型) * 4. MonSkillSet - 怪物技能池atking / atked / dead 等全触发类型)
* 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度) * 5. RogueSpawningEngine - 生成引擎(按英雄强度反推怪物强度)
* *
* 核心公式(最终强度 = heroPower × typeRatio × power_adjust × DDA × hp/ap_mul * 核心公式:
* heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄) * heroPower = Σ calcHeroPower(HeroInfo[uuid], lv) (场上存活英雄)
* targetPower = heroPower × 回合类型系数 × wave.power_adjust × DynamicTuner.factor * targetPower = heroPower × 回合类型系数 × wave.power_adjust × DynamicTuner.factor
* hpScale = targetPower × wave.hp_mul ÷ Σ 怪物基础强度 * hpScale = targetPower × wave.hp_mul ÷ Σ 怪物基础强度
* apScale = targetPower × wave.ap_mul ÷ Σ 怪物基础强度 * 怪物 hp = base_hp × hpScale (走总强度框架,自动匹配玩家)
* 每只怪: hp ×= hpScale, ap ×= apScale *
* 怪物 ap 独立挂钩玩家输出(不走总强度,避免被 hp 挤占导致"涨不动"
* 怪物 ap = 玩家平均攻击 × getApWaveGrowth(wave) × AP_RELATIVE × 类型系数
* 回合递进取线性 1.0 → 3.0wave 1 → 20
* 说明ap_mul 字段已废弃(保留字段仅为兼容配置表,不再参与计算)
* *
* 回合节奏: * 回合节奏:
* - 最大 20 回合,第 20 回合通关 * - 最大 20 回合,第 20 回合通关
@@ -87,6 +91,19 @@ export const SPAWN_INTERVAL_BY_TYPE: Record<WaveType, number> = {
[WaveType.Relax]: 0.12, [WaveType.Relax]: 0.12,
}; };
/**
* 怪物 ap 回合递进曲线(挂钩玩家输出,独立于总强度框架)。
* 怪物 ap = 玩家平均攻击 × AP_WAVE_GROWTH(wave) × AP_RELATIVE × 怪物类型系数。
* 线性 1.0 → 3.0wave 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 过大自动放水) */ /** 回合战斗超时超过后强制结束回合残留怪销毁并扣留存分DynamicTuner 因 clearTime 过大自动放水) */
export const WAVE_TIMEOUT = 75; export const WAVE_TIMEOUT = 75;
@@ -360,7 +377,7 @@ export interface WaveConfig {
squad_pool: string[]; squad_pool: string[];
/** HP 成长乘区(并入强度缩放分子,终值 = heroPower × 系数 × hp_mul ÷ Σ基础强度) */ /** HP 成长乘区(并入强度缩放分子,终值 = heroPower × 系数 × hp_mul ÷ Σ基础强度) */
hp_mul: number; hp_mul: number;
/** AP 成长乘区(同 hp_mul独立控制怪物肉度与输出的成长比例 */ /** @deprecated 已废弃ap 改为挂钩玩家输出 × getApWaveGrowth本字段不再参与计算保留仅为兼容配置表结构 */
ap_mul: number; ap_mul: number;
/** 强度微调(放水 / 加压,默认 1.0 */ /** 强度微调(放水 / 加压,默认 1.0 */
power_adjust?: number; power_adjust?: number;
@@ -594,8 +611,10 @@ export class RogueSpawningEngine {
monsters.push(boss); monsters.push(boss);
} }
// 5. 按英雄强度反推缩放系数hp_mul/ap_mul 并入目标强度乘区,而非预先乘到怪物上, // 5. 属性计算:
// 保证配置表语义单一:最终强度 = heroPower × typeRatio × power_adjust × DDA × hp/ap_mul // - hp 走总强度缩放框架自动匹配玩家战力hp = base_hp × hpScale
// - ap 与总强度脱钩,挂钩玩家输出 × 回合递进(解决"ap 随回合涨不动"
// 原方案 ap 被总强度封顶 + hp/ap 共用分母,增长被熨平到每回合 5~8%
const totalBasePower = monsters.reduce((sum, m) => { const totalBasePower = monsters.reduce((sum, m) => {
const info = HeroInfo[m.uuid]; const info = HeroInfo[m.uuid];
return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap); return sum + (info ? calcHeroPower(info, 1) : m.hp + m.ap);
@@ -603,10 +622,12 @@ export class RogueSpawningEngine {
if (totalBasePower > 0 && targetPower > 0) { if (totalBasePower > 0 && targetPower > 0) {
const hpScale = (targetPower * cfg.hp_mul) / totalBasePower; 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) { for (const m of monsters) {
m.hp = Math.max(1, Math.round(m.hp * hpScale)); 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); 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[] { private generateTestWave(waveNumber: number): GeneratedMonster[] {
const growth = 1 + (waveNumber - 1) * TestModeConfig.growthRatePerWave; 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 同批压轴进场 */ /** 生成 Boss 护卫队(复用 boss_guard 小队模板),与 Boss 同批压轴进场 */
private makeBossGuards(wave: number): GeneratedMonster[] { private makeBossGuards(wave: number): GeneratedMonster[] {
const squad = SquadLibrary["boss_guard"]; const squad = SquadLibrary["boss_guard"];