You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/battle/combat/TowerStatResolver.ts

186 lines
7.1 KiB

import { heroNormalSkillId, isElementHero } from '../config/BattleHeroTable';
import { BattleMapSpec } from '../map/BattleMapSpec';
import { TableNames } from '../../game/ConfigTableData';
import type { TowerCombatStats } from './TowerQueryTypes';
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
/** 常量表「攻击力等级系数」id(对齐 ConstNumType.AtkLevelCoef) */
const CONST_ATK_LEVEL_COEF = 13;
/** setupCombat 用的内部结果(range 为像素) */
export interface TowerSetupStats extends TowerCombatStats {
/** 有效等级 */
effectiveLevel: number;
/** Const 攻击力等级系数 */
levelCoef: number;
}
/** 战斗信息弹窗数值展示 */
export class TowerStatDisplay {
/**
* 攻击力/经验:最多 1 位小数,整数不带小数点。
* @param value 原始数值
*/
static oneDecimal(value: number): string {
const n = Math.round(Number(value) * 10) / 10;
if (!Number.isFinite(n)) {
console.log('[TowerStatDisplay] 非法数值', value);
return String(value);
}
return n.toFixed(1).replace(/\.0$/, '');
}
/**
* 攻速:保留 2 位小数,整数去掉 .00。
* @param value 原始攻速
*/
static attackSpeed(value: number): string {
const n = Number(value);
if (!Number.isFinite(n)) {
console.log('[TowerStatDisplay] 非法攻速', value);
return String(value);
}
return n.toFixed(2).replace(/\.00$/, '');
}
}
/**
* 塔索敌射程(像素):优先 Skill.range;为 0/未配时用 Hero.atk_range;-1=全屏。
* @param hero Hero 行
* @param skill 普攻 Skill;可空
*/
export function resolveTowerRangePx(hero: ITableHero, skill: ITableSkill | null): number {
if (skill) {
if (skill.range < 0) return 99 * BattleMapSpec.CELL_SIZE;
if (skill.range > 0) return skill.range;
}
if (hero.atk_range > 0) return hero.atk_range;
return 3 * BattleMapSpec.CELL_SIZE;
}
/**
* @deprecated 请用 resolveTowerRangePx
* @param baseAtk 普攻配表;null 默认 3 格像素
*/
export function resolveBaseAtkRangePx(baseAtk: ITableSkill | null): number {
if (!baseAtk) return 3 * BattleMapSpec.CELL_SIZE;
if (baseAtk.range < 0) return 99 * BattleMapSpec.CELL_SIZE;
if (baseAtk.range > 0) return baseAtk.range;
return 3 * BattleMapSpec.CELL_SIZE;
}
/**
* 合成等级 → 攻击力等级系数(常量表 id=13 para1,按下标 level-1)。
* @param composeLevel 有效合成等级(≥1)
*/
export function resolveAtkLevelCoef(composeLevel: number): number {
const coefs = gg.data.table.getConstArray(CONST_ATK_LEVEL_COEF);
if (coefs.length === 0) {
console.log('[TowerStatResolver] Const 攻击力等级系数 para1 为空', CONST_ATK_LEVEL_COEF);
return Number.NaN;
}
const level = Math.max(1, Math.floor(composeLevel));
const idx = level - 1;
if (idx >= coefs.length) {
console.log(
'[TowerStatResolver] 合成等级超出攻击力等级系数表',
level,
coefs.length,
);
}
const coef = coefs[Math.min(idx, coefs.length - 1)];
if (typeof coef !== 'number' || !Number.isFinite(coef)) {
console.log('[TowerStatResolver] 攻击力等级系数非法', level, coef);
return Number.NaN;
}
return coef;
}
/**
* 武器被动 type 11 射程加成(像素)。主动技能的 atk_buff 走命中,不计入。
* @param weapon 当前武器行;可空
*/
function weaponLoadoutRangeAddPx(weapon: ITableWeapon | null): number {
if (!weapon) return 0;
const skillId = Number(weapon.act_skill) || 0;
if (!(skillId > 0)) return 0;
const skill = gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId);
if (!skill || Number(skill.type) !== 2) return 0;
return rangeAddFromAtkBuff(skill.atk_buff1) + rangeAddFromAtkBuff(skill.atk_buff2);
}
/**
* atk_buff type 11 射程像素(绝对值 ≤16 按格)。
* @param raw Skill.atk_buff 原文
*/
function rangeAddFromAtkBuff(raw: string | undefined): number {
const s = String(raw ?? '').trim();
if (!s) return 0;
const parts = s.split(',');
if (Number(parts[0]) !== 11) return 0;
const addVal = Math.floor(Number(parts[1]) || 0);
if (addVal === 0) return 0;
return Math.abs(addVal) <= 16 ? addVal * BattleMapSpec.CELL_SIZE : addVal;
}
/**
* 局内合成等级 → 战斗基础数值(不含局外角色攻击/暴击/冷却系数)。
* 实际攻击力 = Hero.atk × 等级系数 + 装备 atk(若有)。
* @param w1 Hero 行
* @param composeLevel 合成等级
* @param battleWeaponId 战斗武器 id;AI/指定装载须传入,缺省才读本机 gg.equip
*/
export function deriveTowerSetupStats(w1: ITableHero, composeLevel: number, battleWeaponId?: number): TowerSetupStats {
const compose = Math.max(1, Math.floor(composeLevel) || 1);
let effectiveLevel: number;
if (isElementHero(w1)) {
const cap = WeaponComposeConfigProvider.instance.getElementMaxLevel(Math.floor(Number(w1.herotype)));
effectiveLevel = Math.min(compose, cap);
} else {
if (!(w1.level_limit > 0)) {
console.log('[TowerStatResolver] Hero.level_limit 未配置', w1.id);
}
const levelLimit = w1.level_limit > 0 ? w1.level_limit : 1;
effectiveLevel = Math.min(compose, levelLimit);
}
const weaponId = resolveSetupWeaponId(w1, battleWeaponId);
const weaponRow = weaponId > 0
? gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, weaponId)
: null;
const exclusiveMul = weaponRow ? gg.equip.getEquipExclusiveMul(w1.id, weaponRow) : 1;
const weaponAtk = (weaponRow?.atk ?? 0) * exclusiveMul;
const levelCoef = resolveAtkLevelCoef(effectiveLevel);
const attack = w1.atk * levelCoef + weaponAtk;
const skillId = heroNormalSkillId(w1, effectiveLevel);
const skillRow = skillId > 0 ? gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId) : null;
const attackSpeed = skillRow?.cd > 0 ? 1 / skillRow.cd : 1;
const critRate = Math.min(1, (w1.critical_chance ?? 0) / 10000);
const critDamage = (w1.critical_dam ?? 15000) / 10000;
const range = resolveTowerRangePx(w1, skillRow) + weaponLoadoutRangeAddPx(weaponRow);
return {
effectiveLevel,
levelCoef,
attack,
attackSpeed,
attackInterval: attackSpeed > 0 ? 1 / attackSpeed : 0,
range,
critRate,
critDamage,
// 0=不限。勿写成 1/Skill.cd:会与基础攻速相同,aspd_add(乾坤枪等)加完再被夹回原值
maxAttackSpeed: 0,
};
}
/**
* 解析 setup 用武器 id:显式 battleWeaponId 优先,否则本机穿戴/初始武器。
* @param w1 Hero 行
* @param battleWeaponId 调用方已解析的战斗武器(含 AI)
*/
function resolveSetupWeaponId(w1: ITableHero, battleWeaponId?: number): number {
if (typeof battleWeaponId === 'number' && battleWeaponId > 0) return battleWeaponId;
const wornId = gg.equip.getHeroEquipId(w1.id);
if (wornId > 0) return wornId;
return Number(w1.startweapon) || 0;
}