// ==UserScript==
// @name Mortal 显示恶手率
// @namespace https://viayoo.com/
// @version 2.0.4
// @description Mortal牌谱解析增强脚本
// @author mcube-12139
// @author modify by Miku39
// @run-at document-idle
// @include /^https?:\/\/mjai.ekyu.moe\/report\/[A-Za-z0-9-_]+.html$/
// @include /^https?:\/\/mjai.ekyu.moe\/?([A-Za-z0-9-_]+.html)?$/
// @grant none
// ==/UserScript==
{
//保留n位小数
function roundFun(value, n) {
return Math.round(value*Math.pow(10,n))/Math.pow(10,n);
}
if(window.location.pathname.startsWith("/report/")) {
document.getElementsByClassName("collapse")[1].toggleAttribute("open", true); //打开 元数据 选项卡
const badMoveUpperLimit = 5; //恶手率
const badMoveUpperLimitCustom = 10; //恶手率
let badChooseNum = 0;
let badChooseNumCustom = 0;
const lang = document.documentElement.lang;
const i18nText = {};
if (lang == "zh-CN") {
i18nText.badMove = "恶手";
i18nText.badMoveRatio = "恶手率";
i18nText.matchRatio = "AI 一致率";
i18nText.metaData = "元数据";
} else if (lang == "ja") {
i18nText.badMove = "Bad move";
i18nText.badMoveRatio = "bad moves/total";
i18nText.matchRatio = "AI一致率";
i18nText.metaData = "メタデータ";
} else if (lang == "ko") {
i18nText.badMove = "Bad move";
i18nText.badMoveRatio = "bad moves/total";
i18nText.matchRatio = "matches/total";
i18nText.metaData = "메타데이터";
} else {
i18nText.badMove = "Bad move";
i18nText.badMoveRatio = "bad moves/total";
i18nText.matchRatio = "matches/total";
i18nText.metaData = "Metadata";
}
i18nText.modelTag = "model tag";
//
i18nText.badMoveError = "(恶手率统计只支持最新版本Mortal,当前版本生成结果不可靠)"
//
i18nText.badMoveUp = " (严重鸡打 权重<=5%)";
i18nText.badMoveDown = " (普通错误 权重5~10%)";
i18nText.badMoveNull = " ";
i18nText.badMoveSymbol = "%";
i18nText.badMoveDiffer = "差值: ";
//
i18nText.badMoveDiffer1 = "微差(0~5): ";
i18nText.badMoveDiffer2 = "小幅差距(5~10): ";
i18nText.badMoveDiffer3 = "低等差距(10~20): ";
i18nText.badMoveDiffer4 = "中等差距(20~40): ";
i18nText.badMoveDiffer5 = "高等差距(40~60): ";
i18nText.badMoveDiffer6 = "大幅度差距(60~80): ";
i18nText.badMoveDiffer7 = "压倒性差距(80~100): ";
//
i18nText.badMoveSum = " (总计)";
const orderLosses = document.getElementsByClassName("order-loss");
for (let i = 0, length = orderLosses.length; i != length; ++i) {
const orderLoss = orderLosses[i];
const chosenIndex = parseInt(orderLoss.innerText.substring(2));
const turnInfo = orderLoss.parentElement;
const summary = turnInfo.parentElement;
const collapseEntry = summary.parentElement;
const details = collapseEntry.lastChild;
const table = details.firstChild;
const tbody = table.lastChild;
const chosenTr = tbody.childNodes[chosenIndex - 1];
const weightTd = chosenTr.lastChild;
//const intSpan = weightTd.firstChild;
const intSpan = weightTd.innerHTML.replace(/<.*?>/g, ""); //过滤html标签, 只保留文字内容
const chosenWeight = parseFloat(intSpan);
if (chosenWeight <= parseFloat(badMoveUpperLimit)) { //严重恶手
const badChooseNode = document.createElement("span");
badChooseNode.innerHTML = ` \u00A0\u00A0\u00A0${i18nText.badMove}${i18nText.badMoveUp}`;
badChooseNode.style.color = "#f00";
badChooseNode.style.fontWeight = 900;
badChooseNode.style.fontSize = "20px";
turnInfo.appendChild(badChooseNode);
collapseEntry.style.border = "2px solid #f00";
badChooseNum++;
}else if (chosenWeight <= parseFloat(badMoveUpperLimitCustom)) { //普通恶手
const badChooseNode = document.createElement("span");
badChooseNode.innerHTML = ` \u00A0\u00A0\u00A0${i18nText.badMove}${i18nText.badMoveDown}`;
badChooseNode.style.color = "#6600FF";
badChooseNode.style.fontWeight = 700;
badChooseNode.style.fontSize = "20px";
turnInfo.appendChild(badChooseNode);
collapseEntry.style.border = "2px solid #6600FF";
badChooseNumCustom++;
}
} //for
// 新增 显示 Mortal 版本
const jsonStr = localStorage.getItem("Mortal_Type");
var mortalMap = null;
if(jsonStr != null) {
let obj = Object.entries(JSON.parse(jsonStr));
mortalMap = new Map(obj);
}
//const metaData = document.getElementsByClassName("collapse")[1];
let metaData;
const detailsElements = document.getElementsByTagName("details");
for (let i = 0, length = detailsElements.length; i != length; ++i) {
const details = detailsElements[i];
const summary = details.firstChild;
if (summary.firstChild.textContent == i18nText.metaData) {
metaData = details;
break;
}
}
const metaDataDl = metaData.lastChild;
let matchRatioDd = null;
let version = null;
for (let i = 0, length = metaDataDl.childNodes.length; i != length; ++i) {
const metaDataChild = metaDataDl.childNodes[i];
if(metaDataChild.nodeName == "DT" && metaDataChild.textContent == i18nText.modelTag) {
let ele = metaDataDl.childNodes[i + 1];
//判断当前是否是最新版本的mortal
const mortal_New = localStorage.getItem("Mortal_New");
if(mortal_New != null) {
if(mortal_New != ele.innerText) {
let aiEle = metaDataDl.childNodes[i - 1];
aiEle.innerText = aiEle.innerText + ` \u00A0\u00A0\u00A0${i18nText.badMoveError}`;
aiEle.style.color = "#f00";
}
}
//处理当前版本
if(mortalMap != null) {
let mortalValue = mortalMap.get(ele.innerText);
ele.innerText = mortalValue;
}
}
if (metaDataChild.nodeName == "DT" && metaDataChild.textContent == i18nText.matchRatio) {
matchRatioDd = metaDataDl.childNodes[i + 1];
version = metaDataDl.childNodes[i + 2];
//
metaDataDl.childNodes[i-2].style.color = "#6600FF"; //rating
metaDataDl.childNodes[i-1].style.color = "#6600FF";
metaDataDl.childNodes[i].style.color = "#6600FF"; //AI 一致率
metaDataDl.childNodes[i+1].style.color = "#6600FF";
break;
}
}
const matchRatioText = matchRatioDd.textContent;
const chooseNumStr = matchRatioText.substring(matchRatioText.indexOf("/") + 1);
const chooseNum = parseInt(chooseNumStr);
const badChooseRatioDt = document.createElement("dt");
badChooseRatioDt.style.color = "#FF0066";
badChooseRatioDt.innerHTML = i18nText.badMoveRatio + i18nText.badMoveNull + badMoveUpperLimit + i18nText.badMoveSymbol;
const badChooseRatioDd = document.createElement("dd");
badChooseRatioDd.style.color = "#FF0066";
badChooseRatioDd.innerHTML = `${badChooseNum}/${chooseNum} = ${(100 * badChooseNum / chooseNum).toFixed(3)}%`;
metaDataDl.insertBefore(badChooseRatioDd, version);
metaDataDl.insertBefore(badChooseRatioDt, badChooseRatioDd);
/* 新增 计算总恶手数 */
badChooseNumCustom += badChooseNum; //计算总恶手数
const badChooseRatioDt2 = document.createElement("dt");
badChooseRatioDt2.style.color = "#FF0066";
badChooseRatioDt2.innerText = i18nText.badMoveRatio + i18nText.badMoveNull + badMoveUpperLimitCustom + i18nText.badMoveSymbol;
const badChooseRatioDd2 = document.createElement("dd");
badChooseRatioDd2.style.color = "#FF0066";
badChooseRatioDd2.innerHTML = `${badChooseNumCustom}/${chooseNum} = ${(100 * badChooseNumCustom / chooseNum).toFixed(3)}%`;
metaDataDl.insertBefore(badChooseRatioDd2, version);
metaDataDl.insertBefore(badChooseRatioDt2, badChooseRatioDd2);
/* 列出选择权重 */
const map = new Map(); //使用map保证重置循坏后的唯一性
const boxObjStr = '{"left":0,"top":0}';
const entry = document.getElementsByClassName("collapse entry");
for (let i = 0, length = entry.length; i != length; ++i) {
entry[i].style.position = "relative";
const roleEle = entry[i].getElementsByClassName("role");
let selfPai = roleEle[0].parentElement;
let mortalPai = roleEle[1].nextElementSibling;
if(mortalPai.tagName.toLocaleLowerCase() == 'details') {
mortalPai = roleEle[1].nextSibling;
}
if (Object.prototype.toString.call(selfPai.childNodes[selfPai.childNodes.length -2]) == '[object SVGSVGElement]') {
if(selfPai.childNodes[selfPai.childNodes.length -2].tagName.toLocaleLowerCase() == 'svg') { //如果有多张牌图片,就使用最后一张牌图片
selfPai = selfPai.childNodes[selfPai.childNodes.length -2];
}
}
if(mortalPai.nextElementSibling.tagName.toLocaleLowerCase() == 'svg') { //如果有多张牌图片,就使用最后一张牌图片
mortalPai = mortalPai.nextElementSibling;
}
const dataEle = entry[i].getElementsByTagName("tbody")[0].childNodes;
var selfPaiData = 0;
var mortalPaiData = 0;
var selfBoxObj = JSON.parse(boxObjStr), mortalBoxObj = JSON.parse(boxObjStr);
map.clear(); //清除map
let j = 0, size = dataEle.length;
while (j != size) {
let selfPaiStr = null;
let mortalPaiStr = null;
let isResetLoop = false; //是否重置循坏
if(selfPai != null){
if(Object.prototype.toString.call(selfPai) == '[object Text]') {
selfPaiStr = selfPai.data;
}else{
let obj = selfPai.getElementsByClassName("face");
if(obj[0] != null){
selfPaiStr = obj[0].href.baseVal;
}else{ //选择跳过的情况
selfPaiStr = selfPai.childNodes[selfPai.childNodes.length -1].data;
}
}
}
if(mortalPai != null){
if(Object.prototype.toString.call(mortalPai) == '[object Text]') {
mortalPaiStr = mortalPai.data;
}else{
let obj = mortalPai.getElementsByClassName("face");
if(obj[0] != null){
mortalPaiStr = obj[0].href.baseVal;
}else{ //选择跳过的情况
mortalPaiStr = mortalPai.childNodes[mortalPai.childNodes.length -1].data;
}
}
}
let data = dataEle[j].childNodes[2].innerHTML.replace(/<.*?>/g, ""); //过滤html标签, 只保留文字内容
let obj1 = dataEle[j].childNodes[0].getElementsByClassName("face");
let dataPaiStr;
if(obj1[obj1.length - 1] != null) { // 如果有多张牌, 则选择最后一张牌作为对比牌 (主要用于吃的情况、碰杠这些牌都是一样的)
dataPaiStr = obj1[obj1.length - 1].href.baseVal;
}else{
dataPaiStr = dataEle[j].childNodes[0].innerHTML;
if(map.has(j) == false) {
map.set(j, true); //保存当前j的值,防止重复开始循坏
j = 0; //如果 有 选择跳过的情况, 则重新开始循坏, 以找到正确的数据
isResetLoop = true;
}
}
if(selfPaiStr == dataPaiStr) { //如果目标操作是自己的操作
selfPaiData = data;
const span = document.createElement("span");
span.innerText = ` \u00A0\u00A0\u00A0` + data;
span.style.position = "absolute";
if(selfBoxObj.top == 0) {
if(!isNaN(selfPai.offsetTop)) {
selfBoxObj.top = (selfPai.offsetTop + selfPai.offsetHeight / 2 - 10);
}else{
selfBoxObj.top = (selfPai.parentElement.offsetTop + selfPai.parentElement.offsetHeight / 2 - 10);
}
span.style.top = selfBoxObj.top + 2 + "px";
}
//span.style.left = selfPai.offsetLeft + selfPai.offsetWidth + 5 + 40 + "px";
span.style.left = "170px";
entry[i].insertBefore(span, entry[i].childNodes[3].nextSibling);
selfPai = null; //置null, 防止继续计算
}else if(mortalPaiStr == dataPaiStr) { //如果目标操作是Mortal的操作
mortalPaiData = data;
const span = document.createElement("span");
span.innerText = ` \u00A0\u00A0\u00A0` + data;
span.style.position = "absolute";
if(mortalBoxObj.top == 0) {
if(!isNaN(mortalPai.offsetTop)) {
mortalBoxObj.top = (mortalPai.offsetTop + mortalPai.offsetHeight / 2 - 10);
}else if(!isNaN(mortalPai.previousElementSibling.offsetTop)) {
mortalBoxObj.top = (mortalPai.previousElementSibling.offsetTop + mortalPai.previousElementSibling.offsetHeight / 2 - 10);
}else if(!isNaN(mortalPai.previousElementSibling.previousElementSibling.offsetTop)) {
mortalBoxObj.top = (mortalPai.previousElementSibling.previousElementSibling.offsetTop + mortalPai.previousElementSibling.previousElementSibling.offsetHeight / 2 - 10);
}
span.style.top = mortalBoxObj.top + 1 + "px";
}
// span.style.left = mortalPai.offsetLeft + mortalPai.offsetWidth + 5 + 40 + "px";
span.style.left = "170px";
if(Object.prototype.toString.call(mortalPai.nextSibling) == '[object Text]') { //如果有多张牌图片,就使用最后一张牌图片后面的文字的位置
mortalPai = mortalPai.nextSibling;
}
entry[i].insertBefore(span, mortalPai.nextSibling);
mortalPai = null; //置null, 防止继续计算
}
if(selfPaiStr == mortalPaiStr) { //如果自己选择打出的牌与mortal选择打出的牌相同
if(map.has(j) == false) {
map.set(j, true); //保存当前j的值,防止重复开始循坏
j = 0; //如果 有 选择跳过的情况, 则重新开始循坏, 以找到正确的数据
isResetLoop = true;
}
}
if(selfPai == null && mortalPai == null) { //是否处理完毕
break; //跳出循坏
}
if(isResetLoop == false){ //不重置循坏时, index++
++j;
}
}//for
/* 计算自己的选择与mortal选择的差值 */
const defaultHandleFunc = (newNode, index, colorStr) => {
newNode.style.color = colorStr; //设置为目标颜色
newNode.innerHTML = ` \u00A0\u00A0\u00A0` + eval("i18nText.badMoveDiffer" + index) + differData;
}
const differData = roundFun(Math.abs(mortalPaiData - selfPaiData), 5); //保留5位小数
if (differData != 0) { //忽略自己和mortal打出的牌一样的结果
const turnInfo = roleEle[0].parentElement.parentElement.childNodes[0];
const newNode = document.createElement("span");
newNode.style.fontWeight = 400;
if (differData < 5) { //微差
defaultHandleFunc(newNode, 1, "#000"); //黑色
}else if (differData < 10) { //小幅差距
defaultHandleFunc(newNode, 2, "#996633"); //褐色
}else if (differData < 20) { //低等差距
defaultHandleFunc(newNode, 3, "#009966"); //淡绿
}else if (differData < 40) { //中等差距
defaultHandleFunc(newNode, 4, "#3399FF"); //淡蓝
}else if (differData < 60) { //高等差距
defaultHandleFunc(newNode, 5, "#3333CC"); //深蓝
}else if (differData < 80) { //大幅度差距
defaultHandleFunc(newNode, 6, "#CC0099"); //淡红
}else{ //压倒性差距
defaultHandleFunc(newNode, 7, "#f00"); //红色
}
turnInfo.appendChild(newNode);
}
}//for
}else {
document.getElementsByName("show-rating").forEach((ele)=>{ele.checked = true}); //默认勾选 显示Rating
const map = new Map();
let childEle = document.getElementById("mortal-model-tag").children;
for (let i = 0; i < childEle.length; i++) {
const ele = childEle[i];
map.set(ele.value, ele.innerText); //将数据保存到map
}
const jsonStr = JSON.stringify(Object.fromEntries(map));
localStorage.setItem("Mortal_New", childEle[0].value); //牌谱解析页面,默认使用最新的Mortal
localStorage.setItem("Mortal_Type", jsonStr);
}
}