miao-plugin/components/Calc.js
Kokomi 78e874aa63 * #更新全部面板 功能暂时恢复
* 目前可直接使用,无需token
  * 部分网络可能无法请求,等待后续服务更新
  * 由于服务逻辑与之前数据不一致,部分角色的属性及伤害计算可能会不准确,如有发现请反馈给喵喵
2022-06-02 18:25:09 +08:00

589 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from "fs";
import lodash from "lodash";
import Format from "./Format.js";
const eleMap = {
Anemo: "风",
Cryo: "冰",
Electro: "雷",
Geo: "岩",
Hydro: "水",
Pyro: "火"
}
const attrMap = {
atk: { type: "pct", val: 5.83, title: "大攻击", text: "5.8%" },
hp: { type: "pct", val: 5.83, title: "大生命", text: "5.8%" },
def: { type: "pct", val: 7.29, title: "大防御", text: "7.3%" },
recharge: { type: "plus", val: 6.48, title: "元素充能", text: "6.5%" },
mastery: { type: "plus", val: 23.31, title: "元素精通", text: "23.3" },
cpct: { type: "plus", val: 3.89, title: "暴击率", text: "3.9%" },
cdmg: { type: "plus", val: 7.77, title: "暴击伤害", text: "7.8%" },
}
let Calc = {
async getCharCalcRule(name) {
const _path = process.cwd();
const cfgPath = `${_path}/plugins/miao-plugin/resources/meta/character/${name}/calc.js`;
let details, buffs = [], defParams = {}, defDmgIdx = -1, mainAttr = "atk,cpct,cdmg", enemyName = "小宝";
if (fs.existsSync(cfgPath)) {
let fileData = await import (`file://${cfgPath}`);
details = fileData.details || false;
buffs = fileData.buffs || [];
defParams = fileData.defParams || {};
if (fileData.defDmgIdx) {
defDmgIdx = fileData.defDmgIdx;
}
if (fileData.mainAttr) {
mainAttr = fileData.mainAttr;
}
if (fileData.enemyName) {
enemyName = fileData.enemyName;
}
}
if (details) {
return { details, buffs, defParams, defDmgIdx, mainAttr, enemyName }
}
return false;
},
// 获取基础属性
attr(profile, avatar) {
let ret = {},
{ attr } = profile;
ret.dataSource = profile.dataSource || "miao";
// 基础属性
lodash.forEach("atk,def,hp".split(","), (key) => {
ret[key] = {
base: attr[`${key}Base`] * 1 || 0,
plus: attr[key] * 1 - attr[`${key}Base`] * 1 || 0,
pct: 0
}
})
lodash.forEach("mastery,recharge".split(","), (key) => {
ret[key] = {
base: attr[key] * 1 || 0,
plus: 0,
pct: 0
}
})
lodash.forEach({ cRate: "cpct", cDmg: "cdmg", hInc: "heal" }, (val, key) => {
ret[val] = {
base: attr[key] * 1 || 0,
plus: 0,
pct: 0,
inc: 0
}
})
lodash.forEach("dmg,phy".split(","), (key) => {
ret[key] = {
base: attr[key + "Bonus"] * 1 || 0,
plus: 0,
pct: 0
}
})
// 技能属性记录
lodash.forEach("a,a2,a3,e,q".split(","), (key) => {
ret[key] = {
pct: 0, // 倍率加成
multi: 0, // 独立倍率乘区加成
plus: 0, // 伤害值提高
dmg: 0, // 伤害提高
cpct: 0,// 暴击提高
cdmg: 0, //爆伤提高
def: 0, // 防御降低
ignore: 0, // 无视防御
}
})
ret.enemy = {
def: 0, // 降低防御
ignore: 0, // 无视防御
phy: 0 // 物理防御
}
ret.shield = {
base: 100, // 基础
plus: 0, // 护盾强效
inc: 100, // 吸收倍率
}
ret.weaponType = avatar.weapon.type_name;
ret.weapon = avatar.weapon;
ret.element = eleMap[avatar.element];
ret.refine = (avatar.weapon.affix_level * 1 - 1) || 0;
ret.multi = 0;
ret.zf = 0;
ret.rh = 0;
ret.kx = 0;
return ret;
},
// 获取天赋数据
talent(talentData, char) {
let ret = {};
lodash.forEach(['a', 'e', 'q'], (key) => {
let lv = talentData[key].level_current * 1 || 1,
lvKey = `Lv${lv}`;
let map = {};
lodash.forEach(char.talent[key].tables, (tr) => {
let val = tr.values[lv - 1];
val = val.replace(/[^\x00-\xff]/g, "").trim();
let valArr = [], valArr2 = [];
lodash.forEach(val.split("/"), (v, idx) => {
let valNum = 0;
lodash.forEach(v.split("+"), (v) => {
v = v.split("*")
let v1 = v[0].replace("%", "").trim();
valNum += v1 * (v[1] || 1);
valArr2.push(v1)
})
valArr.push(valNum);
});
if (isNaN(valArr[0])) {
map[tr.name] = false;
} else if (valArr.length === 1) {
map[tr.name] = valArr[0];
} else {
map[tr.name] = valArr;
}
map[tr.name + "2"] = valArr2;
})
ret[key] = map;
})
return ret;
},
getDs(attr, meta, params) {
return {
...meta,
attr,
params,
refine: attr.refine,
weaponType: attr.weaponType,
weapon: attr.weapon,
element: eleMap[attr.element] || attr.element,
calc(ds) {
return (ds.base || 0) + (ds.plus || 0) + ((ds.base || 0) * (ds.pct || 0) / 100)
}
}
},
calcAttr({ originalAttr, buffs, meta, params = {}, incAttr = '', reduceAttr = '', talent = '' }) {
let attr = lodash.merge({}, originalAttr);
let msg = [];
if (incAttr && attrMap[incAttr]) {
let aCfg = attrMap[incAttr];
attr[incAttr][aCfg.type] += aCfg.val;
}
if (reduceAttr && attrMap[reduceAttr]) {
let aCfg = attrMap[reduceAttr];
attr[reduceAttr][aCfg.type] -= aCfg.val;
}
lodash.forEach(buffs, (buff) => {
let ds = Calc.getDs(attr, meta, params);
ds.currentTalent = talent;
// 如果存在rule则进行计算
if (buff.check && !buff.check(ds)) {
return;
}
if (buff.cons) {
if (ds.cons * 1 < buff.cons * 1) {
return;
}
}
let title = buff.title;
if (buff.mastery) {
let mastery = Math.max(0, attr.mastery.base + attr.mastery.plus);
let masteryNum = 2.78 * mastery / (mastery + 1400) * 100;
buff.data = buff.data || {};
lodash.forEach(buff.mastery.split(","), (key) => {
buff.data[key] = masteryNum;
})
}
lodash.forEach(buff.data, (val, key) => {
if (lodash.isFunction(val)) {
val = val(ds);
}
title = title.replace(`[${key}]`, Format.comma(val, 1));
// 技能提高
let tRet = /^(a|a2|a3|e|q)(Def|Ignore|Dmg|Plus|Pct|Cpct|Cdmg|Multi)$/.exec(key);
if (tRet) {
attr[tRet[1]][tRet[2].toLowerCase()] += val * 1 || 0;
return;
}
let aRet = /^(hp|def|atk|mastery|cpct|cdmg|heal|recharge|dmg|phy|shield)(Plus|Pct|Inc)?$/.exec(key);
if (aRet) {
attr[aRet[1]][aRet[2] ? aRet[2].toLowerCase() : "plus"] += val * 1 || 0;
return;
}
if (key === "enemyDef") {
attr.enemy.def += val * 1 || 0;
return;
}
if (["zf", "rh", "kx"].includes(key)) {
attr[key] += val * 1 || 0;
}
});
msg.push(title);
})
return {
attr, msg
}
},
async weapon(weaponName) {
const _path = process.cwd();
const cfgPath = `${_path}/plugins/miao-plugin/resources/meta/weapons/calc.js`;
let weapons = {};
if (fs.existsSync(cfgPath)) {
let fileData = await import (`file://${cfgPath}`);
weapons = fileData.weapons || {};
}
let weaponCfg = weapons[weaponName] || [];
if (lodash.isPlainObject(weaponCfg)) {
weaponCfg = [weaponCfg];
}
lodash.forEach(weaponCfg, (ds) => {
if (!//.test(ds.title)) {
ds.title = `${weaponName}${ds.title}`;
}
if (ds.refine) {
ds.data = ds.data || {};
lodash.forEach(ds.refine, (r, key) => {
ds.data[key] = ({ refine }) => r[refine] * (ds.buffCount || 1);
})
}
})
return weaponCfg;
},
async reliquaries(sets) {
const _path = process.cwd();
const cfgPath = `${_path}/plugins/miao-plugin/resources/meta/reliquaries/calc.js`;
let buffs = {};
if (fs.existsSync(cfgPath)) {
let fileData = await import (`file://${cfgPath}`);
buffs = fileData.buffs || {};
}
let setMap = {};
lodash.forEach(sets, (set) => {
if (set && set.set) {
let name = set.set.name
setMap[name] = (setMap[name] || 0) + 1
}
});
let retBuffs = [];
lodash.forEach(setMap, (count, setName) => {
if (count >= 2 && buffs[setName + 2]) {
retBuffs.push(buffs[setName + 2])
}
if (count >= 4 && buffs[setName + 4]) {
retBuffs.push(buffs[setName + 4])
}
})
return retBuffs;
},
getDmgFn({ ds, attr, avatar, enemyLv, showDetail = false }) {
let { calc } = ds;
let dmgFn = function (pctNum = 0, talent = false, ele = false, basicNum = 0, mode = "talent") {
let { atk, dmg, phy, cdmg, cpct } = attr;
// 攻击区
let atkNum = calc(atk);
// 倍率独立乘区
let multiNum = attr.multi / 100;
// 增伤区
let dmgNum = (1 + dmg.base / 100 + dmg.plus / 100);
if (ele === "phy") {
dmgNum = (1 + phy.base / 100 + phy.plus / 100);
}
//console.log({ base: Format.comma(dmg.base, 2), plus: Format.comma(dmg.plus, 2) })
let cpctNum = cpct.base / 100 + cpct.plus / 100;
// 爆伤区
let cdmgNum = cdmg.base / 100 + cdmg.plus / 100;
let enemyDef = attr.enemy.def / 100;
let enemyIgnore = attr.enemy.ignore / 100;
let plusNum = 0;
if (talent && attr[talent]) {
pctNum = pctNum / 100;
let ds = attr[talent];
pctNum += ds.pct / 100;
dmgNum += ds.dmg / 100;
cpctNum += ds.cpct / 100;
cdmgNum += ds.cdmg / 100;
enemyDef += ds.def / 100;
enemyIgnore += ds.ignore / 100;
multiNum += ds.multi / 100;
plusNum += ds.plus;
}
// 防御区
let lv = avatar.level;
let defNum = (lv + 100) / ((lv + 100) + (enemyLv + 100) * (1 - enemyDef) * (1 - enemyIgnore));
// 抗性区
let kx = 10 - (attr.kx || 0);
let kNum = 0.9;
if (kx >= 0) {
kNum = (100 - kx) / 100;
} else {
kNum = 1 - kx / 200
}
// 反应区
let eleNum = 1;
if (ele === "phy") {
//do nothing
} else if (ele) {
// todo 更详细
let eleMap = {
'水': { zf: 2 },
'火': { zf: 1.5, rh: 2 },
'冰': { rh: 1.5 }
}
eleNum = (eleMap[attr.element] || {})[ele] || 1;
if (attr[ele]) {
eleNum = eleNum * (1 + attr[ele] / 100);
}
}
cpctNum = Math.max(0, Math.min(1, cpctNum));
if (cpctNum === 0) {
cdmgNum = 0;
}
let ret = {};
if (mode === "basic") {
ret = {
dmg: basicNum * dmgNum * (1 + cdmgNum) * defNum * kNum * eleNum,
avg: basicNum * dmgNum * (1 + cpctNum * cdmgNum) * defNum * kNum * eleNum
}
} else {
// 计算最终伤害
ret = {
dmg: (atkNum * pctNum * (1 + multiNum) + plusNum) * dmgNum * (1 + cdmgNum) * defNum * kNum * eleNum,
avg: (atkNum * pctNum * (1 + multiNum) + plusNum) * dmgNum * (1 + cpctNum * cdmgNum) * defNum * kNum * eleNum
}
}
if (showDetail) {
console.log(attr, { atkNum, pctNum, multiNum, plusNum, dmgNum, cpctNum, cdmgNum, defNum, eleNum, kNum }, ret)
}
return ret;
};
dmgFn.basic = function (basicNum = 0, talent = false, ele = false) {
return dmgFn(0, talent, ele, basicNum, "basic");
}
dmgFn.heal = function (num) {
if (showDetail) {
console.log(num, calc(attr.heal), attr.heal.inc)
}
return {
avg: num * (1 + calc(attr.heal) / 100 + attr.heal.inc / 100)
}
}
dmgFn.shield = function (num) {
return {
avg: num * (calc(attr.shield) / 100) * (attr.shield.inc / 100)
};
}
return dmgFn;
},
async calcData({ profile, char, avatar, talentData, enemyLv = 91, mode = 'profile', dmgIdx = 0 }) {
let charCalcData = await Calc.getCharCalcRule(char.name);
//avatar.element;
if (!charCalcData) {
return false;
}
let talent = Calc.talent(talentData, char);
let meta = {
cons: avatar.actived_constellation_num * 1,
talent
}
let { buffs, details, defParams, mainAttr, defDmgIdx, enemyName } = charCalcData;
defParams = defParams || {};
let originalAttr = Calc.attr(profile, avatar);
let weaponBuffs = await Calc.weapon(avatar.weapon.name);
let reliBuffs = await Calc.reliquaries(avatar.reliquaries);
buffs = lodash.concat(buffs, weaponBuffs, reliBuffs);
lodash.forEach(buffs, (buff) => {
buff.sort = lodash.isUndefined(buff.sort) ? 1 : buff.sort
});
buffs = lodash.sortBy(buffs, ["sort"]);
let { msg } = Calc.calcAttr({ originalAttr, buffs, meta, params: defParams || {} });
let ret = [], detailMap = [], dmgRet = [], dmgDetail = {};
lodash.forEach(details, (detail, detailSysIdx) => {
if (lodash.isFunction(detail)) {
let { attr } = Calc.calcAttr({ originalAttr, buffs, meta });
let ds = lodash.merge({ talent }, Calc.getDs(attr, meta));
detail = detail({ ...ds, attr, avatar });
}
let params = lodash.merge({}, defParams, detail.params || {});
let { attr } = Calc.calcAttr({ originalAttr, buffs, meta, params, talent: detail.talent || "" });
if (detail.check && !detail.check(Calc.getDs(attr, meta, params))) {
return;
}
if (detail.cons && meta.cons * 1 < detail.cons * 1) {
return;
}
let ds = lodash.merge({ talent }, Calc.getDs(attr, meta, params));
let dmg = Calc.getDmgFn({ ds, attr, avatar, enemyLv, showDetail: detail.showDetail }),
basicDmgRet;
if (detail.dmg) {
basicDmgRet = detail.dmg(ds, dmg);
detail.userIdx = detailMap.length;
detailMap.push(detail);
ret.push({
title: detail.title,
...basicDmgRet
})
}
})
if (mode === "dmg") {
let detail;
if (dmgIdx && detailMap[dmgIdx - 1]) {
detail = detailMap[dmgIdx - 1];
} else if (!lodash.isUndefined(defDmgIdx) && details[defDmgIdx]) {
detail = details[defDmgIdx];
} else {
detail = detailMap[0];
}
dmgDetail = {
title: detail.title,
userIdx: detail.userIdx,
basicRet: lodash.merge({}, ret[detail.userIdx]),
attr: []
};
mainAttr = mainAttr.split(",");
let params = lodash.merge({}, defParams, detail.params || {});
let basicDmg = dmgDetail.basicRet;
lodash.forEach(mainAttr, (reduceAttr) => {
dmgDetail.attr.push(attrMap[reduceAttr])
let rowData = [];
lodash.forEach(mainAttr, (incAttr) => {
if (incAttr === reduceAttr) {
rowData.push({ type: 'na' });
return;
}
let { attr } = Calc.calcAttr({
originalAttr,
buffs,
meta,
params,
incAttr,
reduceAttr,
talent: detail.talent || ""
});
let ds = lodash.merge({ talent }, Calc.getDs(attr, meta, params));
let dmg = Calc.getDmgFn({ ds, attr, avatar, enemyLv });
if (detail.dmg) {
let dmgCalcRet = detail.dmg(ds, dmg);
rowData.push({
type: dmgCalcRet.avg === basicDmg.avg ? "avg" : (dmgCalcRet.avg > basicDmg.avg ? "gt" : "lt"),
...dmgCalcRet
})
}
})
dmgRet.push(rowData);
})
}
return {
ret,
msg,
dmgRet,
enemyName,
dmgCfg: dmgDetail
}
}
}
export default Calc;