// ==UserScript==
// @name Mortal 显示恶手率
// @description Mortal牌谱解析增强脚本 (雀魂麻将/天凤/麻雀一番街)
// @version 2.2.6
// @homepage https://www.bilibili.com/read/cv26608482/
// @namespace https://viayoo.com/
// @author Miku39
// @icon data:image/x-icon;base64,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5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+Zj9X/NR6//0Ywwv/Kxd//6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//0Iswv++uN3/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//z8owf+1rtv/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zwmwP+vptn/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zskwP+qodj/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zojwP+nntj/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zkiwP+lnNf/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zkiwP+lm9f/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zkiwP+kmtf/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zkiv/+jmtf/6Ofm/+jn5v/o5+b/6Ofm/+jn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5v/o5+b/6Ofm/+jn5v/n5uX+6Oblyf///wUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOTk5DDn5+bC5+fm/Ojn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fm5f3o5+bD7unpLwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wHs7Owc5+fleOfl5Nbn5uX56Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/n5uX56Ofl1efl5Xnj4+Mcf39/AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
// @resource js_layui https://cdn.staticfile.org/layui/2.8.17/layui.min.js
// @resource js_cryptojs https://cdn.staticfile.org/crypto-js/4.1.1/crypto-js.min.js
// @resource js_immutable https://cdn.staticfile.org/immutable/4.3.4/immutable.min.js
// @resource js_localforage https://cdn.staticfile.org/localforage/1.10.0/localforage.min.js
// @resource css_layui https://cdn.staticfile.org/layui/2.8.17/css/layui.min.css
// @resource css_fontAwesome https://cdn.staticfile.org/font-awesome/6.4.2/css/all.min.css
// @match *://mjai.ekyu.moe/*
// @grant GM_info
// @grant unsafeWindow
// @grant GM_getResourceText
// @grant GM_getResourceURL
// @grant GM_setClipboard
// @run-at document-start
// @license BSD-3-Clause
// ==/UserScript==
((g_window)=>{
'use strict';
// ---------------------------- 全局变量: App变量 ----------------------------
const badMoveUpperLimit = 5; //恶手率
const badMoveUpperLimitCustom = 10; //恶手率
let badChooseNum = 0;
let badChooseNumCustom = 0;
const pageLanguage = document.scrollingElement.lang;
let i18nText;
const localization = {
zh_cn: {
badMove: "恶手",
badMoveRatio: "恶手率",
matchRatio: "AI 一致率",
metaData: "元数据",
},
ja: {
badMove: "Bad move",
badMoveRatio: "bad moves/total",
matchRatio: "AI一致率",
metaData: "メタデータ",
},
ko: {
badMove: "Bad move",
badMoveRatio: "bad moves/total",
matchRatio: "matches/total",
metaData: "메타데이터",
},
en: {
badMove: "Bad move",
badMoveRatio: "bad moves/total",
matchRatio: "matches/total",
metaData: "Metadata",
}
};
if (pageLanguage == "zh-CN") {
i18nText = localization.zh_cn;
} else if (pageLanguage == "ja") {
i18nText = localization.ja;
} else if (pageLanguage == "ko") {
i18nText = localization.ko;
} else {
i18nText = localization.en;
}
i18nText.modelTag = "model tag";
//
i18nText.seatTypeA0 = "东起";
i18nText.seatTypeA1 = "南起";
i18nText.seatTypeA2 = "西起";
i18nText.seatTypeA3 = "北起";
//
i18nText.seatTypeB0 = "东家";
i18nText.seatTypeB1 = "南家";
i18nText.seatTypeB2 = "西家";
i18nText.seatTypeB3 = "北家";
//
i18nText.seatTypeC0 = "自家";
i18nText.seatTypeC1 = "下家";
i18nText.seatTypeC2 = "对家";
i18nText.seatTypeC3 = "上家";
//
i18nText.Ron = "荣和";
i18nText.Tsumo = '自摸';
i18nText.Ryuukyoku = '流局';
i18nText.RyuukyokuTsumo = "流局满贯";
i18nText.RyuukyokuType1 = "九种九牌";
//四风连打、四杠散了、四家立直 //三家和了
//
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 tenhouText = {
Ron: '和了', //荣和
Tsumo: '和了', //自摸
Ryuukyoku: '流局', //荒牌流局
RyuukyokuTsumo: "流し満貫", //流局满贯
RyuukyokuType1: "九種九牌", //九种九牌
//四风连打、四杠散了、四家立直 //三家和了
};
const matchRule = {
isInit: false,
isRon3: true, //是否允许三家荣和
};
const globalSettings = {
AppFunctionSettings: {
scrollAnimation: false, //滚动动画
},
UISettings: {
basic: {
fontColor: "#000", //字体颜色
currentTheme: "", //当前主题
},
theme: [
{
name: "暗色模式",
},
]
},
Other: {
debugMode: { //调试模式
enable: false,
trackCall: {
enable: false,
trackFunction: true,
trackTimestamp: true,
},
logOut: {
enable: true,
filter: "debug",
}
},
safeMode: false, //安全模式 (禁用程序所有功能)
},
};
let strArray2 = []; //恶手选择器
let strArray3 = []; //不一致选择器
// ---------------------------- 全局变量: 模板 ----------------------------
const CSSCompatibilityFlag = ".ui";
const CSSCompatibilityFlagNoPoint = CSSCompatibilityFlag.substring(1);
const commonCSS =
'\
.position_re { position: relative; }\
.position_ab { position: absolute; }\
.position_fi { position: fixed; }\
\
.font_weight_900 { font-weight: 900; }\
.font_weight_700 { font-weight: 700; }\
.font_weight_400 { font-weight: 400; }\
\
.w-auto { width: auto; }\
.w-100 { width: 100%; }\
.w-80 { width: 80%; }\
.w-75 { width: 75%; }\
.w-60 { width: 60%; }\
.w-50 { width: 50%; }\
.w-40 { width: 40%; }\
.w-25 { width: 25%; }\
.w-20 { width: 20%; }\
.w-10 { width: 10%; }\
.w-5 { width: 5%; }\
\
.min-w-auto { min-width: auto; }\
.min-w-100 { min-width: 100%; }\
.min-w-80 { min-width: 80%; }\
.min-w-75 { min-width: 75%; }\
.min-w-60 { min-width: 60%; }\
.min-w-50 { min-width: 50%; }\
.min-w-40 { min-width: 40%; }\
.min-w-25 { min-width: 25%; }\
.min-w-20 { min-width: 20%; }\
.min-w-10 { min-width: 10%; }\
.min-w-5 { min-width: 5%; }\
\
.max-w-auto { max-width: auto; }\
.max-w-100 { max-width: 100%; }\
.max-w-80 { max-width: 80%; }\
.max-w-75 { max-width: 75%; }\
.max-w-60 { max-width: 60%; }\
.max-w-50 { max-width: 50%; }\
.max-w-40 { max-width: 40%; }\
.max-w-25 { max-width: 25%; }\
.max-w-20 { max-width: 20%; }\
.max-w-10 { max-width: 10%; }\
.max-w-5 { max-width: 5%; }\
\
.cursor_default { cursor: default; }\
.cursor_crosshair { cursor: crosshair; }\
.cursor_pointer { cursor: pointer; }\
.cursor_move { cursor: move; }\
.cursor_text { cursor: text; }\
.cursor_wait { cursor: wait; }\
.cursor_help { cursor: help; }\
';
const pageExtendBaseCSS =
'\
details.collapseEntryL1 { border: 2px solid #f00; }\
details.collapseEntryL2 { border: 2px solid #6600FF; }\
.badChoose { font-size: 20px; }\
.level1 { color: #f00; font-weight: 900; }\
.level2 { color: #6600FF; font-weight: 700; }\
.color1 { color: #f00; }\
.color2 { color: #6600FF; }\
.color3 { color: #FF0066; }\
.color4 { color: #990000; }\
.l-130px { left: 130px; }\
.l-170px { left: 170px; }\
.l-190px { left: 190px; }\
.l-196px { left: 196px; }\
.color5 { color: #CC0000; }\
.color6 { color: #333; }\
';
const MainAppBaseCSS =
'\
html { scroll-behavior: auto; }\
/*details { scroll-margin-top: calc(2em + 4px); }*/\
.highlight { animation: shimmer 0.75s infinite; }\
@keyframes shimmer {\
0% { background: #ffd5d500; }\
50% { background: #ffd5d5ff; }\
100% { background: #ffd5d500; }\
}\
';
const MainAppCSS =
'\
.commonDIV { padding: 5px; }\
#settingUI { width: 800px; height: 600px; border: 1px solid red; position: fixed; top: 100px; left: 100px; z-index: 10; }\
#catalogUI { border: 1px solid blue; position: fixed; z-index: 10; }\
';
const origSettingUITemplate =
'\
<div class="commonDIV" id="origSettingUI">\
<fieldset id="panel">\
<legend>设置</legend>\
<span style="font-weight:700">布局:</span>\
<label><input checked="" name="layout" onclick="toggleLayout()" type="radio" value="vertical">垂直</label>\
<label><input name="layout" onclick="toggleLayout()" type="radio" value="horizontal">水平</label>\
<br><span style="font-weight:700">展开:</span><label><input name="expand" onclick="toggleExpand()" type="radio" value="all">全部</label>\
<label><input checked="" name="expand" onclick="toggleExpand()" type="radio" value="diff-only">仅差异项</label>\
<label><input name="expand" onclick="toggleExpand()" type="radio" value="none">无</label>\
<br><a class="no-visit" download="" href="">💾保存本页面</a>\
</fieldset>\
</div>\
';
const settingUIBase =
'\
<div class="ui commonDIV" id="settingUI">\
</div>\
';
const settingUITemplate =
'\
';
const catalogUIBase =
'\
<div class="ui commonDIV" id="catalogUI">\
</div>\
';
const catalogUITemplate =
'\
<div class="ui layui-btn-container" id="catalogUIBuf">\
<button type="button" class="ui layui-btn" id="but_kyoku_prev">\
<i class="ui layui-icon layui-icon-prev"></i>\
</button>\
<button type="button" class="ui layui-btn" id="but_kyoku_next">\
<i class="ui layui-icon layui-icon-next"></i>\
</button>\
<button type="button" class="ui layui-btn layui-btn-disabled" id="but_diff_prev">\
<i class="ui layui-icon layui-icon-left"></i>\
</button>\
<button type="button" class="ui layui-btn layui-btn-disabled" id="but_diff_next">\
<i class="ui layui-icon layui-icon-right"></i>\
</button>\
</div>\
<ul class="ui layui-nav layui-nav-tree layui-bg-gray" id="selectorGroups" lay-filter="selector-filter-nav" style="width: 100%; overflow: auto;">\
<!-- 对局选择器 -->\
<li class="ui layui-nav-item layui-nav-itemed">\
<a class="ui cursor_pointer" href="javascript:;">对局选择器</a>\
<dl class="ui layui-nav-child" class="selector" id="selector1">\
{{# layui.each(d.selector1, function(index, item){ }}\
<dd class="ui cursor_pointer"><a class="ui" id="{{= item.nameID }}">{{= item.name }}</a></dd>\
{{# }); }}\
</dl>\
</li>\
<!-- 恶手选择器 -->\
<li class="ui layui-nav-item">\
<a class="ui cursor_pointer" href="javascript:;">恶手选择器</a>\
<dl class="ui layui-nav-child" class="selector" id="selector2">\
{{# layui.each(d.selector2, function(index, item){ }}\
<dd class="ui cursor_pointer"><a class="ui" id="{{= item.nameID }}">{{= item.name }}</a></dd>\
{{# }); }}\
</dl>\
</li>\
<!-- 不一致选择器 -->\
<li class="ui layui-nav-item">\
<a class="ui cursor_pointer" href="javascript:;">不一致选择器</a>\
<dl class="ui layui-nav-child" class="selector" id="selector3">\
{{# layui.each(d.selector3, function(index, item){ }}\
<dd class="ui cursor_pointer"><a class="ui" name="{{= item.nameID }}">{{= item.name }}</a></dd>\
{{# }); }}\
</dl>\
</li>\
<li class="ui layui-nav-item"><a class="ui" href="javascript:;">打开设置 (等待完善)</a></li>\
</ul>\
';
// ---------------------------- 其他 ----------------------------
let ObserverEleSetList = [];
function setNewActivateEle(targetEle) {
if(targetEle != null && targetEle != undefined) {
//清除旧数据
for (let j = 0; j < ObserverEleSetList.length; j++) {
const element = ObserverEleSetList[j];
element.parentElement.classList.remove("layui-this");
}
ObserverEleSetList.length = 0; //清空数组
//处理新数据
targetEle.parentElement.classList.add("layui-this");
ObserverEleSetList.push(targetEle);
}
}
// ---------------------------- 基本框架 ----------------------------
const FILETYPE = {
JS: "js",
CSS: "css",
}
const JSLOADTYPE = {
ASYNC: { //异步加载
name: "async",
bit: 1,
},
DEFER: { //延迟加载
name: "defer",
bit: 1 << 1,
},
}
class Utils {
static {
Utils.addNewScript("js_cryptojs", GM_getResourceText("js_cryptojs"));
Utils.addNewScript("js_immutable", GM_getResourceText("js_immutable"));
//Utils.loadjscssFile(GM_getResourceURL("js_immutable"), FILETYPE.JS, JSLOADTYPE.ASYNC.bit | JSLOADTYPE.DEFER.bit);
}
static loadjscssFile(filePath, fileType, args_bits_LOADTYPE_bit) {
let ele = undefined;
if (fileType == FILETYPE.JS) {
ele = document.createElement('script');
ele.setAttribute("src", filePath);
if((args_bits_LOADTYPE_bit & (1 << 0)) == 0 ? false : true){
ele.setAttribute(JSLOADTYPE.ASYNC.name, true);
}else if((args_bits_LOADTYPE_bit & (1 << 1)) == 0 ? false : true){
ele.setAttribute(JSLOADTYPE.DEFER.name, true);
}
} else if (fileType == FILETYPE.CSS) {
ele = document.createElement("link");
ele.setAttribute("rel", "stylesheet");
ele.setAttribute("href", filePath);
}else{
Console.orig.log(`不支持的文件类型: ${fileType}`);
return;
}
if (ele != undefined) {
document.getElementsByTagName("head")[0].appendChild(ele);
}
}
static addNewStyle(id, newStyle) {
let styleElement = document.getElementById(id);
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = id;
document.getElementsByTagName('head')[0].appendChild(styleElement);
}
styleElement.appendChild(document.createTextNode(newStyle));
}
static addNewScript(id, newScript) {
let scriptElement = document.getElementById(id);
if (!scriptElement) {
scriptElement = document.createElement('script');
scriptElement.id = id;
document.getElementsByTagName('head')[0].appendChild(scriptElement);
}
scriptElement.appendChild(document.createTextNode(newScript));
}
static addElementsByHTMLTemplateText(htmlTemplate, parentElement) {
let ele = document.createElement("div");
if(parentElement == undefined)
document.body.appendChild(ele);
else
parentElement.appendChild(ele);
ele.outerHTML = htmlTemplate;
}
}
class CustomUtils extends Utils {
static{
// 拓展系统库
Math.constructor.roundEx = (nValue, n) => { //保留n位小数
return Math.round(nValue*Math.pow(10,n))/Math.pow(10,n);
}
}
static handleCSSCompatibility(idPrefix, cssText) { //处理css兼容性, 防止外部css库修改原始css样式
let retCssText = cssText;
let splitStrArray = cssText.split(/\{[\s\S]+?\}/g); //提取每一段的css选择器
let startIndex = 0, endIndex = 0;
splitStrArray.forEach((item,index)=> {
let cssSelectArray = item.match(/[A-Za-z0-9\:\-\_\@\.\#\+\>\<\*\?\;\!\[\]\=\"\'\`\/\\\(\)\{\}\~\^\$\|\%\&]+/g); //提取单个css选择器 //排除了,
//Console.orig.log("cssSelectArray: " + cssSelectArray);
if(cssSelectArray != null) {
for (let i = 0; i < cssSelectArray.length; i++) {
const cssSelectStr = cssSelectArray[i];
if(cssSelectStr.startsWith('@')) //需要排除的内容
break;
if(cssSelectStr == "from" || cssSelectStr == "to") //需要排除的内容
break;
if(cssSelectStr.indexOf("%") != -1) //需要排除的内容
break;
if(cssSelectStr.indexOf(":") != -1) //需要排除的内容 (伪类元素选择器)
break;
//Console.orig.log("cssSelectArray: " + cssSelectArray);
startIndex = retCssText.indexOf(cssSelectStr, startIndex);
endIndex = startIndex + cssSelectStr.length;
retCssText = retCssText.substring(0, startIndex) + cssSelectStr + idPrefix + retCssText.substring(endIndex);
startIndex += idPrefix.length + 1 + cssSelectStr.length; //重新设置startIndex,防止重复查找
}
startIndex = retCssText.indexOf("}", startIndex) + 1; //重新设置startIndex,防止从错误的位置开始查找
//Console.orig.log("\n" + retCssText);
}
});
//重定向字体文件
retCssText = retCssText.replaceAll("url(../", "url(https://cdn.staticfile.org/layui/2.8.17/");
//Console.orig.log("\n" + retCssText);
return retCssText;
}
static handleJSCompatibility(idPrefix, jsText) { //处理js兼容性, 解决动态添加的节点与css兼容性的问题
jsText = jsText.replaceAll('class="', `class="${CSSCompatibilityFlagNoPoint} `); //全部匹配
//Console.orig.log("\n" + jsText);
return jsText;
}
static getTextLineNumByOffsetHeight(ele) {
let styles = getComputedStyle(ele, null);
let lineHeight = parseFloat(styles.lineHeight);
let height = parseFloat(styles.height);
let offsetHeight = parseFloat(ele.offsetHeight);
let lineNum = offsetHeight / lineHeight;
return Math.round(lineNum);
}
static getTextLineNum(ele) {
let styles = getComputedStyle(ele, null);
let lineHeight = parseFloat(styles.lineHeight);
let height = parseFloat(styles.height);
let offsetHeight = parseFloat(ele.offsetHeight);
let lineNum = (height || offsetHeight) / lineHeight;
return Math.round(lineNum);
}
static getStyleOfLineHeight(ele) {
let styles = getComputedStyle(ele, null);
let lineHeight = parseFloat(styles.lineHeight);
return lineHeight;
}
//函数节流: 减少代码执行频率 (在一个单位时间内,只能触发一次函数)
static throttle(fn, interval = 500) {
let run = true;
return function () {
if (!run) return;
run = false;
setTimeout(() => {
fn.apply(this, arguments);
run = true;
}, interval);
};
}
//函数防抖: 判断某个动作结束,如滚动结束、input输入结束等 (在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时) (在规定时间内,只让最后一次生效,前面的不生效)
static debounce(fn, interval = 500) {
let timeout = null;
return function () {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, interval);
};
}
}
class Debug {
static #bDebug = false; //调试模式
static #bPublicApi = true;
static {
let ApiList = [
"GM_setValue",
"GM_getValue",
"GM_addStyle",
"GM_deleteValue",
"GM_listValues",
"GM_addValueChangeListener",
"GM_removeValueChangeListener",
"GM_log",
"GM_getResourceText",
"GM_getResourceURL",
"GM_registerMenuCommand",
"GM_unregisterMenuCommand",
"GM_openInTab",
"GM_xmlhttpRequest",
"GM_download",
"GM_getTab",
"GM_saveTab",
"GM_getTabs",
"GM_notification",
"GM_setClipboard",
"GM_info",
];
if(Debug.#bDebug && Debug.#bPublicApi){
let execute = "";
for (const [index, item] of ApiList.entries()) {
execute += `if(typeof ${item} != 'undefined') g_window.${item} = ${item};\n`;
}
eval(execute);
}
}
static set setDebug(debug){
Debug.#bDebug = debug;
}
static get getDebug(){
return Debug.#bDebug;
}
}
class Console {
static orig = window.console; //保存原始对象
static clear(){
clear();
}
static log(...args){
Debug.getDebug &&
Console.orig.log('%c[log]', 'background: #ffa500; padding: 1px; color: #fff;', args);
}
static warn(...args){
Debug.getDebug &&
Console.orig.log('%c[warn]', 'background: #ffa500; padding: 1px; color: #fff;', args);
}
static error(...args){
Debug.getDebug &&
Console.orig.log('%c[error]', 'background: red; padding: 1px; color: #fff;', args);
}
static info(...args){
Debug.getDebug &&
Console.orig.log('%c[info]', 'background: #ffa500; padding: 1px; color: #fff;', args);
}
static table(...args){
Debug.getDebug &&
Console.orig.table(args);
}
}
g_window.Console = Console;
class Performance {
constructor(name){
if(name != undefined)
this._name = name;
else
this._name = '';
this.nTimeStart = 0;
this.nTimeEnd = 0;
}
setStartTime() {
this.nTimeStart = performance.now();
}
setEndTime() {
this.nTimeEnd = performance.now();
}
getEndTime() {
this.nTimeEnd = performance.now();
let executionTime = this.nTimeEnd - this.nTimeStart;
Console.orig.log(`${this._name}代码执行时间: ${executionTime.toFixed(3)} 毫秒`); //
return executionTime;
}
}
const CallType = {
Positive: "Positive",
Reverse: "Reverse",
}
const HookType = {
Single: "Single",
Multi: "Multi",
}
class ProxyGenerator { //代理生成器: 生成代理方法
proxyInfo = [
CryptoJS.SHA1(this.generateSingle().toString().replace(/\s+/g, "")).toString(),
CryptoJS.SHA1(this.generateMulti().toString().replace(/\s+/g, "")).toString(),
CryptoJS.SHA1(this.generateMultiReverse().toString().replace(/\s+/g, "")).toString(),
];
generateSingle(fcName_, origFcAddr_, newFcAddr_, newFcAddrLastCall_) { //单hook
function _proxy_s(...args){
// debugger
let prototype = _proxy_s.prototype;
if(prototype.hasOwnProperty("_fcData")) {
let fcData = _proxy_s.prototype._fcData;
let origArgs = [...args]; //复制数组
args.unshift(origFcAddr_); //添加到数组开头
let lastCallArgs = [...args]; //复制数组
if(fcData.newFcAddr.length == 2)
args.splice(1, 0, undefined);; //将undefined插入到数组第二个元素
let interceptor = fcData.newFcAddr.apply(this, args); //调用newFcAddr
if(interceptor == true || interceptor == undefined){
let ret = fcData.origFcAddr.apply(this, origArgs); //调用原函数 //this是调用方传递的,也一并传递
if(fcData.newFcAddrLastCall && fcData.newFcAddrLastCall.length == 2)
lastCallArgs.splice(1, 0, ret);; //将ret插入到数组第二个元素
fcData.newFcAddrLastCall.apply(this, lastCallArgs); //调用newFcAddrLastCall
return ret;
}else{
return; //否则就拦截
}
}
}
_proxy_s.prototype._fcData = {
fcName: fcName_ || "",
origFcAddr: origFcAddr_ || null,
newFcAddr: newFcAddr_ || null,
newFcAddrLastCall: newFcAddrLastCall_ || null,
};
return _proxy_s;
}
generateMulti(key_) { //多层hook-正序调用
function _proxy_m(...args){
// debugger
let prototype = _proxy_m.prototype;
if(prototype.hasOwnProperty("_hk")) {
let hk = _proxy_m.prototype._hk;
let key = hk.key;
let hook = hk.obj;
let hookDataArray = hook.fcHookMap.get(key);
let ret;
let origArgs = [...args]; //复制数组
for (const [index, item] of hookDataArray.entries()) { //遍历多层hook
args.unshift(item.origFcAddr); //添加到数组开头
let lastCallArgs = [...args]; //复制数组
if(item.newFcAddr.length == 2)
args.splice(1, 0, undefined);; //将undefined插入到数组第二个元素
let interceptor = item.newFcAddr.apply(this, args); //调用newFcAddr
if(interceptor == true || interceptor == undefined){
ret = item.origFcAddr.apply(this, origArgs); //调用原函数 //this是调用方传递的,也一并传递
if(item.newFcAddrLastCall && item.newFcAddrLastCall.length == 2)
lastCallArgs.splice(1, 0, ret);; //将ret插入到数组第二个元素
item.newFcAddrLastCall.apply(this, lastCallArgs); //调用newFcAddrLastCall
if(index == hookDataArray.length -1){ //在多层hook结尾, 返回返回值
return ret;
}
}else{
return; //否则就拦截
}
}
}
}
_proxy_m.prototype._hk = {
key: key_ || "",
obj: this,
};
return _proxy_m;
}
generateMultiReverse(key_) { //多层hook-逆序调用
function _proxy_mr(...args){
// debugger
let prototype = _proxy_mr.prototype;
if(prototype.hasOwnProperty("_hk")) {
let hk = _proxy_mr.prototype._hk;
let key = hk.key;
let hook = hk.obj;
let hookDataArray = hook.fcHookMap.get(key);
let ret;
let origArgs = [...args]; //复制数组
for (let i = hookDataArray.length -1; i >= 0; i--) { //遍历多层hook
const item = hookDataArray[i];
args.unshift(item.origFcAddr); //添加到数组开头
let lastCallArgs = [...args]; //复制数组
if(item.newFcAddr.length == 2)
args.splice(1, 0, undefined);; //将undefined插入到数组第二个元素
let interceptor = item.newFcAddr.apply(this, args); //调用newFcAddr
if(interceptor == true || interceptor == undefined){
ret = item.origFcAddr.apply(this, origArgs); //调用原函数 //this是调用方传递的,也一并传递
if(item.newFcAddrLastCall && item.newFcAddrLastCall.length == 2)
lastCallArgs.splice(1, 0, ret);; //将ret插入到数组第二个元素
item.newFcAddrLastCall.apply(this, lastCallArgs); //调用newFcAddrLastCall
if(i == 0){ //在多层hook开头, 返回返回值
return ret;
}
}else{
return; //否则就拦截
}
}
}
}
_proxy_mr.prototype._hk = {
key: key_ || "",
obj: this,
};
return _proxy_mr;
}
}
class Hook {
static fnTypeArray = ['[object Function]', '[object AsyncFunction]'];
#proxyGenerator = new ProxyGenerator();
#callType = CallType.Positive;
constructor(proxyGenerator){
if(proxyGenerator != undefined)
this.setProxyGenerator = proxyGenerator;
this.fcHookMap = new Map();
//this.objHookMap = new Map();
}
set setProxyGenerator(proxyGenerator) {
this.#proxyGenerator = proxyGenerator;
}
setCallType(callType) {
this.#callType = callType;
for (const [key, item] of this.fcHookMap) { //遍历map
if(item.length > 1){ //排除单hook,因为单hook没有顺序区别
if(callType == CallType.Positive){
eval(`${key}=this.#proxyGenerator.generateMulti('${key}')`); //替换
return true;
}else if(callType == CallType.Reverse){
eval(`${key}=this.#proxyGenerator.generateMultiReverse('${key}')`); //替换
return true;
}else{
return false;
}
}
}
}
get getCallType(){
return this.#callType;
}
//设置hook时,设置的是代理函数,然后在代理函数里调用newFcAddr
//流程: 其他代码调用 fcAddr =>(实际调用) 代理函数 ->(里面先拿到原函数) 再调用newFcAddr ->(结束后根据返回值如果为true则调用原函数,并返回返回值,false就拦截)
//如果newFcAddr没有返回值 (说明不使用拦截功能),就正常调用原函数,返回返回值
//代理函数是动态生成的,并且代理函数里不能调用被hook的函数,不然会无限递归调用
setSingleHook(fcAddrStr, newFcAddr, newFcAddrLastCall){
// debugger
if(Object.prototype.toString.apply(fcAddrStr) != '[object String]'){
Console.orig.log("setSingleHook()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
return false;
}
let fcAddr = eval(fcAddrStr);
let type = Object.prototype.toString.apply(fcAddr);
let name = fcAddr.name;
let key = fcAddrStr;
if(Hook.fnTypeArray.includes(type)){
if(!this.fcHookMap.has(key)){
Console.orig.log("新fcHook: " + key);
this.fcHookMap.set(key, [{
fcName: name,
origFcAddr: fcAddr,
newFcAddr: newFcAddr,
newFcAddrLastCall: newFcAddrLastCall,
}]); //保存原始
eval(`${fcAddrStr}=this.#proxyGenerator.generateSingle('${name}', fcAddr, newFcAddr, newFcAddrLastCall)`); //替换
return true;
}else{
//先取消hook,再重新单hook
let hookDataArray = this.fcHookMap.get(key);
this.unHookByNameOfSpecifyOrAll(fcAddrStr, hookDataArray[0].newFcAddr);
return this.setSingleHook(fcAddrStr, newFcAddr, newFcAddrLastCall);
}
}else{
Console.orig.log("setSingleHook()失败, 不支持的type: " + type);
return false;
}
}
setHook(fcAddrStr, newFcAddr, newFcAddrLastCall){
// debugger
if(Object.prototype.toString.apply(fcAddrStr) != '[object String]'){
Console.orig.log("setHook()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
return false;
}
let fcAddr = eval(fcAddrStr);
let type = Object.prototype.toString.apply(fcAddr);
let name = fcAddr.name;
let key = fcAddrStr;
if(Hook.fnTypeArray.includes(type)){
if(!this.fcHookMap.has(key)){
Console.orig.log("新fcHook: " + key);
this.fcHookMap.set(key, [{
fcName: name,
origFcAddr: fcAddr,
newFcAddr: newFcAddr,
newFcAddrLastCall: newFcAddrLastCall,
}]); //保存原始
eval(`${fcAddrStr}=this.#proxyGenerator.generateSingle('${name}', fcAddr, newFcAddr, newFcAddrLastCall)`); //替换
return true;
}else{ //多层Hook
Console.orig.log("多层fcHook: " + key);
let hookDataArray = this.fcHookMap.get(key);
hookDataArray.push({
fcName: name,
origFcAddr: hookDataArray[0].origFcAddr,
newFcAddr: newFcAddr,
newFcAddrLastCall: newFcAddrLastCall,
}); //保存原始
this.fcHookMap.set(key, hookDataArray);
if(this.#callType == CallType.Positive){
eval(`${fcAddrStr}=this.#proxyGenerator.generateMulti('${key}')`); //替换
return true;
}else if(this.#callType == CallType.Reverse){
eval(`${fcAddrStr}=this.#proxyGenerator.generateMultiReverse('${key}')`); //替换
return true;
}
}
}else{
Console.orig.log("setHook()失败, 不支持的type: " + type);
return false;
}
}
isHook(fcAddrStr){
if(Object.prototype.toString.apply(fcAddrStr) != '[object String]'){
Console.orig.log("isHook()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
return false;
}
if(this.fcHookMap.has(fcAddrStr)){
return true;
}else{
let fcAddr = eval(fcAddrStr);
if(fcAddr == undefined){
return false;
}
let hash = CryptoJS.SHA1(fcAddr.toString().replace(/\s+/g, "")).toString();
for (const [index, item] of this.#proxyGenerator.proxyInfo.entries()) {
if(item == hash) {
return true;
}
}
return false;
}
}
getHookType(fcAddrStr){
if(Object.prototype.toString.apply(fcAddrStr) != '[object String]'){
Console.orig.log("getHookType()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
return false;
}
const hookDataArray = this.fcHookMap.get(fcAddrStr);
if(hookDataArray != undefined) {
return hookDataArray.length == 1 ? HookType.Single : HookType.Multi;
}else{
return null;
}
}
#unHookCommon_unload(fcAddrStr, hookData, hookDataArray, i){
if(hookDataArray.length == 1){ //单hook 或 只有1个元素
eval(`${fcAddrStr}=hookData.origFcAddr`); //还原
this.fcHookMap.delete(fcAddrStr);
return true;
}else{ //多层
hookDataArray.splice(i, 1); //删除当前记录
this.fcHookMap.set(fcAddrStr, hookDataArray);
return true;
}
}
#unHookCommon_mode(fcAddrStr, newFcAddr, hookData, hookDataArray, i){
if(newFcAddr != undefined && hookData.newFcAddr == newFcAddr){ //unHookByName //取消指定key某一层hook
return this.#unHookCommon_unload(fcAddrStr, hookData, hookDataArray, i);
}else if(newFcAddr == undefined){ //unAllHookByName //取消指定key下的所有hook
return this.#unHookCommon_unload(fcAddrStr, hookData, hookDataArray, i);
}
}
//取消hook, 主要针对多层hook
//unHook(key, 新函数地址)
//unHook(key)
//如果指定newFcAddr, 则取消指定key某一层hook
//如果只指定fcAddrStr, 则取消指定key下的所有hook
unHookByNameOfSpecifyOrAll(fcAddrStr, newFcAddr){
// debugger
if(Object.prototype.toString.apply(fcAddrStr) != '[object String]'){
Console.orig.log("unHookByNameOfSpecifyOrAll()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
return false;
}
if(this.isHook(fcAddrStr)){
let fcAddr = eval(fcAddrStr);
let type = Object.prototype.toString.apply(fcAddr);
let key = fcAddrStr;
if(Hook.fnTypeArray.includes(type)){
if(this.fcHookMap.has(key)){
let hookDataArray = this.fcHookMap.get(key);
for (let i = hookDataArray.length -1; i >= 0; i--) { //逆序遍历
const hookData = hookDataArray[i];
let ret = this.#unHookCommon_mode(fcAddrStr, newFcAddr, hookData, hookDataArray, i);
if(newFcAddr != undefined && ret){ //单unHook, 并且执行成功, 则直接返回
return true;
}else if(newFcAddr == undefined && i == 0){ //否则是 取消指定key下的所有hook, 就判断是否循坏完成, 循坏完成后返回
return true;
}
}
Console.orig.log("unHook()失败, 找到hook, 但是实参newFcAddr有误: " + newFcAddr.name);
return false;
}else{
Console.orig.log("unHook()失败, 找到hook, 但是实参key有误: " + key);
return false;
}
}else{
Console.orig.log("unHook()失败, 不支持的type: " + type);
return false;
}
}else{
Console.orig.log("unHook()失败, 找不到指定的hook!");
return false;
}
}
unAllHook(){ //取消所有
for (const [key, hookDataArray] of this.fcHookMap) { //遍历map
for (let i = hookDataArray.length -1; i >= 0; i--) { //逆序遍历
const hookData = hookDataArray[i];
this.#unHookCommon_mode(key, undefined, hookData, hookDataArray, i);
}
}
return true;
}
}
class DynamicProxy {
static apply(obj, fcName, ...args) { //执行目标对象的方法(对象, 方法名, 方法参数)
let prototype = Object.getPrototypeOf(obj);
if(prototype.hasOwnProperty(fcName)) {
return eval("prototype." + fcName).apply(obj, args);
}
}
static autoWired(mainObj, targetClass, CallBack) { //依赖注入
for (const [name, value] of Object.entries(mainObj)) { //遍历对象获取指定类型的属性, 属性的类型通过参数传入
if(value instanceof targetClass){
return CallBack(name, value); //CallBack(属性名, 属性值)
}
}
return null;
}
}
class CodeTemplate {
static hook = new Hook();
static autoWiredProxy(mainObj, executeFcName){ //依赖注入代理
DynamicProxy.autoWired(mainObj, Performance, (name, value)=>{
//使用这个属性调用apply实现具体的功能
DynamicProxy.apply(value, executeFcName); //apply(属性值(对象), 要执行的方法)
return true;
});
}
static autoWired_Performance(fcAddrStr, mainObj){
const fcStartStub = (origFcAddr, ...args)=>{
//mainObj.perfor.setStartTime(); //直接调用
//DynamicProxy.apply(mainObj.perfor, "setStartTime"); //间接调用
CodeTemplate.autoWiredProxy(mainObj, "setStartTime"); //依赖注入
};
const fcEndStub = (origFcAddr, ret, ...args)=>{
//mainObj.perfor.getEndTime(); //直接调用
//DynamicProxy.apply(mainObj.perfor, "getEndTime",); //间接调用
CodeTemplate.autoWiredProxy(mainObj, "getEndTime"); //依赖注入
};
CodeTemplate.hook.setHook(fcAddrStr, fcStartStub, fcEndStub);
}
}
const URLTYPE = {
LINK: 1, //链接
FUZZYMATCH: 1 << 1, //模糊匹配
RegExp: 1 << 2, //正则表达式
}
class URL {
static test(testURL, urlTYPE, urlRule) {
if(urlTYPE == URLTYPE.LINK){
return urlRule.includes(testURL); //子串
}else if(urlTYPE == URLTYPE.FUZZYMATCH){
let targetURL = urlRule;
//转义可能的特殊字符 //\和/这种字符必须首先执行 //*和?这种通配符放在后面单独处理
let specharsArray = ['\\', '/', '[', ']', '(', ')', '{', '}', '^', '$', '-', '.', '+', '|', ',', ':', '=', '!', '<', '%'];
specharsArray.forEach((item,index)=> {
targetURL = targetURL.replaceAll(item, '\\' + item);
});
//替换通配符 //?必须比*先执行
targetURL = targetURL.replaceAll("?", "[\\s\\S]?");
targetURL = targetURL.replaceAll("*", "([\\s\\S]+)?");
//测试URL
let regExp = new RegExp(targetURL);
return regExp.test(testURL);
}else if(urlTYPE == URLTYPE.RegExp){
return urlRule.test(testURL);
}
}
}
class CallBack {
//---------- 通用 ----------
static commonInit(){
//加载库-css
Utils.loadjscssFile(GM_getResourceURL("css_fontAwesome"), FILETYPE.CSS);
Utils.addNewStyle("css_layui", CustomUtils.handleCSSCompatibility(CSSCompatibilityFlag, GM_getResourceText("css_layui")));
//加载自定义-css
//Utils.addNewStyle("CustomCSS", commonCSS + pageExtendBaseCSS + MainAppBaseCSS + MainAppCSS);
Utils.addNewStyle("commonCSS", commonCSS);
Utils.addNewStyle("pageExtendBaseCSS", pageExtendBaseCSS);
Utils.addNewStyle("MainAppBaseCSS", MainAppBaseCSS);
Utils.addNewStyle("MainAppCSS", MainAppCSS);
//加载库-js
Utils.addNewScript("js_localforage", GM_getResourceText("js_localforage"));
Utils.addNewScript("js_layui", CustomUtils.handleJSCompatibility(CSSCompatibilityFlag, GM_getResourceText("js_layui")));
}
static commonReayd() {
//执行初始化
localforage.config({
name: 'MortalApp',
storeName: 'AppDB',
version: '1.0',
description: 'default database',
});
}
//---------- 详细页 ----------
static reportInit() {
CallBack.commonInit();
//加载模板
//addElementsByHTMLTemplateText(settingUITemplate);
Utils.addElementsByHTMLTemplateText(catalogUIBase);
return CallBack.reportReady();
}
static reportReady() {
//执行初始化
CallBack.commonReayd();
PageExtend.init();
MainApp.init();
return true;
}
static async reportPageRun() {
await PageExtend.run();
await MainApp.run();
return true;
}
//---------- 主页 ----------
static mainPageInit() {
CallBack.commonInit();
//加载模板
return CallBack.mainPageReady();
}
static mainPageReady() {
//执行初始化
CallBack.commonReayd();
return true;
}
static async mainPageRun() {
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));
try {
//牌谱解析页面,默认使用最新的Mortal
await localforage.setItem("Mortal_New", childEle[0].value);
await localforage.setItem("Mortal_Type", jsonStr);
} catch (err) {
Console.orig.error("存储数据时发生错误: \n" + err);
}
return true;
}
}
class App {
static perfor = new Performance("dom");
static URLRuleSet = [{
name: "牌谱解析页面",
url: /^https?:\/\/mjai.ekyu.moe\/report\/[A-Za-z0-9-_]+.html/,
urlType: URLTYPE.RegExp,
exclude: [
],
init: CallBack.reportInit,
entry: CallBack.reportPageRun,
},{
name: "主页",
url: /^https?:\/\/mjai.ekyu.moe\/?([A-Za-z0-9-_]+.html)?/,
urlType: URLTYPE.RegExp,
exclude: [
//排除主动访问非页面URL的内容,比如图片等
/^https?:\/\/mjai.ekyu.moe\/?([A-Za-z0-9-_]+)?\/?[A-Za-z0-9-_]+.(?!.*(html|htm|jsp|php|asp))/,
],
init: CallBack.mainPageInit,
entry: CallBack.mainPageRun,
},
]; //URL规则集
constructor(){
this.init();
}
init() {
document.addEventListener('readystatechange', event => {
if (event.target.readyState === 'interactive') {
App.perfor.setStartTime();
//Console.orig.log('interactive');
this.yun();
//initLoader();
}
else if (event.target.readyState === 'complete') {
App.perfor.getEndTime();
//Console.orig.log('complete');
//initApp();
}
});
return true;
}
yun(){
let isExclude = false;
for (const [index, item] of App.URLRuleSet.entries()) {
let currentURL = window.location.origin + window.location.pathname;
if(URL.test(currentURL, item.urlType, item.url)){ //测试URL是否匹配
if(item.exclude.length > 0){
for (const [i, excludeItem] of item.exclude.entries()) {
if(URL.test(currentURL, item.urlType, excludeItem)){ //测试URL是否被排除
isExclude = true;
break;
}
}
}
if(!isExclude){
if(item.init.apply())
item.entry.apply();
}
return;
}
}
}
}
// ---------------------------- 功能定义 ----------------------------
class MortalBase {
constructor(){
}
static init(){
return true;
}
static run(){
}
static stop(){
}
static clean(){
}
}
const OUTSTYLE = {
A: 1,
B: 1 << 1,
C: 1 << 2,
}
class PageExtend extends MortalBase {
static perfor = new Performance("PageExtend");
constructor(){
//super();
}
static init(){
//Console.orig.log("PageExtend.init()");
if(Debug.getDebug){
CodeTemplate.autoWired_Performance("PageExtend.run", PageExtend);
}
return true;
}
static async run(){
//Console.orig.log("PageExtend.run()");
const [result1, result2, result3] =
//显示恶手 (收集数据) //起始信息详细化 (独立) (重要) //列出选择权重 (独立)
await Promise.all([PageExtend.showBadChoose(), PageExtend.showStartInfo(), PageExtend.showChooseWeight()]);
//console.log(result1, result2, result3);
//修改 元数据 选项卡 (数据统计需要在showBadChoose()之后执行)
await PageExtend.alterMetaData();
}
static stop(){
}
static clean(){
}
//显示恶手
static async showBadChoose() {
//Console.orig.log("MainApp.showBadChoose()");
const orderLossEleArray = document.getElementsByClassName("order-loss");
for (let i = 0; i < orderLossEleArray.length; i++) {
const orderLoss = orderLossEleArray[i];
const nChooseIndex = parseInt(orderLoss.innerText.match(/[\d]+/)[0]);
const nChooseSum = parseInt(orderLoss.nextSibling.textContent.match(/[\d]+/)[0]);
const turnInfo = orderLoss.parentElement;
const summary = turnInfo.parentElement;
const collapseEntry = summary.parentElement;
const table = collapseEntry.lastChild.firstChild;
const tbody = table.lastChild;
const nChooseTR = tbody.childNodes[nChooseIndex -1];
const nChooseWeightTD = nChooseTR.lastChild;
const chosenWeight = parseFloat(nChooseWeightTD.innerHTML.replace(/<.*?>/g, "")); //过滤html标签, 只保留文字内容
const kyokuTitleEle = collapseEntry.parentElement.parentElement.firstChild.getElementsByTagName("a")[0];
if (chosenWeight <= parseFloat(badMoveUpperLimit)) { //严重恶手
const badChooseNode = document.createElement("span");
badChooseNode.classList.add("badChoose");
badChooseNode.classList.add("level1");
badChooseNode.innerHTML = ` \u00A0\u00A0\u00A0${i18nText.badMove}${i18nText.badMoveUp}`;
turnInfo.appendChild(badChooseNode);
collapseEntry.classList.add("collapseEntryL1");
badChooseNum++;
//收集数据3-恶手
let nameID = "badChoose-1-" + strArray2.length;
collapseEntry.id = nameID + "-main"; //设置id
strArray2.push({
name: kyokuTitleEle.textContent + " " + summary.textContent,
href: kyokuTitleEle.href.match(/#[\S\s]+$/)[0],
type: "id",
nameID: nameID
});
}else if (chosenWeight <= parseFloat(badMoveUpperLimitCustom)) { //普通恶手
const badChooseNode = document.createElement("span");
badChooseNode.classList.add("badChoose");
badChooseNode.classList.add("level2");
badChooseNode.innerHTML = ` \u00A0\u00A0\u00A0${i18nText.badMove}${i18nText.badMoveDown}`;
turnInfo.appendChild(badChooseNode);
collapseEntry.classList.add("collapseEntryL2");
badChooseNumCustom++;
//收集数据3-恶手
let nameID = "badChoose-2-" + strArray2.length;
collapseEntry.id = nameID + "-main"; //设置id
strArray2.push({
name: kyokuTitleEle.textContent + " " + summary.textContent,
href: kyokuTitleEle.href.match(/#[\S\s]+$/)[0],
type: "id",
nameID: nameID
});
}
//收集数据2-不一致
let nameID = "discord-" + strArray3.length;
collapseEntry.setAttribute("name", nameID + "-main"); //设置name
strArray3.push({
name: kyokuTitleEle.textContent + " " + summary.textContent,
href: kyokuTitleEle.href.match(/#[\S\s]+$/)[0],
type: "name",
nameID: nameID
});
} //for
//Console.orig.log("end MainApp.showBadChoose()");
}
//修改 元数据 选项卡
static async alterMetaData() {
//Console.orig.log("MainApp.alterMetaData()");
let mortalMap = null;
let mortal_New = null;
try {
// 新增 显示 Mortal 版本
const jsonStr = await localforage.getItem("Mortal_Type");
mortal_New = await localforage.getItem("Mortal_New");
if(jsonStr != null) {
let obj = Object.entries(JSON.parse(jsonStr));
mortalMap = new Map(obj);
}
} catch (err) {
Console.orig.error("获取数据时发生错误: \n" + err);
}
// 修改 元数据 选项卡
let metaData = null;
const detailsElements = document.getElementsByTagName("details");
for (let i = 0; i < detailsElements.length; i++) {
const details = detailsElements[i];
const summary = details.firstChild;
if (summary.firstChild.textContent == i18nText.metaData) {
metaData = details;
metaData.toggleAttribute("open", true); //打开 元数据 选项卡
break;
}
}
const metaDataDL = metaData.lastChild;
let matchRatioDD = null;
let version = null;
for (let i = 0; i < metaDataDL.childNodes.length; i++) {
const metaDataChild = metaDataDL.childNodes[i];
if(metaDataChild.nodeName == "DT" && metaDataChild.textContent == i18nText.modelTag) {
let ele = metaDataDL.childNodes[i + 1];
//判断当前是否是最新版本的mortal
if(mortal_New != null) {
if(mortal_New != ele.innerText) {
let aiEle = metaDataDL.childNodes[i - 1];
aiEle.classList.add("color1");
aiEle.innerText = aiEle.innerText + ` \u00A0\u00A0\u00A0${i18nText.badMoveError}`;
}
}
//处理当前版本
if(mortalMap != null) {
let mortalValue = mortalMap.get(ele.innerText);
if(mortalValue != undefined) {
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].classList.add("color2"); //rating
metaDataDL.childNodes[i-1].classList.add("color2");
metaDataDL.childNodes[i].classList.add("color2"); //AI 一致率
metaDataDL.childNodes[i+1].classList.add("color2");
break;
}
}
const matchRatioText = matchRatioDD.textContent;
const chooseNumStr = matchRatioText.substring(matchRatioText.indexOf("/") + 1);
const chooseNum = parseInt(chooseNumStr);
const badChooseRatioDT = document.createElement("dt");
badChooseRatioDT.classList.add("color3");
badChooseRatioDT.innerHTML = i18nText.badMoveRatio + i18nText.badMoveNull + badMoveUpperLimit + i18nText.badMoveSymbol;
const badChooseRatioDD = document.createElement("dd");
badChooseRatioDD.classList.add("color3");
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.classList.add("color3");
badChooseRatioDT2.innerText = i18nText.badMoveRatio + i18nText.badMoveNull + badMoveUpperLimitCustom + i18nText.badMoveSymbol;
const badChooseRatioDD2 = document.createElement("dd");
badChooseRatioDD2.classList.add("color3");
badChooseRatioDD2.innerHTML = `${badChooseNumCustom}/${chooseNum} = ${(100 * badChooseNumCustom / chooseNum).toFixed(3)}%`;
metaDataDL.insertBefore(badChooseRatioDD2, version);
metaDataDL.insertBefore(badChooseRatioDT2, badChooseRatioDD2);
//Console.orig.log("end MainApp.alterMetaData()");
}
//起始信息详细化
static async showStartInfo() {
//Console.orig.log("MainApp.showStartInfo()");
/* 起始信息详细化 */
function parmeHandle(eastScoreChange, southScoreChange, westScoreChange, northScoreChange) {
let scoreArray = [{sc: eastScoreChange, i: 0}, {sc: southScoreChange, i: 1}, {sc: westScoreChange, i: 2}, {sc: northScoreChange, i: 3}];
let newScoreArray = scoreArray.filter((obj) => {
return obj.sc != 0;
});
let scoreAddArray = newScoreArray.filter((obj) => { //荣和的玩家
return obj.sc > 0;
});
let scoreSubArray = newScoreArray.filter((obj) => { //放铳的玩家
return obj.sc < 0;
});
scoreAddArray.sort((a,b)=>{return b.sc-a.sc});
return {scoreArray: scoreArray, newScoreArray: newScoreArray, scoreAddArray: scoreAddArray, scoreSubArray: scoreSubArray};
}
function handleRon(kyoku, startPlayerIndex, eastScoreChange, southScoreChange, westScoreChange, northScoreChange) { //处理荣和
let obj = parmeHandle(eastScoreChange, southScoreChange, westScoreChange, northScoreChange);
let scoreAddArray = obj.scoreAddArray;
let scoreSubArray = obj.scoreSubArray;
let selfPlayerIndex = getPlayerIndexByPlayerSeatName(getPlayerSeatNameByPlayerIndex(startPlayerIndex, kyoku, OUTSTYLE.B), OUTSTYLE.B);
//
let str = "";
for (let i = 0; i < scoreAddArray.length; i++) {
const scoreAdd = scoreAddArray[i];
let scAddPlayerSeatName = getPlayerSeatNameByPlayerIndex(scoreAdd.i, kyoku, OUTSTYLE.B); //荣和的玩家
let scAddPlayerViewName = getSelfViewPlayerNameByTargetPlayerIndex(selfPlayerIndex, getPlayerIndexByPlayerSeatName(scAddPlayerSeatName, OUTSTYLE.B));
let scSubPlayerSeatName = getPlayerSeatNameByPlayerIndex(scoreSubArray[0].i, kyoku, OUTSTYLE.B); //放铳的玩家
let scSubPlayerViewName = getSelfViewPlayerNameByTargetPlayerIndex(selfPlayerIndex, getPlayerIndexByPlayerSeatName(scSubPlayerSeatName, OUTSTYLE.B));
str += scAddPlayerSeatName + ` (${scAddPlayerViewName}) ` +
`<span class="color4">${i18nText.Ron}</span>` + i18nText.badMoveNull +
scSubPlayerSeatName + ` (${scSubPlayerViewName}) ` +
"+" + scoreAdd.sc + i18nText.badMoveNull + scoreSubArray[0].sc;
}
return str;
}
function handleTsumo(kyoku, startPlayerIndex, eastScoreChange, southScoreChange, westScoreChange, northScoreChange) { //处理自摸
}
function handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange, southScoreChange, westScoreChange, northScoreChange) { //处理流局
}
function getTextByOutStyle(outStyle, index) {
if(outStyle == OUTSTYLE.A)
return eval("i18nText.seatTypeA" + index); //seatTypeA0 //东起
else if(outStyle == OUTSTYLE.B)
return eval("i18nText.seatTypeB" + index); //seatTypeB0 //东家
else if(outStyle == OUTSTYLE.C)
return eval("i18nText.seatTypeC" + index); //seatTypeC0 //自家
else{
console.warn("outStyle是无效的!");
}
}
function getSelfViewPlayerNameByTargetPlayerIndex(selfIndex, targetPlayerIndex) {
const viewMap = new Map();
let viewArray = [
{ value: -3, name: i18nText.seatTypeC1 }, //下家
{ value: -2, name: i18nText.seatTypeC2 }, //对家
{ value: -1, name: i18nText.seatTypeC3 }, //上家
{ value: 0, name: i18nText.seatTypeC0 }, //自家
{ value: 1, name: i18nText.seatTypeC1 }, //下家
{ value: 2, name: i18nText.seatTypeC2 }, //对家
{ value: 3, name: i18nText.seatTypeC3 }, //上家
];
viewArray.forEach((item,index)=> {
viewMap.set(item.value, item.name);
});
const getValue = (offset) => {
if(offset > 0)
return offset -4;
else
return offset +4;
}
let offset = targetPlayerIndex - selfIndex;
let result;
do {
result = viewMap.get(offset);
} while (offset = getValue(offset), result == undefined);
return result;
}
function getPlayerIndexByPlayerSeatName(playerSeatName, outStyle) {
let seatArray;
if(outStyle == OUTSTYLE.A)
seatArray = [i18nText.seatTypeA0, i18nText.seatTypeA1, i18nText.seatTypeA2, i18nText.seatTypeA3]; //东起, 南起, 西起, 北起
else if(outStyle == OUTSTYLE.B)
seatArray = [i18nText.seatTypeB0, i18nText.seatTypeB1, i18nText.seatTypeB2, i18nText.seatTypeB3]; //东家, 南家, 西家, 北家
else{
console.warn("outStyle是无效的!");
seatArray = [i18nText.seatTypeA0, i18nText.seatTypeA1, i18nText.seatTypeA2, i18nText.seatTypeA3]; //东起, 南起, 西起, 北起
}
const seatMap = new Map();
seatArray.forEach((item,index)=> {
seatMap.set(item, index);
});
return seatMap.get(playerSeatName);
}
function getPlayerSeatNameByPlayerIndex(playerIndex, kyoku, outStyle) {
let seatArray;
if(outStyle == OUTSTYLE.A)
seatArray = [i18nText.seatTypeA0, i18nText.seatTypeA3, i18nText.seatTypeA2, i18nText.seatTypeA1]; //东起, 北起, 西起, 南起
else if(outStyle == OUTSTYLE.B)
seatArray = [i18nText.seatTypeB0, i18nText.seatTypeB3, i18nText.seatTypeB2, i18nText.seatTypeB1]; //东家, 北家, 西家, 南家
else{
console.warn("outStyle是无效的!");
seatArray = [i18nText.seatTypeA0, i18nText.seatTypeA3, i18nText.seatTypeA2, i18nText.seatTypeA1]; //东起, 北起, 西起, 南起
}
const seatMap = new Map();
seatArray.forEach((item,index)=> {
seatMap.set(item, index);
});
const getValueByEachArray = (array, startIndex, eachCount) => {
let length = array.length;
let targetIndex = startIndex;
for (let i = eachCount; i > 0; i--) {
if(++targetIndex >= length)
targetIndex = 0;
}
return array[targetIndex];
}
const getStart = (playerIndex, kyoku, outStyle) => {
switch (playerIndex + kyoku) {
case 0:
return getTextByOutStyle(outStyle, 0); //东起
case 1:
return getTextByOutStyle(outStyle, 1); //南起
case 2:
return getTextByOutStyle(outStyle, 2); //西起
case 3:
return getTextByOutStyle(outStyle, 3); //北起
default:
return getStart(playerIndex, kyoku -4, outStyle);
}
}
const get = (playerIndex, kyoku, outStyle) => {
let startIndex = seatMap.get(getStart(playerIndex, 0, outStyle));
return getValueByEachArray(seatArray, startIndex, kyoku);
}
if(kyoku == 0)
return getStart(playerIndex, kyoku, outStyle);
else
return get(playerIndex, kyoku, outStyle);
}
const summaryEle = document.getElementsByClassName("kyoku-toc")[0];
summaryEle.classList.add("position_re");
for (let j = 0; j < summaryEle.children.length; j++) {
const summary = summaryEle.children[j];
summary.classList.add("min-w-20"); //
}
summaryEle.children[1].classList.add("position_ab");
summaryEle.children[1].classList.add("l-196px"); //
const kyokuEle = summaryEle.getElementsByTagName("a");
const endInfoEle = summaryEle.getElementsByClassName("end-status");
//
const section = document.getElementsByTagName("section");
for (let i = 0, length = section.length; i != length; ++i) {
const titleEle = section[i].children[0];
const titleKyokuEle = titleEle.getElementsByTagName("a"); //只有1个元素
const titleEndInfoEle = titleEle.getElementsByClassName("end-status"); //只有1个元素
const tenhouData = section[i].getElementsByTagName("iframe")[0].src;
const playerIndexStr = tenhouData.match(/tw=[0-3]/)[0];
const startPlayerIndex = parseInt(playerIndexStr.substring(playerIndexStr.length -1)); //起始玩家索引
const json = JSON.parse(decodeURI(tenhouData.substring(tenhouData.indexOf("{")))); //天凤对局数据
const kyoku = json.log[0][0][0]; //局数
const count = json.log[0][0][1]; //本场数
const currScore = json.log[0][1]; //当前点数
const scoreChange = json.log[0][json.log[0].length -1]; //点数变动
const endMode = scoreChange[0];
//解析规则
if(matchRule.isInit == false) {
const disp = json.rule.disp;
if(disp.indexOf("間") != -1) { //雀魂
matchRule.isRon3 = true;
//Console.orig.log("雀魂牌谱");
}else if(disp.indexOf("Player") != -1) { //一番街
matchRule.isRon3 = false;
//Console.orig.log("一番街牌谱");
}else{ //默认为天凤
matchRule.isRon3 = false;
//Console.orig.log("默认为天凤牌谱(包括自定义牌谱)");
}
matchRule.isInit = true;
}
//四家当前分数
const eastScore = currScore[0]; //东
const southScore = currScore[1]; //南
const westScore = currScore[2]; //西
const northScore = currScore[3]; //北
//四家分数变化(直接) //送棒的-1000没有显示
let eastScoreChange = [];
let southScoreChange = [];
let westScoreChange = [];
let northScoreChange = [];
let ronCount = (scoreChange.length -1) / 2;
if(scoreChange.length > 1) { //比如九种九牌, 是没有分数变化的数据的
//是否有多家荣和
for (let j = 0; j < ronCount; j++) { //处理可能的多家荣和
eastScoreChange.push(scoreChange[1+ j*2][0]); //东
southScoreChange.push(scoreChange[1+ j*2][1]); //南
westScoreChange.push(scoreChange[1+ j*2][2]); //西
northScoreChange.push(scoreChange[1+ j*2][3]); //北
}
}
//判断模式
if(endMode == tenhouText.Ron) { //自摸、荣和
let str = "";
for (let j = 0; j < ronCount; j++) { //处理可能的多家荣和
if(j>0) //处理多家
str += ", ";
let obj = parmeHandle(eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
if(eastScoreChange[j] == 0 ||
southScoreChange[j] == 0 ||
westScoreChange[j] == 0 ||
northScoreChange[j] == 0) { //如果有任何一家分数变动为0, 则为荣和
str += handleRon(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
}if(obj.scoreAddArray.length == 3) { //判断是否是三家荣和
if(matchRule.isRon3) //如果启用了三家和了的规则 //? 可能是没有必要的判断? 等待使用三种游戏牌谱分别进行查证
str += handleRon(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
else{ //流局 //? 可能是没有必要的判断? 等待使用三种游戏牌谱分别进行查证
// let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
// Console.orig.log(str);
}
}else{ //否则都是自摸
// str += handleTsumo(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
}
}//for
if(str.length > 0) {
const span = document.createElement("span");
span.innerHTML = ` \u00A0\u00A0\u00A0` + str;
endInfoEle[i].parentElement.appendChild(span);
let lineNum = CustomUtils.getTextLineNum(span.parentNode);
if(lineNum > 1){ //如果新添加的文字有多行, 则进行对齐
for (let j = 1; j < lineNum; j++) {
summaryEle.children[0].insertBefore(document.createElement("br"), kyokuEle[i].parentElement.nextElementSibling);
}
}
}
}else if(endMode == tenhouText.Ryuukyoku){ //荒牌流局 //流局, 如果有分数改变则处理
// let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
// Console.orig.log(str);
}else if(endMode == tenhouText.RyuukyokuTsumo) { //流局满贯 (等同于自摸8000)
// let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
// Console.orig.log(str);
}else if(endMode == tenhouText.RyuukyokuType1) { //九种九牌 //流局, 如果有分数改变则处理
// let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
// Console.orig.log(str);
}else{ //四风连打、四杠散了、四家立直 //三家和了 //流局, 如果有分数改变则处理
// let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
// Console.orig.log(str);
}
const span = document.createElement("span");
let str = getPlayerSeatNameByPlayerIndex(startPlayerIndex, kyoku, OUTSTYLE.A);
span.innerText = ` \u00A0\u00A0\u00A0` + str;
span.classList.add("position_ab");
span.classList.add("l-130px");
if(str == i18nText.seatTypeA0) //东起
span.classList.add("color5");
else
span.classList.add("color6");
kyokuEle[i].parentElement.appendChild(span);
titleKyokuEle[0].innerText += `\u00A0\u00A0` + str;
titleKyokuEle[0].id = titleKyokuEle[0].href.match(/#[\S\s]+$/)[0] + "-main"; //设置id
}//for
//Console.orig.log("end MainApp.showStartInfo()");
}
//列出选择权重
static async showChooseWeight() {
//Console.orig.log("MainApp.showChooseWeight()");
/* 列出选择权重 */
const defaultHandleFunc = (newNode, differData, index, colorStr) => {
newNode.style.color = colorStr; //设置为目标颜色
newNode.innerHTML = ` \u00A0\u00A0\u00A0` + eval("i18nText.badMoveDiffer" + index) + differData;
}
const map = new Map(); //使用map保证重置循坏后的唯一性
const boxObjStr = '{"left":0,"top":0}';
const entry = document.getElementsByClassName("collapse entry");
let spanSelf, spanMortal;
for (let i = 0, length = entry.length; i != length; ++i) {
entry[i].classList.add("position_re");
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;
let selfPaiData = 0;
let mortalPaiData = 0;
let 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;
spanSelf = document.createElement("span");
spanSelf.innerText = ` \u00A0\u00A0\u00A0` + data;
spanSelf.classList.add("position_ab");
if(selfBoxObj.top == 0) {
if(!isNaN(selfPai.offsetTop)) {
selfBoxObj.top = (selfPai.offsetTop + selfPai.offsetHeight / 2 - 10);
}else if(!isNaN(selfPai.parentElement.offsetTop)) {
selfBoxObj.top = (selfPai.parentElement.offsetTop + selfPai.parentElement.offsetHeight / 2 - 10);
}else{
console.error("[dom struct inconsistency] source:", "selfPai");
console.info("[debug]", `i: ${i}`);
}
spanSelf.style.top = selfBoxObj.top + 2 + "px";
}
spanSelf.classList.add("l-170px");
entry[i].insertBefore(spanSelf, entry[i].childNodes[3].nextSibling);
selfPai = null; //置null, 防止继续计算
}else if(mortalPaiStr == dataPaiStr) { //如果目标操作是Mortal的操作
mortalPaiData = data;
spanMortal = document.createElement("span");
spanMortal.innerText = ` \u00A0\u00A0\u00A0` + data;
spanMortal.classList.add("position_ab");
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);
}else{
console.error("[dom struct inconsistency] source:", "mortalPai");
console.info("[debug]", `i: ${i}`);
}
spanMortal.style.top = mortalBoxObj.top + 1 + "px";
}
spanMortal.classList.add("l-170px");
if(Object.prototype.toString.call(mortalPai.nextSibling) == '[object Text]') { //如果有多张牌图片,就使用最后一张牌图片后面的文字的位置
mortalPai = mortalPai.nextSibling;
}
entry[i].insertBefore(spanMortal, 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选择的差值 */
if(mortalPaiData == selfPaiData) //忽略自己和mortal打出的牌一样的结果
continue;
const differData = Math.constructor.roundEx(Math.abs(mortalPaiData - selfPaiData), 5); //保留5位小数
const turnInfo = entry[i].children[0].children[0];
const newNode = document.createElement("span");
newNode.classList.add("font_weight_400");
if (differData < 5) { //微差
defaultHandleFunc(newNode, differData, 1, "#000"); //黑色
}else if (differData < 10) { //小幅差距
defaultHandleFunc(newNode, differData, 2, "#996633"); //褐色
}else if (differData < 20) { //低等差距
defaultHandleFunc(newNode, differData, 3, "#009966"); //淡绿
}else if (differData < 40) { //中等差距
defaultHandleFunc(newNode, differData, 4, "#3399FF"); //淡蓝
}else if (differData < 60) { //高等差距
defaultHandleFunc(newNode, differData, 5, "#3333CC"); //深蓝
}else if (differData < 80) { //大幅度差距
defaultHandleFunc(newNode, differData, 6, "#CC0099"); //淡红
}else{ //压倒性差距
defaultHandleFunc(newNode, differData, 7, "#f00"); //红色
}
turnInfo.appendChild(newNode);
let lineNum = CustomUtils.getTextLineNum(turnInfo);
let diffValue = Math.abs(roleEle[0].offsetTop - spanSelf.offsetTop);
if(lineNum > 1 && diffValue >= 5){ //如果新添加的文字有多行, 则进行对齐 //移动设备x轴分辨率较小,并且各种设备分辨率不同,就会导致可能不需要对齐
let lineHeight = CustomUtils.getStyleOfLineHeight(turnInfo);
let origTopSelf = parseFloat(spanSelf.style.top);
let origTopMortal = parseFloat(spanMortal.style.top);
spanSelf.style.top = origTopSelf + lineHeight + "px";
spanMortal.style.top = origTopMortal + lineHeight + "px";
}
}//for
//Console.orig.log("end MainApp.showChooseWeight()");
}
}
class MainApp extends MortalBase {
static perfor = new Performance("MainApp");
constructor(){
//super();
}
static init(){
//Console.orig.log("MainApp.init()");
if(Debug.getDebug){
CodeTemplate.autoWired_Performance("MainApp.run", MainApp);
}
return true;
}
static async run(){
//Console.orig.log("MainApp.run()");
MainApp.createCatalogUI();
}
static stop(){
}
static clean(){
}
//
static createCatalogUI() {
//Console.orig.log("MainApp.createCatalogUI()");
const summaryEle = document.getElementsByClassName("kyoku-toc")[0];
//数据处理
let catalogUI = document.getElementById("catalogUI");
layui.use(function(){
let element = layui.element;
let laytpl = layui.laytpl;
let $ = layui.$;
//收集数据1-对局
let strArray1 = [];
let array = summaryEle.children[0].children;
let length = summaryEle.children[0].children.length;
for (let j = 0; j < length; j++) {
const ele = array[j];
if(ele.children.length == 0)
continue; //过滤<br>等没有子节点的单标签
strArray1.push({
name: ele.children[0].innerText + (ele.children[1] != undefined ? (" " + ele.children[1].innerText.trim()) : ""),
type: "id",
nameID: ele.children[0].href.match(/#[\S\s]+$/)[0]
});
}
let data = { //模板数据
selector1: strArray1, //对局选择器
selector2: strArray2, //恶手选择器
selector3: strArray3, //不一致选择器
};
//使用模板进行解析
let compile = laytpl(catalogUITemplate); // 模板解析
compile.render(data, (htmlStr)=>{
$('#catalogUI').html(htmlStr);
}); // 模板渲染
// 渲染导航组件
element.render('nav', 'selector-filter-nav');
/* 新增右置对局选择器和切换器(固定/可滑动呼出) */
let catalogUIBuf = document.getElementById("catalogUIBuf");
let selectorGroups = document.getElementById("selectorGroups");
let selector1_a = document.querySelectorAll("#selector1 a"); //对局选择器 //name->id
let selector2_a = document.querySelectorAll("#selector2 a"); //恶手选择器 //id
let selector3_a = document.querySelectorAll("#selector3 a"); //不一致选择器 //name
let section = document.getElementsByTagName("section");
let eleSection = section[0];
selectorGroups.style.height = (document.scrollingElement.clientHeight - 100 - catalogUIBuf.offsetHeight) + "px"; //滚动条
catalogUI.style.left = (eleSection.offsetLeft + eleSection.offsetWidth + 60) + "px"; //x位置
let vTop = (eleSection.offsetTop - catalogUI.offsetHeight);
catalogUI.style.top = 60 + "px"; //y位置
/* 将页面滚动导致的元素可见性改变与选择器绑定 */
const scrollCallBack = (e) => {
let section = document.getElementsByTagName("section");
let scrollTop = document.scrollingElement.scrollTop;
let offset = document.scrollingElement.clientHeight / 2;
for (let i = 0; i < section.length; i++) {
if ((i == 0 && scrollTop +offset < section[1].offsetTop) || //第一个节点高度范围 //0~第二个元素之间的高度 //小于第二个元素的高度
(i == section.length -1 && scrollTop >= section[section.length -2].offsetTop) || //最后一个节点高度范围 //大于倒数第二个元素的高度
(scrollTop +offset >= section[i].offsetTop && scrollTop +offset < section[i+1].offsetTop)) { //中间的节点高度范围 //相邻的两个节点之间
const titleEle = section[i].children[0];
const titleKyokuEle = titleEle.getElementsByTagName("a"); //只有1个元素
// Console.orig.log("可见: " + titleKyokuEle[0].innerText, titleKyokuEle[0].id);
let targetEle = document.getElementById(titleKyokuEle[0].id.replace("-main", ""));
setNewActivateEle(targetEle);
return;
}
}
};
window.addEventListener("scroll", CustomUtils.throttle(scrollCallBack, 100));
scrollCallBack();
/* 绑定选择器事件 */
let scrollHandle = (targetNode, verticalAlign)=>{
// document.scrollingElement.scrollTop = targetNode.offsetTop;
if(globalSettings.AppFunctionSettings.scrollAnimation){
// 页面动画滚动过度 A标签高亮
targetNode.scrollIntoView({
behavior: "smooth",
block: verticalAlign || "start",
inline: "start",
});
}else{
//页面直接滚动不过度 A标签高亮
targetNode.scrollIntoView({
behavior: "instant", //动画
block: verticalAlign || "start", //垂直对齐方式
inline: "start", //水平对齐方式
});
}
};
let nameIdSelectorHandle = function(e) {
let idStr = e.target.name;
let targetNode = document.getElementById(idStr + "-main");
if(targetNode == null)
return Console.orig.error("nameIdSelectorHandle", "获取到的目标节点为空!");
if(targetNode.id.indexOf("kyoku") != -1){
//高亮显示
let parentEle = targetNode.parentElement.parentElement.parentElement;
let changeEle = parentEle.getElementsByClassName("sticky")[0];
changeEle.style.background = "transparent";
parentEle.classList.add("highlight");
setTimeout(()=>{
parentEle.classList.remove("highlight");
changeEle.style.background = "";
}, 750);
scrollHandle(targetNode, "start");
}else{
//高亮显示
targetNode.classList.add("highlight");
setTimeout(()=>{
targetNode.classList.remove("highlight");
}, 750);
scrollHandle(targetNode, "center");
}
};
let idSelectorHandle = function(e) {
let idStr = e.target.id;
let targetNode = document.getElementById(idStr + "-main");
if(targetNode == null)
return Console.orig.error("idSelectorHandle", "获取到的目标节点为空!");
if(targetNode.id.indexOf("kyoku") != -1){
//高亮显示
let parentEle = targetNode.parentElement.parentElement.parentElement;
let changeEle = parentEle.getElementsByClassName("sticky")[0];
changeEle.style.background = "transparent";
parentEle.classList.add("highlight");
setTimeout(()=>{
parentEle.classList.remove("highlight");
changeEle.style.background = "";
}, 750);
scrollHandle(targetNode, "start");
}else{
//高亮显示
targetNode.classList.add("highlight");
setTimeout(()=>{
targetNode.classList.remove("highlight");
}, 750);
scrollHandle(targetNode, "center");
}
};
let nameSelectorHandle = function(e) {
let idStr = e.target.name;
let targetNode = document.getElementsByName(idStr + "-main");
if(targetNode == null)
return Console.orig.error("nameSelectorHandle", "获取到的目标节点为空!");
if(targetNode[0].id.indexOf("kyoku") != -1){
//高亮显示
let parentEle = targetNode.parentElement.parentElement.parentElement;
let changeEle = parentEle.getElementsByClassName("sticky")[0];
changeEle.style.background = "transparent";
parentEle.classList.add("highlight");
setTimeout(()=>{
parentEle.classList.remove("highlight");
changeEle.style.background = "";
}, 750);
scrollHandle(targetNode[0], "start");
}else{
//高亮显示
targetNode[0].classList.add("highlight");
setTimeout(()=>{
targetNode[0].classList.remove("highlight");
}, 750);
scrollHandle(targetNode[0], "center");
}
};
//对局选择器
for (let i = 0; i < selector1_a.length; i++) {
const ele = selector1_a[i];
ele.addEventListener('click', idSelectorHandle);
}
//恶手选择器
for (let i = 0; i < selector2_a.length; i++) {
const ele = selector2_a[i];
ele.addEventListener('click', idSelectorHandle);
}
//不一致选择器
for (let i = 0; i < selector3_a.length; i++) {
const ele = selector3_a[i];
ele.addEventListener('click', nameSelectorHandle);
}
/* 绑定按钮事件 */
document.getElementById("but_kyoku_prev").addEventListener("click", function(e){
let ele = document.getElementsByClassName("ui layui-this")[0];
if(ele == undefined)
return Console.orig.error("click", "#but_kyoku_prev", "获取到的目标节点为空!");
let nextEle = ele.previousElementSibling;
if(nextEle != null){
nextEle = nextEle.children[0];
setNewActivateEle(nextEle);
nextEle.click();
}
}, false);
document.getElementById("but_kyoku_next").addEventListener("click", function(e){
let ele = document.getElementsByClassName("ui layui-this")[0];
if(ele == undefined)
return Console.orig.error("click", "#but_kyoku_next", "获取到的目标节点为空!");
let nextEle = ele.nextElementSibling;
if(nextEle != null){
nextEle = nextEle.children[0];
setNewActivateEle(nextEle);
nextEle.click();
}
}, false);
document.getElementById("but_diff_prev").addEventListener("click", function(e){
}, false);
document.getElementById("but_diff_next").addEventListener("click", function(e){
}, false);
});
//Console.orig.log("end MainApp.createCatalogUI()");
}
//
static createSettingUI() {
}
}
const app = new App();
})(unsafeWindow);