import { heroNormalSkillId } from '../config/BattleHeroTable'; import { BattleMapSpec } from '../map/BattleMapSpec'; import { TableNames } from '../../game/ConfigTableData'; import type { TowerCombatStats } from './TowerQueryTypes'; /** 常量表「攻击力等级系数」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(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 { if (!(w1.level_limit > 0)) { console.log('[TowerStatResolver] Hero.level_limit 未配置', w1.id); } const levelLimit = w1.level_limit > 0 ? w1.level_limit : 1; const effectiveLevel = Math.max(1, Math.min(composeLevel, levelLimit)); const weaponId = resolveSetupWeaponId(w1, battleWeaponId); const weaponRow = weaponId > 0 ? gg.data.table.getTableData(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(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; }