支持自定义评分规则、支持动态选用、更新相关文档

This commit is contained in:
UCPr 2025-03-27 01:31:46 +08:00
parent 06ec5152a0
commit 504d2792a8
11 changed files with 217 additions and 114 deletions

View file

@ -4,12 +4,12 @@ import {
getSmallSquareAvatar,
getSquareAvatar,
} from '../lib/download.js';
import { formatScoreWeight, scoreWeight } from '../lib/score.js';
import { avatar_ability, scoreFnc } from './damage/avatar.js';
import { imageResourcesPath } from '../lib/path.js';
import { Equip, Weapon } from './equip.js';
import { Property } from './property.js';
import { Skill } from './skill.js';
import { avatar_ability } from './damage/avatar.js';
import { hasScoreData, scoreData } from '../lib/score.js';
import _ from 'lodash';
import fs from 'fs';
@ -262,8 +262,9 @@ export class ZZZAvatarInfo {
this.isNew = isNew;
/** @type {number} 等级级别(取十位数字)*/
this.level_rank = Math.floor(this.level / 10);
this.scoreWeight = formatScoreWeight(scoreFnc[this.id] && scoreFnc[this.id](this)?.[1]) || scoreWeight[this.id];
for (const equip of this.equip) {
equip.get_score(this.id);
equip.get_score(this.scoreWeight);
}
}
@ -356,7 +357,7 @@ export class ZZZAvatarInfo {
/** @type {number|boolean} */
get equip_score() {
if (hasScoreData(this.id)) {
if (this.scoreWeight) {
let score = 0;
for (const equip of this.equip) {
score += equip.score;
@ -435,7 +436,7 @@ export class ZZZAvatarInfo {
/** 面板属性label效果 */
get_label(propID) {
const base = scoreData[this.id][propID];
const base = this.scoreWeight[propID];
if (!base) return '';
return base === 1 ? 'yellow' :
base >= 0.75 ? 'blue' :

View file

@ -1,3 +1,51 @@
# 词条权重自定义
## 方法一:预设方法(推荐)
### 基础步骤
1. 复制模板文件:[ZZZ-plugin/model/damage/character/模板/score.js](./character/模板/score.js)
2. 进入角色数据文件夹:[ZZZ-plugin/model/damage/character/角色名](./character/)
3. 粘贴文件,并将粘贴的模板文件重命名为**score_user.js**
4. 打开**score_user.js**,根据需要调整权重数值
5. 保存并重启
示例图:
<p align="center">
<img width=800 src="https://s2.loli.net/2025/03/27/OcuIPDyE5sHSJZw.jpg" title="词条权重自定义基础步骤">
</p>
### 进阶操作
> 将崽底层日志模式切换为**debug**模式,可在控制台查看评分计算详细过程;且会自动监听现有评分计算文件实时热更新。可按需开启
在函数体中,可根据玩家角色数据**动态选用**不同的权重方案
- 函数参数:[ZZZAvatarInfo](../avatar.js#L173)(角色数据)
- 函数返回值:
- 元组:[评分规则名, 权重数据]
- 类型:**[string, { [词条名: string]: number }]**
- 若返回其他类型,会自动选择默认评分规则
## 方法二:直接修改默认权重
> 注意:直接修改插件所属文件,将会导致后续该文件更新冲突。若你不清楚如何解决冲突,请使用[方法一](#方法一预设方法推荐)
打开插件默认词条权重文件直接修改相应权重保存即可,重启生效
文件路径:[ZZZ-plugin/resources/map/EquipScore.json](../../resources/map/EquipScore.json)
## 鸣谢
感谢**银狐**对评分计算规则的指导建议
[点此查看](./Score.ts)评分计算规则源码如果有任何对评分计算的想法建议欢迎与我联系ucpr251@gmail.com
# 伤害计算自定义
@ -7,7 +55,7 @@
后文将带你入门插件伤害计算逻辑,只要你需要自定义伤害计算,都建议你完整阅读:
> 底层已对伤害计算进行了规范化、模块化,只需要填参数就可以实现常见的伤害计算逻辑,即使不懂代码也可以参考模板独立完成。若存在问题、建议,可于插件群内询问,或联系我的邮箱UCPr251@gmail.com
> 底层已对伤害计算进行了规范化、模块化,只需要填参数就可以实现常见的伤害计算逻辑,即使不懂代码也可以参考模板独立完成。若存在问题、建议,可于插件群内询问,或与我沟通ucpr251@gmail.com
伤害计算需要明确这几部分:**初始属性**、**局内Buff**、**技能属性**、**敌方属性**,后文将分别说明
@ -231,6 +279,7 @@ Buff来源可分为三大类武器、套装、角色影画、核心被动
> - 追加攻击
> - 直接以“追加攻击”命名
> - 追加攻击不被认为是一种新的输出手段(技能),它更类似于原直伤输出的一个特殊属性,使该伤害能同时享受对其**原所属技能类型**的增益和**仅对追加攻击生效**的增益,故一般不将其单独作为技能类型而是在技能属性的重定向参数中添加**追加攻击**参数,请参考[技能重定向说明注意事项](#技能类型命名对Buff作用的影响)
#### 技能类型命名解释说明
@ -281,6 +330,7 @@ buff作用范围将以技能类型命名为依据向后覆盖。以上述[艾莲
- 对于`“X"(造成的伤害)被视为“Y”(伤害)`此类特殊技能,需要指定技能**重定向参数**同时上述buff覆盖规则会发生变化具体请参考[源码内描述](./Calculator.ts#L22)
> 需要注意的是:即使出现`“X"(造成的伤害)被视为“Y”(伤害)`,对**Y**类型的增益**X**不一定能吃到,视具体情况变化
> 对于被视为**追加攻击**伤害的技能,其重定向参数一般为包含其**原技能类型**和**追加攻击**的数组,因为它可以享受到分别对两者生效的不同增益(以游戏内具体情况为准),参考[**零号·安比**伤害计算文件](./character/零号·安比/calc.js#L51)
### 技能倍率

View file

@ -1,5 +1,5 @@
import { baseValueData, scoreData } from '../../lib/score.js';
import { idToName } from '../../lib/convert/property.js';
import { baseValueData } from '../../lib/score.js';
import { getMapData } from '../../utils/file.js';
var rarity;
(function (rarity) {
@ -10,13 +10,13 @@ var rarity;
const mainStats = getMapData('EquipMainStats');
const subStats = Object.keys(baseValueData).map(Number);
export default class Score {
scoreData;
equip;
weight;
partition;
userMainStat;
constructor(charID, equip) {
this.scoreData = scoreData[charID];
constructor(equip, weight) {
this.equip = equip;
this.weight = weight;
this.partition = this.equip.equipment_type;
this.userMainStat = this.equip.main_properties[0].property_id;
}
@ -37,13 +37,13 @@ export default class Score {
}
get_max_count() {
const subMaxStats = subStats
.filter(p => p !== this.userMainStat && this.scoreData[p])
.sort((a, b) => this.scoreData[b] - this.scoreData[a]).slice(0, 4);
.filter(p => p !== this.userMainStat && this.weight[p])
.sort((a, b) => this.weight[b] - this.weight[a]).slice(0, 4);
if (!subMaxStats.length)
return 0;
logger.debug(`[${this.partition}号位]理论副词条:` + subMaxStats.map(idToName).reduce((a, p, i) => a + `${p}*${this.scoreData[subMaxStats[i]].toFixed(2)} `, ''));
let count = this.scoreData[subMaxStats[0]] * 6;
subMaxStats.slice(1).forEach(p => count += this.scoreData[p] || 0);
logger.debug(`[${this.partition}号位]理论副词条:` + subMaxStats.map(idToName).reduce((a, p, i) => a + `${p}*${this.weight[subMaxStats[i]].toFixed(2)} `, ''));
let count = this.weight[subMaxStats[0]] * 6;
subMaxStats.slice(1).forEach(p => count += this.weight[p] || 0);
logger.debug(`[${this.partition}号位]理论词条数:${logger.blue(count)}`);
return count;
}
@ -51,7 +51,7 @@ export default class Score {
let count = 0;
for (const prop of this.equip.properties) {
const propID = prop.property_id;
const weight = this.scoreData[propID];
const weight = this.weight[propID];
if (weight) {
logger.debug(`[${this.partition}号位]实际副词条:${idToName(propID)} ${logger.green(prop.count + 1)}*${weight}`);
count += weight * (prop.count + 1);
@ -75,17 +75,17 @@ export default class Score {
return score;
}
const mainMaxStat = mainStats[this.partition]
.filter(p => this.scoreData[p])
.sort((a, b) => this.scoreData[b] - this.scoreData[a])[0];
const mainScore = (mainMaxStat ? 12 * (this.scoreData[this.userMainStat] || 0) / this.scoreData[mainMaxStat] : 12) * this.get_level_multiplier();
.filter(p => this.weight[p])
.sort((a, b) => this.weight[b] - this.weight[a])[0];
const mainScore = (mainMaxStat ? 12 * (this.weight[this.userMainStat] || 0) / this.weight[mainMaxStat] : 12) * this.get_level_multiplier();
const subScore = actual_count / max_count * 43;
const score = (mainScore + subScore) * rarity_multiplier;
logger.debug(`[${this.partition}号位] ${logger.magenta(`(${mainScore} + ${subScore}) * ${rarity_multiplier} = ${score}`)}`);
return score;
}
static main(charID, equip) {
static main(equip, weight) {
try {
return new Score(charID, equip).get_score();
return new Score(equip, weight).get_score();
}
catch (err) {
logger.error('角色驱动盘评分计算错误:', err);

View file

@ -1,6 +1,7 @@
import type { Equip } from '../equip.js'
import { baseValueData, scoreData } from '../../lib/score.js'
import type { scoreWeight } from '../../lib/score.js'
import { idToName } from '../../lib/convert/property.js'
import { baseValueData } from '../../lib/score.js'
import { getMapData } from '../../utils/file.js'
enum rarity { S, A, B }
@ -11,15 +12,16 @@ const mainStats = getMapData('EquipMainStats') as { [partition: string]: number[
const subStats = Object.keys(baseValueData).map(Number)
export default class Score {
protected scoreData: typeof scoreData[string]
protected equip: Equip
/** 词条权重 */
protected weight: typeof scoreWeight[string]
/** 驱动盘n号位 */
protected partition: number
/** 用户主词条 */
protected userMainStat: number
constructor(charID: string, equip: Equip) {
this.scoreData = scoreData[charID]
constructor(equip: Equip, weight: Score['weight']) {
this.equip = equip
this.weight = weight
this.partition = this.equip.equipment_type
this.userMainStat = this.equip.main_properties[0].property_id
}
@ -47,12 +49,12 @@ export default class Score {
get_max_count() {
/** 权重最大的4个副词条 */
const subMaxStats = subStats
.filter(p => p !== this.userMainStat && this.scoreData[p])
.sort((a, b) => this.scoreData[b] - this.scoreData[a]).slice(0, 4)
.filter(p => p !== this.userMainStat && this.weight[p])
.sort((a, b) => this.weight[b] - this.weight[a]).slice(0, 4)
if (!subMaxStats.length) return 0
logger.debug(`[${this.partition}号位]理论副词条:` + subMaxStats.map(idToName).reduce((a, p, i) => a + `${p}*${this.scoreData[subMaxStats[i]].toFixed(2)} `, ''))
let count = this.scoreData[subMaxStats[0]] * 6 // 权重最大副词条强化五次
subMaxStats.slice(1).forEach(p => count += this.scoreData[p] || 0) // 其他词条各计入一次
logger.debug(`[${this.partition}号位]理论副词条:` + subMaxStats.map(idToName).reduce((a, p, i) => a + `${p}*${this.weight[subMaxStats[i]].toFixed(2)} `, ''))
let count = this.weight[subMaxStats[0]] * 6 // 权重最大副词条强化五次
subMaxStats.slice(1).forEach(p => count += this.weight[p] || 0) // 其他词条各计入一次
logger.debug(`[${this.partition}号位]理论词条数:${logger.blue(count)}`)
return count
}
@ -62,7 +64,7 @@ export default class Score {
let count = 0
for (const prop of this.equip.properties) {
const propID = prop.property_id
const weight = this.scoreData[propID]
const weight = this.weight[propID]
if (weight) {
logger.debug(`[${this.partition}号位]实际副词条:${idToName(propID)} ${logger.green(prop.count + 1)}*${weight}`)
count += weight * (prop.count + 1)
@ -90,18 +92,18 @@ export default class Score {
}
// 456号位
const mainMaxStat = mainStats[this.partition]
.filter(p => this.scoreData[p])
.sort((a, b) => this.scoreData[b] - this.scoreData[a])[0]
const mainScore = (mainMaxStat ? 12 * (this.scoreData[this.userMainStat] || 0) / this.scoreData[mainMaxStat] : 12) * this.get_level_multiplier()
.filter(p => this.weight[p])
.sort((a, b) => this.weight[b] - this.weight[a])[0]
const mainScore = (mainMaxStat ? 12 * (this.weight[this.userMainStat] || 0) / this.weight[mainMaxStat] : 12) * this.get_level_multiplier()
const subScore = actual_count / max_count * 43
const score = (mainScore + subScore) * rarity_multiplier
logger.debug(`[${this.partition}号位] ${logger.magenta(`(${mainScore} + ${subScore}) * ${rarity_multiplier} = ${score}`)}`)
return score
}
static main(charID: string, equip: Equip) {
static main(equip: Equip, weight: Score['weight']) {
try {
return new Score(charID, equip).get_score()
return new Score(equip, weight).get_score()
} catch (err) {
logger.error('角色驱动盘评分计算错误:', err)
return 0

View file

@ -7,11 +7,12 @@ import chokidar from 'chokidar';
import path from 'path';
import fs from 'fs';
const damagePath = path.join(pluginPath, 'model', 'damage');
export const charData = {};
export const charData = Object.create(null);
export const scoreFnc = Object.create(null);
const calcFnc = {
character: {},
weapon: {},
set: {}
character: Object.create(null),
weapon: Object.create(null),
set: Object.create(null)
};
async function init() {
const isWatch = await (async () => {
@ -33,7 +34,7 @@ function watchFile(path, fnc) {
return;
const watcher = chokidar.watch(path, {
awaitWriteFinish: {
stabilityThreshold: 50
stabilityThreshold: 251
}
});
watcher.on('change', (path) => {
@ -46,24 +47,42 @@ async function importChar(charName, isWatch = false) {
if (!id)
return logger.warn(`未找到角色${charName}的ID`);
const dir = path.join(damagePath, 'character', charName);
const calcFile = fs.existsSync(path.join(dir, 'calc_user.js')) ? 'calc_user.js' : 'calc.js';
const dataPath = path.join(dir, (fs.existsSync(path.join(dir, 'data_user.json')) ? 'data_user.json' : 'data.json'));
const getFileName = (name, ext) => fs.existsSync(path.join(dir, `${name}_user${ext}`)) ? `${name}_user${ext}` : `${name}${ext}`;
const dataPath = path.join(dir, getFileName('data', '.json'));
const calcFile = getFileName('calc', '.js');
const scoreFile = getFileName('score', '.js');
try {
const loadCharData = () => charData[id] = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
const calcFilePath = path.join(dir, calcFile);
const loadCalcJS = async () => {
if (!fs.existsSync(calcFilePath))
return;
const m = await import(`./character/${charName}/${calcFile}?${Date.now()}`);
if (!m.calc && (!m.buffs || !m.skills))
throw new Error('伤害计算文件格式错误:' + charName);
calcFnc.character[id] = m;
};
const scoreFilePath = path.join(dir, scoreFile);
const loadScoreJS = async () => {
if (!fs.existsSync(scoreFilePath))
return;
const m = await import(`./character/${charName}/${scoreFile}?${Date.now()}`);
const fnc = m.default;
if (!fnc || typeof fnc !== 'function')
throw new Error('评分权重文件格式错误:' + charName);
scoreFnc[id] = fnc;
};
if (isWatch) {
watchFile(calcFilePath, () => importChar(charName));
watchFile(dataPath, () => charData[id] = JSON.parse(fs.readFileSync(dataPath, 'utf8')));
watchFile(dataPath, loadCharData);
watchFile(calcFilePath, loadCalcJS);
watchFile(scoreFilePath, loadScoreJS);
}
charData[id] = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
if (!fs.existsSync(calcFilePath))
return;
const m = await import(`./character/${charName}/${calcFile}?${Date.now()}`);
if (!m.calc && (!m.buffs || !m.skills))
throw new Error('伤害计算文件格式错误');
calcFnc.character[id] = m;
loadCharData();
await loadCalcJS();
await loadScoreJS();
}
catch (e) {
logger.error(`导入角色${charName}伤害计算错误:`, e);
logger.error(`导入角色${charName}计算文件错误:`, e);
}
}
async function importFile(type, name, isWatch = false) {

View file

@ -12,15 +12,19 @@ import fs from 'fs'
const damagePath = path.join(pluginPath, 'model', 'damage')
export const charData: {
[id: number]: {
[id: string]: {
skill: { [skillName: string]: number[] }
buff: { [buffName: string]: number[] }
}
} = {}
} = Object.create(null)
export const scoreFnc: {
[charID: string]: (charData: ZZZAvatarInfo) => [string, { [propID: string]: number }] | undefined
} = Object.create(null)
const calcFnc: {
character: {
[id: number]: {
[charID: string]: {
calc?: (buffM: BuffManager, calc: Calculator, avatar: ZZZAvatarInfo) => void
buffs: buff[]
skills: skill[]
@ -39,9 +43,9 @@ const calcFnc: {
}
}
} = {
character: {},
weapon: {},
set: {}
character: Object.create(null),
weapon: Object.create(null),
set: Object.create(null)
}
async function init() {
@ -54,10 +58,10 @@ async function init() {
}
})()
await Promise.all(fs.readdirSync(path.join(damagePath, 'character')).filter(v => v !== '模板').map(v => importChar(v, isWatch)))
for (const type of ['weapon', 'set']) {
for (const type of ['weapon', 'set'] as const) {
await Promise.all(
fs.readdirSync(path.join(damagePath, type)).filter(v => v !== '模板.js' && !v.endsWith('_user.js') && v.endsWith('.js'))
.map(v => importFile(type as 'weapon' | 'set', v.replace('.js', ''), isWatch))
.map(v => importFile(type, v.replace('.js', ''), isWatch))
)
}
}
@ -66,7 +70,7 @@ function watchFile(path: string, fnc: () => void) {
if (!fs.existsSync(path)) return
const watcher = chokidar.watch(path, {
awaitWriteFinish: {
stabilityThreshold: 50
stabilityThreshold: 251
}
})
watcher.on('change', (path) => {
@ -79,21 +83,38 @@ async function importChar(charName: string, isWatch = false) {
const id = aliasToID(charName)
if (!id) return logger.warn(`未找到角色${charName}的ID`)
const dir = path.join(damagePath, 'character', charName)
const calcFile = fs.existsSync(path.join(dir, 'calc_user.js')) ? 'calc_user.js' : 'calc.js'
const dataPath = path.join(dir, (fs.existsSync(path.join(dir, 'data_user.json')) ? 'data_user.json' : 'data.json'))
const getFileName = (name: string, ext: '.js' | '.json') =>
fs.existsSync(path.join(dir, `${name}_user${ext}`)) ? `${name}_user${ext}` : `${name}${ext}`
const dataPath = path.join(dir, getFileName('data', '.json'))
const calcFile = getFileName('calc', '.js')
const scoreFile = getFileName('score', '.js')
try {
const loadCharData = () => charData[id] = JSON.parse(fs.readFileSync(dataPath, 'utf8'))
const calcFilePath = path.join(dir, calcFile)
if (isWatch) {
watchFile(calcFilePath, () => importChar(charName))
watchFile(dataPath, () => charData[id] = JSON.parse(fs.readFileSync(dataPath, 'utf8')))
const loadCalcJS = async () => {
if (!fs.existsSync(calcFilePath)) return
const m = await import(`./character/${charName}/${calcFile}?${Date.now()}`)
if (!m.calc && (!m.buffs || !m.skills)) throw new Error('伤害计算文件格式错误:' + charName)
calcFnc.character[id] = m
}
charData[id] = JSON.parse(fs.readFileSync(dataPath, 'utf8'))
if (!fs.existsSync(calcFilePath)) return
const m = await import(`./character/${charName}/${calcFile}?${Date.now()}`)
if (!m.calc && (!m.buffs || !m.skills)) throw new Error('伤害计算文件格式错误')
calcFnc.character[id] = m
const scoreFilePath = path.join(dir, scoreFile)
const loadScoreJS = async () => {
if (!fs.existsSync(scoreFilePath)) return
const m = await import(`./character/${charName}/${scoreFile}?${Date.now()}`)
const fnc = m.default
if (!fnc || typeof fnc !== 'function') throw new Error('评分权重文件格式错误:' + charName)
scoreFnc[id] = fnc
}
if (isWatch) {
watchFile(dataPath, loadCharData)
watchFile(calcFilePath, loadCalcJS)
watchFile(scoreFilePath, loadScoreJS)
}
loadCharData()
await loadCalcJS()
await loadScoreJS()
} catch (e) {
logger.error(`导入角色${charName}伤害计算错误:`, e)
logger.error(`导入角色${charName}计算文件错误:`, e)
}
}

View file

@ -0,0 +1,17 @@
/** @type {import('../../avatar.ts')['scoreFnc'][string]} */
export default function (avatar) {
return ['评分规则名', {
"生命值百分比": 0,
"攻击力百分比": 0.75,
"防御力百分比": 0,
"冲击力": 0,
"暴击率": 1,
"暴击伤害": 1,
"穿透率": 0.75,
"穿透值": 0.25,
"能量回复": 0,
"异常精通": 0,
"异常掌控": 0,
"冰属性伤害提高": 1
}]
}

View file

@ -1,10 +1,6 @@
import { property } from '../lib/convert.js';
import { getSuitImage, getWeaponImage } from '../lib/download.js';
import {
scoreData,
hasScoreData,
getEquipPropertyEnhanceCount
} from '../lib/score.js';
import { getEquipPropertyEnhanceCount } from '../lib/score.js';
import Score from './damage/Score.js';
/**
@ -186,14 +182,13 @@ export class Equip {
/**
* 获取装备属性分数
* @param {string} charID
* @param {{[propID: string]: number}} weight 权重
* @returns {number}
*/
get_score(charID) {
if (hasScoreData(charID)) {
this.properties.forEach(item => item.base_score = scoreData[charID][item.property_id] || 0);
this.score = Score.main(charID, this);
}
get_score(weight) {
if (!weight) return this.score;
this.properties.forEach(item => item.base_score = weight[item.property_id] || 0);
this.score = Score.main(this, weight);
return this.score;
}