diff --git a/lib/convert/char.js b/lib/convert/char.js index e3f1732..1c9fc7a 100644 --- a/lib/convert/char.js +++ b/lib/convert/char.js @@ -31,6 +31,14 @@ export const IDToCharSprite = id => { return data?.['sprite_id'] } +/** + * 角色数据 + * @param {string | number} id + */ +export const IDToCharData = id => { + return PartnerId2Data[id] || null +} + /** * @param {string} name * @returns {number | null} diff --git a/lib/score.js b/lib/score.js index 342deaa..3882e96 100644 --- a/lib/score.js +++ b/lib/score.js @@ -1,46 +1,37 @@ import { getMapData } from '../utils/file.js'; -import { charNameToID } from './convert/char.js'; +import { IDToCharData } from './convert/char.js'; import { nameToId } from './convert/property.js'; /** @type {{ [propID: string]: number }} */ export const baseValueData = getMapData('EquipBaseValue'); -const equipScore = getMapData('EquipScore'); -/** @type {{ [charID: string]: { [propID: string]: number } }} */ -export const scoreWeight = {}; + +const elementType2propId = (elementType) => [31503, 31603, 31703, 31803, , 31903][elementType - 200]; /** - * 将权重数据格式化为ID格式权重数据并处理小词条 - * @returns {{ [propID: string]: number }} + * 将权重数据格式化为ID格式权重数据 + * @returns {{ rules?: string[], [propID: string]: number }} */ -export function formatScoreWeight(oriScoreWeight) { +export function formatScoreWeight(oriScoreWeight, charID) { if (!oriScoreWeight) return false; + if (Array.isArray(oriScoreWeight)) return oriScoreWeight; if (typeof oriScoreWeight !== 'object') return false; const weight = {}; for (const propName in oriScoreWeight) { if (!oriScoreWeight[propName] && oriScoreWeight[propName] !== 0) continue; - const propID = +propName || nameToId(propName); + let propID; + if (charID && propName === '属性伤害加成') { + propID = elementType2propId(IDToCharData(charID)?.ElementType); + } else { + propID = +propName || nameToId(propName); + } if (!propID) continue; weight[propID] = oriScoreWeight[propName]; }; - /** 小生命、小攻击、小防御映射为大生命、大攻击、大防御的1/3 */ - for (const [small, big] of [[11103, 11102], [12103, 12102], [13103, 13102]]) { - if (weight[big]) { - weight[small] ??= weight[big] / 3; - }; - }; return weight; } -for (const charName in equipScore) { - // 兼容原ID格式 - const charID = +charName || charNameToID(charName); - if (!charID) - continue; - scoreWeight[charID] = formatScoreWeight(equipScore[charName]); -}; - /** * 获取词条强化次数 * @param {string} propertyID 属性id diff --git a/model/avatar.js b/model/avatar.js index fe76e89..e809c01 100644 --- a/model/avatar.js +++ b/model/avatar.js @@ -4,12 +4,13 @@ import { getSmallSquareAvatar, getSquareAvatar, } from '../lib/download.js'; -import { baseValueData, formatScoreWeight, scoreWeight } from '../lib/score.js'; -import { avatar_calc, scoreFnc } from './damage/avatar.js'; import { idToShortName2 } from '../lib/convert/property.js'; import { imageResourcesPath } from '../lib/path.js'; +import { avatar_calc } from './damage/avatar.js'; +import { baseValueData } from '../lib/score.js'; import { Equip, Weapon } from './equip.js'; import { Property } from './property.js'; +import Score from './damage/Score.js' import { Skill } from './skill.js'; import path from 'path'; import _ from 'lodash'; @@ -262,9 +263,9 @@ export class ZZZAvatarInfo { this.isNew = isNew; /** @type {number} 等级级别(取十位数字)*/ this.level_rank = Math.floor(this.level / 10); - const weight = scoreFnc[this.id] && scoreFnc[this.id](this); - this.weightRule = weight?.[0] || '默认'; - this.scoreWeight = _.defaults(formatScoreWeight(weight?.[1]) || {}, scoreWeight[this.id]); + const weight = Score.getFinalWeight(this); + this.weightRule = weight[0]; + this.scoreWeight = weight[1]; for (const equip of this.equip) { equip.get_score(this.scoreWeight); } diff --git a/model/damage/README.md b/model/damage/README.md index 1ffe862..3ab4730 100644 --- a/model/damage/README.md +++ b/model/damage/README.md @@ -14,7 +14,7 @@ 5. 保存并重启 -示例图: +示例图(“冰属性伤害加成”可简写为“属性伤害加成”):
@@ -24,7 +24,31 @@
> 将崽底层日志模式切换为**debug**模式,可在控制台查看评分计算详细过程;且会自动监听现有评分计算文件实时热更新。可按需开启
-在函数体中,可根据玩家角色数据**动态选用**不同的权重方案。参考[爱丽丝评分规则](./character/爱丽丝/score.js)
+在函数体中,可根据玩家角色数据**动态选用**不同的权重方案。示例(原爱丽丝直伤流规则):
+
+```js
+/** @type {import('../../avatar.ts')['scoreFnc'][string]} */
+export default function (avatar) {
+ const { CRITRate, CRITDMG, AnomalyProficiency } = avatar.initial_properties
+ // (暴击率 * 2 + 爆伤 >= 200%) 且 (异常精通 < 300) 时转为直伤流规则
+ if (CRITRate * 2 + CRITDMG >= 2 && AnomalyProficiency < 300) {
+ return ['直伤流', {
+ "生命值百分比": 0,
+ "攻击力百分比": 0.75,
+ "防御力百分比": 0,
+ "冲击力": 0,
+ "暴击率": 1,
+ "暴击伤害": 1,
+ "穿透率": 1,
+ "穿透值": 0.25,
+ "能量自动回复": 0,
+ "异常精通": 0.5,
+ "异常掌控": 0,
+ "属性伤害加成": 1
+ }]
+ }
+}
+```
- 函数参数:[ZZZAvatarInfo](../avatar.js#L173)(角色数据)
@@ -38,7 +62,9 @@
> 注意:直接修改插件所属文件,将会导致后续该文件更新冲突。若你不清楚如何解决冲突,请使用[方法一](#方法一预设方法推荐)
-打开插件默认词条权重文件直接修改相应权重保存即可,重启生效
+默认权重使用**权重模板**规则编写,详见[预定义权重规则](./Score.ts#L208):选择第一个符合条件的规则,若皆不符合则选择第一个有效规则
+
+打开插件默认词条权重文件直接修改相应数据保存即可,重启生效
文件路径:[ZZZ-plugin/resources/map/EquipScore.json](../../resources/map/EquipScore.json)
diff --git a/model/damage/Score.js b/model/damage/Score.js
index f0669aa..f83c08c 100644
--- a/model/damage/Score.js
+++ b/model/damage/Score.js
@@ -1,12 +1,25 @@
+import { baseValueData, formatScoreWeight } from '../../lib/score.js';
import { idToName } from '../../lib/convert/property.js';
-import { baseValueData } from '../../lib/score.js';
+import { aliasToID } from '../../lib/convert/char.js';
import { getMapData } from '../../utils/file.js';
+import { scoreFnc } from './avatar.js';
var rarity;
(function (rarity) {
rarity[rarity["S"] = 0] = "S";
rarity[rarity["A"] = 1] = "A";
rarity[rarity["B"] = 2] = "B";
})(rarity || (rarity = {}));
+const equipScore = getMapData('EquipScore');
+for (const charName in equipScore) {
+ const charID = +charName || aliasToID(charName);
+ if (!charID) {
+ logger.warn(`驱动盘评分:未找到角色${charName}的角色ID`);
+ delete equipScore[charName];
+ continue;
+ }
+ equipScore[charID] = equipScore[charName];
+ delete equipScore[charName];
+}
const mainStats = getMapData('EquipMainStats');
const subStats = Object.keys(baseValueData).map(Number);
export default class Score {
@@ -92,4 +105,250 @@ export default class Score {
return 0;
}
}
+ static getFinalWeight(avatar) {
+ let def_weight = equipScore[avatar.id];
+ if (!def_weight && !scoreFnc[avatar.id]) {
+ switch (avatar.avatar_profession) {
+ case 1:
+ def_weight = ['主C·双爆'];
+ break;
+ case 2:
+ def_weight = ['冲击·双爆', '冲击·攻击'];
+ break;
+ case 3:
+ def_weight = ['主C·异常', '辅助·异常'];
+ break;
+ case 4:
+ case 5:
+ def_weight = ['辅助·双爆', '辅助·异常'];
+ break;
+ case 6:
+ def_weight = ['命破·双爆'];
+ break;
+ }
+ }
+ const delRules = (rules) => {
+ if (rules.length === 1) {
+ rule_name = rules[0];
+ final_weight = predefinedWeights[rules[0]]?.value;
+ }
+ else {
+ for (const name of rules) {
+ if (predefinedWeights[name]?.rule(avatar)) {
+ rule_name = name;
+ final_weight = predefinedWeights[name].value;
+ break;
+ }
+ }
+ if (!final_weight) {
+ for (const name of rules) {
+ if (predefinedWeights[name]) {
+ rule_name = name;
+ final_weight = predefinedWeights[name].value;
+ break;
+ }
+ }
+ }
+ }
+ final_weight = { ...final_weight };
+ };
+ let rule_name = '默认', final_weight;
+ if (Array.isArray(def_weight)) {
+ delRules(def_weight);
+ }
+ else if (def_weight.rules) {
+ const { rules, ...rest } = def_weight;
+ delRules(rules);
+ if (Object.keys(rest).length) {
+ rule_name += '·改';
+ Object.assign(final_weight, rest);
+ }
+ }
+ else {
+ final_weight = def_weight;
+ }
+ final_weight = formatScoreWeight(final_weight, avatar.id);
+ const calc_weight = scoreFnc[avatar.id] && scoreFnc[avatar.id](avatar);
+ if (calc_weight) {
+ rule_name = calc_weight[0];
+ final_weight = { ...final_weight, ...formatScoreWeight(calc_weight[1], avatar.id) };
+ }
+ for (const [small, big, name] of [[11103, 11102, 'HP'], [12103, 12102, 'ATK'], [13103, 13102, 'DEF']]) {
+ if (final_weight[big]) {
+ final_weight[small] ??= +(baseValueData[small] * 100 / (baseValueData[big] * avatar.base_properties[name]) * final_weight[big]).toFixed(2);
+ }
+ }
+ return [rule_name, final_weight];
+ }
}
+const predefinedWeights = {
+ 主C·双爆: {
+ rule: (avatar) => {
+ const { ATK, CRITRate, CRITDMG, AnomalyMastery, AnomalyProficiency } = avatar.initial_properties;
+ return ATK > 2400 && CRITRate * 2 + CRITDMG >= 2.2 && AnomalyMastery < 150 && AnomalyProficiency < 200;
+ },
+ value: {
+ "生命值百分比": 0,
+ "攻击力百分比": 0.75,
+ "防御力百分比": 0,
+ "冲击力": 0,
+ "暴击率": 1,
+ "暴击伤害": 1,
+ "穿透率": 1,
+ "穿透值": 0.25,
+ "能量自动回复": 0,
+ "异常精通": 0,
+ "异常掌控": 0,
+ "属性伤害加成": 1
+ }
+ },
+ 主C·异常: {
+ rule: (avatar) => {
+ const { ATK, CRITRate, CRITDMG, AnomalyMastery, AnomalyProficiency } = avatar.initial_properties;
+ if (CRITRate * 2 + CRITDMG >= 2)
+ return false;
+ if (ATK < 2400)
+ return false;
+ if (AnomalyMastery >= 180 && AnomalyProficiency >= 200)
+ return true;
+ if (AnomalyMastery >= 120 && AnomalyProficiency >= 300)
+ return true;
+ if (AnomalyMastery >= 150 && AnomalyProficiency >= 250)
+ return true;
+ return false;
+ },
+ value: {
+ "生命值百分比": 0,
+ "攻击力百分比": 0.75,
+ "防御力百分比": 0,
+ "冲击力": 0,
+ "暴击率": 0,
+ "暴击伤害": 0,
+ "穿透率": 1,
+ "穿透值": 0.25,
+ "能量自动回复": 0,
+ "异常精通": 1,
+ "异常掌控": 1,
+ "属性伤害加成": 1
+ }
+ },
+ 命破·双爆: {
+ rule: (avatar) => {
+ return true;
+ },
+ value: {
+ "生命值百分比": 0,
+ "攻击力百分比": 0.25,
+ "防御力百分比": 0,
+ "冲击力": 0,
+ "暴击率": 1,
+ "暴击伤害": 1,
+ "穿透率": 0,
+ "穿透值": 0,
+ "能量自动回复": 0,
+ "异常精通": 0,
+ "异常掌控": 0,
+ "属性伤害加成": 1
+ }
+ },
+ 辅助·双爆: {
+ rule: (avatar) => {
+ const { CRITRate, CRITDMG, AnomalyProficiency } = avatar.initial_properties;
+ return CRITRate * 2 + CRITDMG >= 1.5 && AnomalyProficiency < 200;
+ },
+ value: {
+ "生命值百分比": 0,
+ "攻击力百分比": 0.75,
+ "防御力百分比": 0,
+ "冲击力": 0,
+ "暴击率": 1,
+ "暴击伤害": 1,
+ "穿透率": 0.75,
+ "穿透值": 0.25,
+ "能量自动回复": 1,
+ "异常精通": 0,
+ "异常掌控": 0,
+ "属性伤害加成": 1
+ }
+ },
+ 辅助·攻击: {
+ rule: (avatar) => {
+ const { CRITRate, CRITDMG } = avatar.initial_properties;
+ return CRITRate * 2 + CRITDMG >= 1.5;
+ },
+ value: {
+ "生命值百分比": 0,
+ "攻击力百分比": 1,
+ "防御力百分比": 0,
+ "冲击力": 0,
+ "暴击率": 1,
+ "暴击伤害": 0.75,
+ "穿透率": 0.75,
+ "穿透值": 0.25,
+ "能量自动回复": 1,
+ "异常精通": 0,
+ "异常掌控": 0,
+ "属性伤害加成": 1
+ }
+ },
+ 辅助·异常: {
+ rule: (avatar) => {
+ const { CRITRate, CRITDMG, AnomalyProficiency } = avatar.initial_properties;
+ return CRITRate * 2 + CRITDMG < 2 && AnomalyProficiency >= 200;
+ },
+ value: {
+ "生命值百分比": 0,
+ "攻击力百分比": 0.75,
+ "防御力百分比": 0,
+ "冲击力": 0,
+ "暴击率": 0,
+ "暴击伤害": 0,
+ "穿透率": 0.75,
+ "穿透值": 0.25,
+ "能量自动回复": 1,
+ "异常精通": 1,
+ "异常掌控": 1,
+ "属性伤害加成": 1
+ }
+ },
+ 冲击·双爆: {
+ rule: (avatar) => {
+ const { CRITRate, CRITDMG } = avatar.initial_properties;
+ return CRITRate * 2 + CRITDMG >= 1.5;
+ },
+ value: {
+ "生命值百分比": 0,
+ "攻击力百分比": 0.75,
+ "防御力百分比": 0,
+ "冲击力": 1,
+ "暴击率": 1,
+ "暴击伤害": 1,
+ "穿透率": 0.75,
+ "穿透值": 0.25,
+ "能量自动回复": 0,
+ "异常精通": 0,
+ "异常掌控": 0,
+ "属性伤害加成": 1
+ }
+ },
+ 冲击·攻击: {
+ rule: (avatar) => {
+ const { ATK, CRITRate, CRITDMG } = avatar.initial_properties;
+ return ATK > 2000 && CRITRate * 2 + CRITDMG >= 1;
+ },
+ value: {
+ "生命值百分比": 0,
+ "攻击力百分比": 1,
+ "防御力百分比": 0,
+ "冲击力": 1,
+ "暴击率": 1,
+ "暴击伤害": 0.75,
+ "穿透率": 0.75,
+ "穿透值": 0.25,
+ "能量自动回复": 0,
+ "异常精通": 0,
+ "异常掌控": 0,
+ "属性伤害加成": 1
+ }
+ },
+};
diff --git a/model/damage/Score.ts b/model/damage/Score.ts
index e7eeb56..55687c9 100644
--- a/model/damage/Score.ts
+++ b/model/damage/Score.ts
@@ -1,11 +1,27 @@
+import type { ZZZAvatarInfo } from '../avatar.js'
import type { Equip } from '../equip.js'
-import type { scoreWeight } from '../../lib/score.js'
+import { baseValueData, formatScoreWeight } from '../../lib/score.js'
import { idToName } from '../../lib/convert/property.js'
-import { baseValueData } from '../../lib/score.js'
+import { aliasToID } from '../../lib/convert/char.js'
import { getMapData } from '../../utils/file.js'
+import { scoreFnc } from './avatar.js'
enum rarity { S, A, B }
+type Weight = { [propID: string]: number }
+
+//@ts-expect-error
+const equipScore = getMapData('EquipScore') as { [charID: string]: string[] | { rules?: string[], [propID: string]: number } }
+for (const charName in equipScore) {
+ const charID = +charName || aliasToID(charName)
+ if (!charID) {
+ logger.warn(`驱动盘评分:未找到角色${charName}的角色ID`)
+ delete equipScore[charName]
+ continue
+ }
+ equipScore[charID] = equipScore[charName]
+ delete equipScore[charName]
+}
/** 主词条可能属性 */
const mainStats = getMapData('EquipMainStats') as { [partition: string]: number[] }
/** 副词条可能属性 */
@@ -14,12 +30,12 @@ const subStats = Object.keys(baseValueData).map(Number)
export default class Score {
protected equip: Equip
/** 词条权重 */
- protected weight: typeof scoreWeight[string]
+ protected weight: Weight
/** 驱动盘n号位 */
protected partition: number
/** 用户主词条 */
protected userMainStat: number
- constructor(equip: Equip, weight: Score['weight']) {
+ constructor(equip: Equip, weight: Weight) {
this.equip = equip
this.weight = weight
this.partition = this.equip.equipment_type
@@ -101,7 +117,7 @@ export default class Score {
return score
}
- static main(equip: Equip, weight: Score['weight']) {
+ static main(equip: Equip, weight: Weight) {
try {
return new Score(equip, weight).get_score()
} catch (err) {
@@ -110,4 +126,253 @@ export default class Score {
}
}
+ static getFinalWeight(avatar: ZZZAvatarInfo): [name: string, Weight] {
+ let def_weight = equipScore[avatar.id]
+ // 无预设权重且无计算函数(新角色),选择相应基本规则
+ if (!def_weight && !scoreFnc[avatar.id]) {
+ switch (avatar.avatar_profession) {
+ case 1: // 强攻
+ def_weight = ['主C·双爆']
+ break
+ case 2: // 击破
+ def_weight = ['冲击·双爆', '冲击·攻击']
+ break
+ case 3: // 异常
+ def_weight = ['主C·异常', '辅助·异常']
+ break
+ case 4: // 支援
+ case 5: // 防护
+ def_weight = ['辅助·双爆', '辅助·异常']
+ break
+ case 6: // 命破
+ def_weight = ['命破·双爆']
+ break
+ }
+ }
+ /** 选择第一个符合条件的规则,若皆不符合则选择第一个有效规则 */
+ const delRules = (rules: string[]) => {
+ if (rules.length === 1) {
+ rule_name = rules[0]
+ final_weight = predefinedWeights[rules[0]]?.value
+ } else {
+ for (const name of rules) {
+ if (predefinedWeights[name]?.rule(avatar)) {
+ rule_name = name
+ final_weight = predefinedWeights[name].value
+ break
+ }
+ }
+ if (!final_weight) {
+ for (const name of rules) {
+ if (predefinedWeights[name]) {
+ rule_name = name
+ final_weight = predefinedWeights[name].value
+ break
+ }
+ }
+ }
+ }
+ final_weight = { ...final_weight }
+ }
+ let rule_name = '默认', final_weight: Weight | undefined
+ if (Array.isArray(def_weight)) {
+ delRules(def_weight)
+ } else if (def_weight.rules) {
+ const { rules, ...rest } = def_weight
+ delRules(rules)
+ if (Object.keys(rest).length) {
+ rule_name += '·改'
+ Object.assign(final_weight!, rest)
+ }
+ } else {
+ final_weight = def_weight
+ }
+ // console.log(avatar.name_mi18n, 'default_final_weight', final_weight)
+ final_weight = formatScoreWeight(final_weight, avatar.id)
+ const calc_weight = scoreFnc[avatar.id] && scoreFnc[avatar.id](avatar)
+ if (calc_weight) {
+ rule_name = calc_weight[0]
+ final_weight = { ...final_weight, ...formatScoreWeight(calc_weight[1], avatar.id) }
+ }
+ // 小生命、小攻击、小防御动态映射为大生命、大攻击、大防御相对于基础属性的等效权重
+ for (const [small, big, name] of [[11103, 11102, 'HP'], [12103, 12102, 'ATK'], [13103, 13102, 'DEF']] as const) {
+ if (final_weight[big]) {
+ final_weight[small] ??= +(baseValueData[small] * 100 / (baseValueData[big] * avatar.base_properties[name]) * final_weight[big]).toFixed(2)
+ }
+ }
+ // console.log(avatar.name_mi18n, rule_name, final_weight)
+ return [rule_name, final_weight]
+ }
+
}
+
+/** 预定义权重规则 */
+const predefinedWeights: Record