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.
188 lines
7.2 KiB
188 lines
7.2 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;
|
|
// 五行数字:技能宝石由调用方传入;无则 0,禁止回落到名将装备
|
|
if (isElementHero(w1)) return 0;
|
|
const wornId = gg.equip.getHeroEquipId(w1.id);
|
|
if (wornId > 0) return wornId;
|
|
return Number(w1.startweapon) || 0;
|
|
}
|
|
|