// ==UserScript==
// @name Mortal GUI Appeareance Improvement
// @name:zh Mortal界面美化、功能增强
// @name:zh-CN Mortal界面美化、功能增强
// @name:zh-TW Mortal界麵美化、功能增強
// @description Improve the appearance of mortal killerducky GUI
// @description:zh 美化界面,允许自定义背景、牌背等,添加恶手率、牌效计算等
// @description:zh-CN 美化界面,允许自定义背景、牌背等,添加恶手率、牌效计算等
// @description:zh-TW 美化界麵,允許自定義背景、牌背等,添加噁手率、牌效計算等
// @version 2.0.1
// @namespace Mortal Appearance
// @author CiterR
// @icon https://mjai.ekyu.moe/favicon-32x32.png
// @match *://mjai.ekyu.moe/killerducky/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
/*
--------------------------- BUG ---------------------------
☑1.暗杠dora显示问题,和.float元素div的overflow有关
☑2.牌效计算时提前计入了未开宝指
☑3.牌效悬浮窗没响应鼠标移出事件时导致的驻留
-------------------------- TODO ---------------------------
1.牌效不计算7对
2.计算改良(性能允许情况)
3.计算一向听的好型率
☑4.优化计算恶手率的启动方式
5.添加吃碰牌时的牌效计算
*/
//-------------------------------------------- CSS Part should start here --------------------------------------------//
function mortalAddStyle() {
let css = `
/*All the URL in this script shouldn't provide to users*/
.grid-main {
background-position: center;/*桌布居中*/
/*background-position-x: 0px;/*水平调整*/
/*background-position-y: 0px;/*垂直调整*/
/*background-size: 145%; /*桌布缩放控制*/
border-radius: 15px;
border: 2px solid pink;
}/*添加桌布*/
.grid-info {
border: 2px;
border-color: white;
border-style: solid;
border-radius: 24px;
background: #93adae;
z-index: 3; /*和牌顶3D模拟配合使用*/
}/*中央信息板 */
.killer-call-img {
position: relative;
top: 50px;
scale: 1.2;
}/*Mortal外观调整*/
html {
height: 98%;
}/*避免滚动条*/
body {
/*background: white;*/
background: linear-gradient(90deg, #2351ff8a, #0bfff7, #fff, #e7eaa7c9, #ff3e4f);
height: 98%;
}/*网页颜色更改*/
.outer {
margin-left: -100px;
}/*主页面左偏置*/
.opt-info {
margin-left: 90px;
}/*指示栏偏置*/
.opt-info table {
border-radius: 15px;
background: #74abb6;
box-shadow: 4px -4px 6px 1px #f6f6f6;
}/*指示栏样式调整*/
/* img[src="media/Regular_shortnames/back.svg"]{
content: url('');
}/*牌背设置*/
.grid-hand {
background: hsl(0deg 0% 100% / 0%);
}/*对手手牌区透明化*/
.grid-hand-p3 {
height: 530px;
}/*上家鸣牌位置调整*/
.grid-hand-p0-container{
background: hsl(0deg 0% 100% / 0%);
scale: 1.15;
width: 555px; /*手牌宽度减少,避免穿模,可能会引发问题*/
position: relative;
left: -15px;
top: 50px;
}/*手牌区透明化及放大调整*/
.tileImg{
border-radius: 4px;
/*border-top: 3px groove #bbc9d9;/*牌顶3D模拟,效果不好*/
}/*麻将牌修改:圆角*/
.killer-call-bars > svg > rect, .discard-bars > svg > rect {
rx: 2px;
}/*绿条切割矩形:圆角*/
main{
/*scale: 1.2;*/
top: 50px;
position: relative;
}/*主页面放大*/
.info-doras {
scale: 1.4;
}/*Dora显示加大*/
.info-round {
background: hsl(192.97deg 17.21% 42.16%);
border-color: transparent;
border-radius: 15px;
}/*场次切换器*/
.info-this-round-modal{
background: hsl(190deg 100% 20%);
border-width: 3px;
border-radius: 10px;
border-style:solid;
border-color:unset;
}/*对局报告器*/
.close {
background: red;
scale: 1.2;
border: 0px;
border-radius: 50%;
right: 5px;
width: 20px;
height: 20px;
}/*对局报告器关闭按钮*/
.killer-call-bars{
scale: 1.5;
position: relative;
left: 20px;
top: 20px;
border-radius: 20px;
background:hsl(190deg 31.45% 58.49%);
box-shadow: 5px 5px 6px 1px #f6f6f6;
}/*何切栏放大*/
.killer-call-bars > svg > text:nth-child(2) {
fill: #f72727;
}/*第一选标红*/
/*.killer-call-bars > svg > rect[x="4.5"], rect[x="14.5"], rect[x="24.5"] {
stroke: red;
}
.killer-call-bars > svg {
height: 116px;
}/*第一选红框显示*/
.sidebar{
margin-left: 60px;
justify-content: flex-start;
align-content; center;
flex-direction: column;
}
.sidebar > * {
margin:5px;
}/*右侧栏样式*/
.controls {
background: hsl(190deg 49.75% 89.34% / 36%);
border-radius: 20px;
height: 325px;
box-shadow: 5px 5px 6px 1px #f6f6f6;
}/*右侧控制板*/
.controls > * {
margin: 5px;
color: black;
border-color: white;
border-radius: 15px;
background: #74abb6;
width: 115px;
}/*控制板按钮样式*/
.tileImg:hover {
background: #cdcbcb;
}/*悬浮选牌*/
.modal, button {
border-radius: 10px;
}/*选项、关于窗口*/
#about-modal {
background: linear-gradient(45deg, hsl(190deg 100% 20%), hsl(190 100% 30% / 1), hsl(190 100% 40% / 1));
}/*关于窗口背景*/
.newSetting {
height: 50px;
width: 150px;
}/*新添加按钮调整*/
.opt-info table .tileImg {
width: calc(var(--tile-img-width)*0.7);
height: auto;
position: relative;
top: 5px;
}/*调整Mortal候选牌大小*/
.wider-table td {
height: 36px;
padding-top: 2px;
padding-bottom: 2px;
}/*配合上一条缩窄排版*/
#about-body-0 > li:last-child > span {
display: none;
}
#about-body-0 > li:last-child:after {
content: '如有BUG,关闭此脚本 / Disable Script When BUG';
}/*声明修改*/
`
GM_addStyle(css)
}
//-------------------------------------------- CSS Part should end here --------------------------------------------//
//-------------------------------------------- Extra Functions should start here --------------------------------------------//
/*全局变量*/
const standardTileHeight = 20; //牌张大小常数
const standardTileWidth = standardTileHeight / 4 * 3;
let timer = null; //消抖定时器
function listenerAdder(strips) { //给出高度条相对百分比
let maxStripHeight = 1;
strips.forEach(e=>{
if(e.getAttribute('width') !== '20') {
maxStripHeight = Math.max(e.getAttribute('height'), maxStripHeight);
}
});
strips.forEach(e=>{
if (e.getAttribute('width') !== '10') return;
const showHoverWin = ()=>{ //对高度条设置鼠标悬浮响应事件
let p0Element = document.querySelector(".opt-info > table:last-child tr:nth-of-type(2) > td:last-child");
let p0 = parseFloat(p0Element.innerText) / 100;
let normProb = e.getAttribute('height') / maxStripHeight; //归一化公式 n(x)=sqrt(x/p0)
let realProb = p0 * (normProb ** 2); //逆操作还原
let pos = e.getBoundingClientRect();
let tooltip = document.createElement('div');
tooltip.className = 'hoverInfo';
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = '#7dbcc980';
tooltip.style.border = '1px solid white';
tooltip.style.padding = '5px';
tooltip.style.borderRadius = '5px'; // 设置悬浮窗的样式 Hover window styles
tooltip.textContent = (realProb * 100).toFixed(2) + '%';
tooltip.style.top = `${pos.y - 40}px`;
tooltip.style.left = `${pos.x - 25}px`;
e.style.opacity = '0.6';
document.body.appendChild(tooltip); // 将悬浮窗添加到页面中
const deleteTooltip = ()=>{ // 给悬浮窗绑定鼠标移出事件
e.style.opacity = '1';
tooltip.remove(); // 移除悬浮窗
e.removeEventListener('mouseout', deleteTooltip);
}
e.addEventListener('mouseout', deleteTooltip);
}
e.addEventListener('mouseover', showHoverWin);
});
};
function mortalOptionColorize(errTolerance = [ 1, 5, 10, -1 ]) { //最后一个参数-1,为绝对值恶手,>0为比值恶手
let actionTable = document.querySelector(".opt-info > table:last-child");
let actionTrList = actionTable.querySelectorAll("tr");
let actionCardList = new Array(); // 第一个是无用项
let possibilityList = new Array();
let lastTr = actionTrList[actionTrList.length - 1];
lastTr.querySelector("td:first-child").style.borderBottomLeftRadius = "15px";
lastTr.querySelector("td:last-child").style.borderBottomRightRadius = "15px";
// 设置表格底部圆角
actionTrList.forEach(e=>{
let cardAct = e.querySelector("td:first-child > span");
let action, card;
if (cardAct != null) {
action = cardAct.textContent.substring(0, 1); //获取牌操作
}
let cardImg = e.querySelector("td:first-child > span > img");
if (cardImg != null) {
let cardURL = cardImg.getAttribute('src');
card = cardURL.substring(
cardURL.lastIndexOf('/')+1, cardURL.lastIndexOf('.')); //获取出牌选择
}
actionCardList.push(action + card);
let possibilityTr = e.querySelector("td:last-child");
if (possibilityTr.textContent != 'P') {
possibilityList.push(possibilityTr.textContent);// 获取概率数据
}
});
//获取玩家选择和Mortal一选
let actionCard = new Array();
let mainActionSpan = document.querySelectorAll(".opt-info > table:first-child span");
mainActionSpan.forEach(e=>{
let action = e.textContent.substring(0, 1);//操作
let card;
let cardImg = e.querySelector('img');
if (cardImg != null) {
let cardURL = cardImg.getAttribute('src');
card = cardURL.substring(cardURL.lastIndexOf('/')+1, cardURL.lastIndexOf('.'));//牌张
}
actionCard.push(action + card);
});
let possibilityPlayer = 0;
let playerSelect = 0;
//给玩家选择进行标记
for (let i = 1; i < actionCardList.length; i++) {
if (actionCardList[i] == actionCard[0]) {
actionTrList[i].style.background = "rgb(171, 196, 49)";
possibilityPlayer = parseFloat(possibilityList[i - 1]);
playerSelect = i - 1;
break;
}
}
//判断恶手并标红
let fatalErr = parseFloat(errTolerance[0]);
let normalErr = parseFloat(errTolerance[1]);
let arguableErr = parseFloat(errTolerance[2]);
let fatalErrEdge = parseFloat(errTolerance[3]);
let pRatio= parseFloat(possibilityPlayer) / parseFloat(possibilityList[0]);
let colorChoice = -1; //分别为红0、橙1、蓝2,及上方标记的黄绿-1
if (actionCard[0] != actionCard[1]) {
if (fatalErrEdge < 0) { //绝对值恶手
if (possibilityPlayer < fatalErr) colorChoice = 0;
else if (possibilityPlayer < normalErr) colorChoice = 1;
else if (possibilityPlayer < arguableErr) colorChoice = 2;
} else if (fatalErrEdge > 0) { //比值恶手
if (possibilityPlayer < fatalErrEdge) colorChoice = 0; //权重过小,直接判断恶手
else if (pRatio < fatalErr) colorChoice = 0;
else if (pRatio < normalErr) colorChoice = 1;
else if (pRatio < arguableErr) colorChoice = 2;
}
}
let playerSelectInMain = document.querySelectorAll('.discard-bars-svg > rect[width="20"]');
switch (colorChoice) {
case 0 :
actionTrList[playerSelect + 1].style.background = "red";
playerSelectInMain.forEach(e=>{ e.style.fill = "red"; });
break;
case 1 :
actionTrList[playerSelect + 1].style.background = "#ff5a00";
playerSelectInMain.forEach(e=>{ e.style.fill = "#ff5a00"; });
break;
case 2 :
actionTrList[playerSelect + 1].style.background = "blue";
playerSelectInMain.forEach(e=>{ e.style.fill = "blue"; });
break;
}
}
function createButtonBox(){
let settingOption = document.querySelector('.options-div');
let buttonBox = document.createElement('div');
buttonBox.style.display = 'flex';
buttonBox.className = 'buttonBox-div';
buttonBox.style.flexWrap = 'wrap';
buttonBox.style.width = '500px';
buttonBox.style.justifyContent = 'space-evenly';
settingOption.appendChild(buttonBox);
}
function backgroundSetting(){
let buttonBox = document.querySelector('.buttonBox-div');
let setBackgroundButton = document.createElement('button');
let backgroundURL = GM_getValue('backgroundPicUrl', 'https://backgroundURL.example');
let backgroundImg = document.createElement('img');
setBackgroundButton.className = 'newSetting';
buttonBox.appendChild(setBackgroundButton); //插入按钮
setBackgroundButton.textContent = '修改背景图';
setBackgroundButton.addEventListener('click', ()=>{
let inputURL = prompt('输入背景图URL', backgroundURL);
if (inputURL !== null) {
backgroundURL = inputURL.trim();
backgroundImg.src = backgroundURL;
GM_setValue('backgroundPicUrl', backgroundURL); //存储背景图链接
}
document.querySelector('.grid-main').style.backgroundImage = `url(${backgroundURL})`;
});
document.querySelector('.grid-main').style.backgroundImage = `url(${backgroundURL})`;
backgroundImg.src = backgroundURL; //设置被存储好的背景
backgroundImg.style.maxWidth = '200px';
backgroundImg.style.maxHeight = '200px';
backgroundImg.style.marginTop = '30px';
backgroundImg.style.justifySelf = 'center';
backgroundImg.onload = ()=>{ document.querySelector('.options-div').appendChild(backgroundImg); }
backgroundImg.onerror = ()=> {
console.log('Can not to load background pic');
document.querySelector('.grid-main').style.background = 'green';
}
}
function tileBackSetting(){
let buttonBox = document.querySelector('.buttonBox-div');
let setTileBackButton = document.createElement('button');
let tileBackURL = GM_getValue('tileBackPicURL', 'https://tilebackURL.example');
let tileBackImg = document.querySelectorAll('img[src="media/Regular_shortnames/back.svg"]');
setTileBackButton.className = 'newSetting';
buttonBox.appendChild(setTileBackButton); //插入按钮
setTileBackButton.textContent = '设置牌背';
setTileBackButton.addEventListener('click', ()=>{
let inputURL = prompt('输入牌背URL', tileBackURL);
if (inputURL !== null) {
tileBackURL = inputURL.trim();
GM_setValue('tileBackPicURL', tileBackURL); //存储牌背链接
}
let tilebackStyle = `img[src="media/Regular_shortnames/back.svg"]{
content: url('${tileBackURL}'); }`
GM_addStyle(tilebackStyle);
});
if (tileBackURL == 'https://tilebackURL.example') return;
let tilebackStyle = `img[src="media/Regular_shortnames/back.svg"]{
content: url('${tileBackURL}'); }`
GM_addStyle(tilebackStyle); //使用CSS添加
}
function logoSetting(){
let buttonBox = document.querySelector('.buttonBox-div');
let setLogoButton = document.createElement('button');
let logoURL = GM_getValue('logoURL', 'https://logoURL.example');
setLogoButton.className = 'newSetting';
buttonBox.appendChild(setLogoButton); //插入按钮
setLogoButton.textContent = '修改形象图';
setLogoButton.addEventListener('click', ()=>{
let inputURL = prompt('输入形象图URL', logoURL);
if (inputURL !== null) {
logoURL = inputURL.trim();
GM_setValue('logoURL', logoURL); //存储形象图链接
}
document.querySelector('.killer-call-img').src = `url(${logoURL})`;
});
if (logoURL !== 'https://logoURL.example') {
let logoStyle = `
.killer-call-img {
content: url('${logoURL}');
position: relative;
top: 50px;
scale: 1.2;
}`;
GM_addStyle(logoStyle);
}
}
function optInfoSwitch(){
let buttonBox = document.querySelector('.buttonBox-div');
let mortalOptionSwitch = document.createElement('button');
let mortalOpt = document.querySelector('.opt-info');
let outer = document.querySelector('.outer');
let state = GM_getValue('mortalOptionState', true);
mortalOptionSwitch.className = 'newSetting';
buttonBox.appendChild(mortalOptionSwitch); //插入按钮
if (!state) { //初始化按钮对应的状态
mortalOptionSwitch.textContent = '开启Mortal选项面板';
mortalOpt.style.display = 'none';
outer.style.marginLeft = '0px';
} else {
mortalOptionSwitch.textContent = '关闭Mortal选项面板';
mortalOpt.style.display = 'initial';
outer.style.marginLeft = '-100px';
}
mortalOptionSwitch.addEventListener('click', ()=>{
state = !state;
if (!state) {
mortalOptionSwitch.textContent = '开启Mortal选项面板';
mortalOpt.style.display = 'none';
outer.style.marginLeft = '0px';
} else {
mortalOptionSwitch.textContent = '关闭Mortal选项面板';
mortalOpt.style.display = 'initial';
outer.style.marginLeft = '-100px';
}
GM_setValue('mortalOptionState', state); //存储状态
});
}
function fullScreenEnlarge(){
let scaleArray = GM_getValue('scaleStr', '1.2, 1.35');
let scale = scaleArray.split(',');
let defaultScale = parseFloat(scale[0]);
let fullScreenScale = parseFloat(scale[1]);
addEventListener('keydown', (e)=>{ //进入全屏放大
if (e.key === 'F11') {
event.preventDefault();
document.documentElement.requestFullscreen();
}
});
addEventListener('fullscreenchange',()=>{
let mainInFull = document.querySelector('main');
if (!document.fullscreen) { //退出全屏重置
mainInFull.style.scale = `${defaultScale}`;
mainInFull.style.top = '50px';
} else {
mainInFull.style.scale = `${fullScreenScale}`;
mainInFull.style.top = '110px';
}
});
document.querySelector('.killer-call-img').addEventListener('click', ()=>{ //快捷全屏
if (!document.fullscreen){
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
});
}
function createStripsHoverWindow() {
let bars = document.querySelector('#discard-bars'); //设置监听svg监听
let observer = new MutationObserver((mutationList, observer)=>{
let strips = bars.querySelectorAll('.discard-bars-svg>rect');
listenerAdder(strips);
})
if (bars === null) {
console.log('SelectorError!');
} else {
observer.observe(bars, {childList: true, subtree: true}); //当svg被重置时更新选择器strips
}
let callBars = document.querySelector('.killer-call-bars');
let observerAdviser = new MutationObserver((mutationList, observerAdviser)=>{
mutationList.forEach(e=>{
if (e.type === 'childList') {
let stripsAdviser = document.querySelectorAll('.killer-call-bars>svg>rect');
listenerAdder(stripsAdviser);
let remainWindow = document.querySelectorAll(".hoverInfo");
remainWindow.forEach(w=>{ w.remove() }); //svg更新时清空浮窗
}
});
});
observerAdviser.observe(callBars, {childList: true});
}
function startMortalOptionObserver(errTolerance) {
let optState = GM_getValue('mortalOptionState', true);
// if (!optState) return; //关闭状态不设置监听
let optInfo = document.querySelector('.opt-info');
let observerInfo = new MutationObserver(
(mutationList, observerInfo)=>{
mortalOptionColorize(errTolerance);
}
); //设置mortal选项更新监听
observerInfo.observe(optInfo, {childList: true});
}
function setCustomErrTolerance() {
let buttonBox = document.querySelector('.buttonBox-div');
let setErrToleranceButton = document.createElement('button');
let errToleranceStr = GM_getValue('errToleranceStr', '1, 5, 10, -1');
let errTolerance = errToleranceStr.split(',');
setErrToleranceButton.className = 'newSetting';
buttonBox.appendChild(setErrToleranceButton); //插入按钮
setErrToleranceButton.textContent = '自定义恶手率';
setErrToleranceButton.addEventListener('click', ()=>{
let explainText ='输入恶手率组合,四个参数 (刷新后生效)\n' +
'x4=-1为绝对模式,低于权重直接判定\n' +
'x4> 0为比值模式,与一选相除再判定'
let inputStr = prompt(explainText, errToleranceStr);
if (inputStr !== null) {
let input = inputStr.replace(',',','); //替换中文逗号
let numArray = input.split(',');
let newErrTolerance = numArray.map(Number);
if (newErrTolerance.length !== 4) {
alert('参数数量不一致!');
return;
}
GM_setValue('errToleranceStr', inputStr); //存储恶手率字符串
errToleranceStr = inputStr;
}
});
return errTolerance;
}
function addTableRow(table, str, value) {
const tr = table.insertRow();
let cell = tr.insertCell();
cell.textContent = `${str}`;
cell = tr.insertCell();
cell.textContent = `${value}`;
}
function setMainAreaEnlarge() {
let buttonBox = document.querySelector('.buttonBox-div');
let scaleButton = document.createElement('button');
let scaleStr = GM_getValue('scaleStr', '1.2, 1.35');
let scaleArray = scaleStr.split(',');
document.querySelector('main').style.scale = `${scaleArray[0]}`;//应用放大
scaleButton.className = 'newSetting';
buttonBox.appendChild(scaleButton); //插入按钮
scaleButton.textContent = '界面放大倍数';
scaleButton.addEventListener('click', ()=>{
let explainText ='输入放大倍数组合,四个参数 (刷新后生效)\n' +
'第一个参数,非全屏状态的放大倍数\n' +
'第二个参数,全屏状态下的放大倍数'
let inputStr = prompt(explainText, scaleStr);
if (inputStr !== null) {
let input = inputStr.replace(',',','); //替换中文逗号
let numArray = input.split(',');
let newScaleArray = numArray.map(Number);
if (newScaleArray.length !== 2) {
alert('参数数量不一致!');
return;
}
GM_setValue('scaleStr', inputStr); //存储倍数字符串
scaleStr = inputStr;
}
});
return scaleArray;
}
async function errCalculate(errTolerance) {
let fatalErrCnt = 0;
let normalErrCnt = 0;
let arguableErrCnt = 0;
/* 感谢脚本Mortal Killer Plus作者sabertaz的数据获取思路
const urlParams = new URLSearchParams(window.location.search);
const dataURL = urlParams.get("data");
const response = await fetch(dataURL);
const data = await response.json();
const reviewData = data.review; */
async function waitReview() {
return new Promise((resolve) => {
const check = setInterval(() => {
if (unsafeWindow.MM.GS.fullData.review) {
clearInterval(check); // 停止轮询
resolve(unsafeWindow.MM.GS.fullData.review); // 返回数据
} }, 500); //间隔500ms
});
}
const reviewData = await waitReview(); //由killerducky作者挂载的Debug信息
for (const kyokus of reviewData.kyokus) {
for (const curRound of kyokus.entries) {
const mismatch = !curRound.is_equal;
const pPlayer = curRound.details[curRound.actual_index].prob * 100;
const pMortal = curRound.details[0].prob * 100;
if (mismatch && parseFloat(errTolerance[3]) < 0) { //绝对值恶手
if (pPlayer <= parseFloat(errTolerance[0])) fatalErrCnt++;
if (pPlayer <= parseFloat(errTolerance[1])) normalErrCnt++;
if (pPlayer <= parseFloat(errTolerance[2])) arguableErrCnt++;
} else if (mismatch && parseFloat(errTolerance[3]) > 0) { //比值恶手
const pRate = parseFloat(pPlayer) / parseFloat(pMortal);
if (pPlayer <= parseFloat(errTolerance[3])) {
fatalErrCnt++;
normalErrCnt++;
arguableErrCnt++;
continue;
}
if (pRate <= parseFloat(errTolerance[0])) fatalErrCnt++;
if (pRate <= parseFloat(errTolerance[1])) normalErrCnt++;
if (pRate <= parseFloat(errTolerance[2])) arguableErrCnt++;
}
}
}
const totalReviewed = reviewData.total_reviewed;
const fatalErrRate = ((fatalErrCnt / totalReviewed) * 100).toFixed(2);
const fatalErrStr = `${fatalErrCnt}/${totalReviewed} = ${fatalErrRate}%`;
const normalErrRate = ((normalErrCnt / totalReviewed) * 100).toFixed(2);
const normalErrStr = `${normalErrCnt}/${totalReviewed} = ${normalErrRate}%`;
const arguableErrRate = ((arguableErrCnt / totalReviewed) * 100).toFixed(2);
const arguableErrStr = `${arguableErrCnt}/${totalReviewed} = ${arguableErrRate}%`;
let metadataTable = document.querySelector(".about-metadata table:first-child");
let errRateZH = " 恶手率";
if (parseFloat(errTolerance[3]) < 0) errRateZH = "% 恶手率";
addTableRow(metadataTable, `${errTolerance[0]}${errRateZH}`, fatalErrStr);
addTableRow(metadataTable, `${errTolerance[1]}${errRateZH}`, normalErrStr);
addTableRow(metadataTable, `${errTolerance[2]}${errRateZH}`, arguableErrStr);
}
function addDoraFlash(doraIndicators, state) { //同时负责上一次dora特效的关闭:state=0
let doras = new Array();
doraIndicators.forEach(e =>{
let doraStr = '';
switch(e[1]) {
case 'z':
if(parseInt(e[0]) < 5) {
doraStr = `${ parseInt(e[0]) % 4 + 1 }z`; //东南西北
} else {
doraStr = `${ (parseInt(e[0]) - 4 ) % 3 + 5}z`; //白发中
} break;
default:
if (parseInt(e[0]) === 0) {
doraStr = `6${e[1]}`;//赤宝牌的特例
} else {
doraStr = `${ parseInt(e[0]) % 9 + 1 }${e[1]}`;
} break;
}
doras.push(doraStr);
});
if(state) doras.push('0m', '0p', '0s');
for (const dora of doras) {
let doraStyle;
if (state) {
doraStyle = `
.tileDiv:has(img[src="media/Regular_shortnames/${dora}.svg"]) {
position: relative;
overflow: hidden;
border-radius: 5px;
}
.tileDiv:has(img[src="media/Regular_shortnames/${dora}.svg"])::after {
content: '';
position: absolute;
inset: -40%;
background: linear-gradient(45deg, rgba(255,255,255,0) 40%, rgba(255, 255, 255, 0.7), rgba(255,255,255,0) 60%);
animation: doraFlash 2s infinite;
transform: translateY(-100%);
z-index: 1; /*解决失焦问题*/
}
@keyframes doraFlash {
to {
transform: translateY(100%);
}
}`;
} else { //关闭伪元素显示和动画
doraStyle = `
.tileDiv:has(img[src="media/Regular_shortnames/${dora}.svg"]){
overflow: visible;
}
.tileDiv:has(img[src="media/Regular_shortnames/${dora}.svg"])::after {
content: none;
background: transparent;
animation: none;
}`;
}
GM_addStyle(doraStyle);
}
if (typeof addDoraFlash.executed === "undefined" || !addDoraFlash.executed) {
addDoraFlash.executed = false;
let rotatedDoraFix = `
.pov-p0 > div:has(.rotate) {
height: var(--tile-width);
align-self: flex-end;
}
.pov-p0 > div > .rotate {
transform: rotate(90deg) translate(calc(-1 * var(--tile-height)), 0px);
}
/*自家鸣牌立直调整*/
.pov-p1 > div:has(.rotate) {
width: var(--tile-width);
align-self: flex-end;
}/*下家鸣牌立直调整*/
.pov-p2 > div:has(.rotate) {
height: var(--tile-width);
}
.grid-discard-p2 > div:has(.rotate) {
align-self: flex-end;
}/*对家鸣牌立直调整*/
.pov-p3 > div:has(.rotate) {
width: var(--tile-width);
}
.grid-discard-p3 > div:has(.rotate) {
align-self: flex-end;
}/*上家鸣牌立直调整*/
.tileDiv:has(.tileImg.rotate.float) {
overflow:visible;
}/*修复自杠Dora4显示*/
`;
GM_addStyle(rotatedDoraFix);
}
}
function startDoraObserver(doraCheck_ms = 1500) { //事实上网页上挂载了window.MM.GS.gs.dora,但貌似无法监听
let preDoraIndicator = new Array();
const checkInterval = doraCheck_ms; //每隔x毫秒查询dora指示牌
const interval = setInterval(() => {
let doraInfo = document.querySelectorAll('.info-doras > div > img');
let doraIndicator = new Array();
doraInfo.forEach(e=>{
let cardURL = e.getAttribute('src');//获取doraIdr
let doraStr = cardURL.substring(cardURL.lastIndexOf('/')+1, cardURL.lastIndexOf('.'));
if (doraStr !== 'back') doraIndicator.push(doraStr);
});
if (!(function(doraIndicator, preDoraIndicator) { //数组相等的匿名函数
if (doraIndicator.length !== preDoraIndicator.length) return false;
for (let i = 0; i < doraIndicator.length; i++) {
if (doraIndicator[i] !== preDoraIndicator[i]) { return false; }};
return true;
})(doraIndicator, preDoraIndicator)) {
addDoraFlash(preDoraIndicator, false);//清除上一次的dora特效;
addDoraFlash(doraIndicator, true);
//console.log(preDoraIndicator, '----Updated To--->', doraIndicator);
preDoraIndicator = []; //保存当前dora
doraIndicator.forEach(d=>{ preDoraIndicator.push(d); });
}
}, checkInterval);
}
function startEfficencyCalc(calcDelay_ms = 800) {
let effEnable = GM_getValue('effEnable', true);
if (!effEnable) return;
const calcDelay = (mutationsList) => {
let effHover = document.querySelectorAll('.eff-hover');
effHover.forEach((e)=>{ e.remove(); }); //清除未正确移除的浮窗
if (mutationsList.length <= 1) return; //非摸牌更新
if (timer) clearTimeout(timer); //等待时间过短
timer = setTimeout(()=>{
timer = null;
calcEfficency(); //延迟后计算牌效,跳过快速浏览
}, calcDelay_ms);
};
const svgBarsDetector = new MutationObserver((mutations, observer) => {
const target = document.querySelector('.discard-bars-svg');
if (target) {
const startCalc = new MutationObserver(calcDelay);
startCalc.observe(target, { childList: true, subtree: false });
svgBarsDetector.disconnect();
}
});
svgBarsDetector.observe(document.body, { childList: true, subtree: true }); //等待bars-svg加载
}
function calcEfficency() { //负责函数调用
let cardInfo = getCardInfo();
if(!cardInfo) return; //不摸牌不计算
let shantenCnt = shanten(cardInfo.handset);
if(shantenCnt === -1) return; //和牌返回
let ukeireSet = kiruEfficency(cardInfo.handset, cardInfo.seenTiles);
addEffCardset(ukeireSet, shantenCnt);
}
function getCardInfo() {
let handcard = unsafeWindow.MM.GS.gs.hands[unsafeWindow.MM.GS.heroPidx];
let tsumocard = unsafeWindow.MM.GS.gs.drawnTile[unsafeWindow.MM.GS.heroPidx];
if (!tsumocard) return; //没有自摸牌
handcard.push(tsumocard);
let handset = new Array(5).fill().map(() => new Array(10).fill(0)); //矩阵化手牌
handcard.forEach(e=>{
let idx = Math.floor(e / 10), idy = e % 10; //添菜idy
if (idx === 5) {
idx = idy;
idy = 5;
} //处理红五
handset[idx][idy]++;
});
let seenTiles = new Array(5).fill().map(() => new Array(10).fill(0)); //已现牌组,不包括手牌
let calls = unsafeWindow.MM.GS.gs.calls;
let discardPond = unsafeWindow.MM.GS.gs.discardPond;
let doraIdr = unsafeWindow.MM.GS.gs.doraIndicator;
let doraInfo = document.querySelectorAll('.info-doras > div > img');
let doraCnt = 0;
doraInfo.forEach(e=>{
let cardURL = e.getAttribute('src');
let doraStr = cardURL.substring(cardURL.lastIndexOf('/')+1, cardURL.lastIndexOf('.'));
if (doraStr !== 'back') doraCnt++;
}); //获取dora数量,修正计入未开指示牌问题
for (let card of calls) {
if (typeof card !== 'number') continue;
let idx = Math.floor(card / 10), idy = card % 10;
if (idx === 5) {
idx = idy;
idy = 5;
}
seenTiles[idx][idy]++;
}
for (let ply of discardPond) {
ply.forEach(e=>{
let idx = Math.floor(e.tile / 10), idy = e.tile % 10;
if (idx === 5) {
idx = idy;
idy = 5;
}
seenTiles[idx][idy]++;
})
}
for (let i = 0; i < doraCnt; i++) {
let idr = doraIdr[i];
let idx = Math.floor(idr / 10), idy = idr % 10;
if (idx === 5) {
idx = idy;
idy = 5;
}
seenTiles[idx][idy]++;
}
for (let i = 1; i <= 4; i++) handset[i][0] = seenTiles[i][0] = i;
return { handset: handset, seenTiles: seenTiles };
}
function breakdown(A, depth) {
if (depth >= 4) return 0; //四张孤张剪枝
let ret = 0, i = 1;
while (i <= 9 && !A[i]) i++; //定位第一张
if (i > 9) return 0; //空白返回
if (i + 2 <= 9 && A[i] && A[i + 1] && A[i + 2] && A[0] != 4) {
A[i]--; A[i + 1]--; A[i + 2]--;
ret = Math.max(ret, breakdown(A, depth) + 2100);
A[i]++; A[i + 1]++; A[i + 2]++;
}
else {
if (i + 2 <= 9 && A[i] && A[i + 2] && A[0] != 4) {
A[i]--; A[i + 2]--;
ret = Math.max(ret, breakdown(A, depth) + 1001);
A[i]++; A[i + 2]++;
}
if (i + 1 <= 9 && A[i] && A[i + 1] && A[0] != 4) {
A[i]--; A[i + 1]--;
ret = Math.max(ret, breakdown(A, depth) + 1001);
A[i]++; A[i + 1]++;
}
}
if (A[i] >= 3) {
A[i] -= 3;
ret = Math.max(ret, breakdown(A, depth) + 2100);
A[i] += 3;
}
if (A[i] >= 2) {
A[i] -= 2;
ret = Math.max(ret, breakdown(A, depth) + 1010);
A[i] += 2;
}
A[i]--;
ret = Math.max(ret, breakdown(A, depth + 1));
A[i]++;
return ret;
}
function shantenStandard(S) {
let analysis = 0, cardnum = 0;
for (let A of S) {
let ret = 0;
for (let i = 1; i <= 9; i++) {
ret += A[i];
cardnum += A[i];
}
if (!ret) continue; //该类牌空
analysis += breakdown(A, 0);
}
let block = Math.floor(analysis % 1000 / 100);
let pair = Math.floor(analysis % 100 / 10);
let dazi = analysis % 10;
block += Math.floor((14 - cardnum) / 3); //处理鸣牌
if (pair > 1) {
dazi += pair - 1;
pair = 1;
} // 对搭转换
while (block + dazi > 4 && dazi > 0) dazi--; // 4N+2规则
return 8 - (2 * block + dazi + pair);
}
function shantenChiitoi(S) {
let pair = 0;
for (let A of S) {
for (let i = 1; i <= 9; i++) {
if (A[i] >= 2) pair++;
}
}
return 6 - pair;
}
function shanten(S) { return Math.min(shantenStandard(S), shantenChiitoi(S)); }
function ukeire(S, curShanten) {
let vaildcard = new Array(5).fill().map(() => new Array(10).fill(0)); //进张
for (let i = 0; i <= 4; i++) vaildcard[i][0] = i; //初始化
// let curShanten = shanten(S); /*外部传入向听数节省调用时间 */
for (let i = 1; i <= 3; i++) { //数牌
for (let j = 1; j <= 9; j++) {
let k = j - 2, acc = 0;
while (k < 1) k++;
while (k <= j + 2) {
if (k > 9) break;
acc += S[i][k++];
}
if (!acc) continue; //不摸孤张
S[i][j]++;
if (shanten(S) < curShanten) vaildcard[i][j]++;
S[i][j]--;
}
}
for (let j = 1; j <= 7; j++) { //字牌
if (!S[4][j]) continue;
S[4][j]++;
if (shanten(S) < curShanten) vaildcard[4][j]++;
S[4][j]--;
}
return vaildcard;
}
function kiruEfficency(S, seen) {
let ret = [];
let curShanten = shanten(S);
for (let i = 1; i <= 4; i++) {
for (let j = 1; j <= 9; j++) {
if (!S[i][j]) continue;
let pai, num = j.toString();
switch (i) {
case 1: pai = num + "m"; break;
case 2: pai = num + "p"; break;
case 3: pai = num + "s"; break;
case 4: pai = num + "z"; break;
}
S[i][j]--;
if (shanten(S) == curShanten) { //出牌向听不减
let vaild = ukeire(S, curShanten);
let left = tileleft(S, vaild, seen);
let vaildstr = convertToStr(vaild);
ret.push({ pai: pai, left: left, ukeStr: vaildstr, uke: vaild });
}
S[i][j]++;
}
}
ret.sort((a,b)=> b.left.leftNor - a.left.leftNor);
return ret;
}
function tileleft(S, uke, seen) {
let leftNor = 0, leftPure = 0;
for (let i = 1; i <= 4; i++) {
for (let j = 1; j <= 9; j++) {
if (!uke[i][j]) continue;
leftNor += 4 - S[i][j] - seen[i][j];
leftPure += 4 - S[i][j];
}
}
return { leftNor, leftPure };
}
function convertToStr(S) {
let str = '';
for (let i = 1; i <= 4; i++) {
let acc = 0;
for (let j = 1; j <= 9; j++) {
let tmp = S[i][j];
acc += tmp;
while (tmp--) str += j.toString();
}
if (!acc) continue;
switch (i) {
case 1: str += 'm'; break;
case 2: str += 'p'; break;
case 3: str += 's'; break;
case 4: str += 'z'; break;
}
}
return str;
}
function addEffCardset(ukeireSet, shantenCnt) {
let effWindow = document.querySelector('.efficency-call-div');
if (!effWindow) return; //没有创建窗口则取消
effWindow.innerHTML = ''; //清空内容
let shantenText = `${shantenCnt} 向听`;
if(!shantenCnt) shantenText = '听牌';
let showShanten = document.createElement('text');
showShanten.textContent = shantenText;
showShanten.style.textAlign = 'center';
showShanten.style.width = '100%';
showShanten.marginTop = '1%';
effWindow.appendChild(showShanten);
for (let ukeInfo of ukeireSet) {
let pai = ukeInfo.pai;
let tile = document.createElement('img');
let leftText = ukeInfo.left.leftNor.toString().padStart(2, '0') + ':'
+ ukeInfo.left.leftPure.toString().padStart(2, '0');
let showLeftText = document.createElement('text');
let wrapDiv = document.createElement('div');
tile.src = `media/Regular_shortnames/${pai}.svg`;
tile.className = 'tileImg effTile';
showLeftText.style.fontSize = 'xx-small';
showLeftText.style.lineHeight = '2';
showLeftText.style.marginLeft = '2px';
showLeftText.textContent = leftText;
wrapDiv.style.display = 'flex';
wrapDiv.style.marginLeft = '3%';
wrapDiv.style.marginTop = '1%'; /*样式设定*/
tile.addEventListener('mouseover', ()=> { //添加浮窗
let effHover = document.createElement('div');
let hoverPai, cnt = 0;
for (let i = 1; i <= 4; i++) {
for (let j = 1; j <= 9; j++) {
if (!ukeInfo.uke[i][j]) continue;
switch(i) {
case 1: hoverPai = j.toString() + 'm'; break;
case 2: hoverPai = j.toString() + 'p'; break;
case 3: hoverPai = j.toString() + 's'; break;
case 4: hoverPai = j.toString() + 'z'; break;
}
cnt++;
let hoverTile = document.createElement('img');
hoverTile.src = `media/Regular_shortnames/${hoverPai}.svg`;
hoverTile.className = 'tileImg hoverTile';
effHover.appendChild(hoverTile); //向浮窗添加进张
}
}
let posParent = effWindow.getBoundingClientRect();
let maxWidthcCnt = Math.min(13, cnt);
let posX = ( posParent.left + posParent.right - (standardTileWidth + 4) * maxWidthcCnt ) / 2;
let posY = posParent.top - Math.ceil(cnt / 13) * (standardTileHeight + 4) - 10;
effHover.style.width = `${maxWidthcCnt * (standardTileWidth + 4)}px`; //.tileImg样式中还有2px的padding
effHover.style.left = `${posX}px`
effHover.style.top = `${posY}px`
effHover.className = 'eff-hover';
document.body.appendChild(effHover);
const deleteEffHover = ()=>{ //清除监听器和窗口
effHover.remove();
tile.removeEventListener('mouseout', deleteEffHover);
};
tile.addEventListener('mouseout', deleteEffHover);
});
wrapDiv.appendChild(tile);
wrapDiv.appendChild(showLeftText);
effWindow.appendChild(wrapDiv);
}
}
function addEffWindow() { //添加牌效窗口
let buttonBox = document.querySelector('.buttonBox-div');
let efficencySwitch = document.createElement('button');
let effEnable = GM_getValue('effEnable', true);
efficencySwitch.className = 'newSetting';
buttonBox.appendChild(efficencySwitch); //插入按钮
if (!effEnable) { //初始化按钮对应的状态
efficencySwitch.textContent = '开启牌效计算';
} else {
efficencySwitch.textContent = '关闭牌效计算';
}
efficencySwitch.addEventListener('click', ()=>{
effEnable = !effEnable;
if (!effEnable) {
efficencySwitch.textContent = '开启牌效计算';
} else {
efficencySwitch.textContent = '关闭牌效计算';
}
GM_setValue('effEnable', effEnable); //存储状态
});
if (!effEnable) return; //不开启牌效计算,则仅添加按钮
let effDiv = document.createElement('div');
let killerCallDiv = document.querySelector('.killer-call-div');
let effCss = `
.efficency-call-div {
scale: 1.4;
width: calc(var(--zoom)*245px);
height: calc(var(--zoom)*110px);
background: hsl(190deg 31.45% 58.49%);
box-shadow: 5px 5px 6px 1px #f6f6f6;
border-radius: 20px;
margin-top: 34%;
margin-left: 14%;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
}
.eff-hover {
position: absolute;
display: flex;
flex-wrap: wrap;
scale: 1.5;
background: #00c0ff80;
box-shadow: 0px 0px 5px 5px #0090ff;
border-radius: 5px;
}
.effTile {
filter: none;
width: ${standardTileWidth}px;
height: ${standardTileHeight}px;
box-shadow: inset 0 0 2px #880000;
margin-left: 3%;
}
.hoverTile {
filter: none;
width: ${standardTileWidth}px;
height: ${standardTileHeight}px;
box-shadow: inset 0 0 2px #880000;
}`;
/* 分别是牌效窗口、进张悬浮窗、牌效张、浮窗张
在css字符串内加注释,会有bug,很神奇吧js */
GM_addStyle(effCss);
document.querySelector('.killer-call-img').style.display = 'none'; //关闭logo
effDiv.addEventListener('click', ()=>{ //代替logo的快捷全屏
if (!document.fullscreen) document.documentElement.requestFullscreen();
else document.exitFullscreen();
});
effDiv.className = 'efficency-call-div';
killerCallDiv.appendChild(effDiv);
}
//-------------------------------------------- Extra Functions should end here --------------------------------------------//
(function() {
//-------------------------------------------- Main Code should start here --------------------------------------------//
'use strict';
//↓↓↓一系列按钮及其功能
createButtonBox();
backgroundSetting();
tileBackSetting();
logoSetting();
optInfoSwitch();
setMainAreaEnlarge();
addEffWindow();
let errTolerance = setCustomErrTolerance();
//↑↑↑一系列按钮及其功能
mortalAddStyle(); //应用CSS样式
fullScreenEnlarge(); //全屏时缩放调整
createStripsHoverWindow(); //创建绿条悬浮窗
startMortalOptionObserver(errTolerance); //mortal选项染色
errCalculate(errTolerance); //计算恶手率
startDoraObserver(); //添加宝牌特效
startEfficencyCalc(); //启动牌效计算
//-------------------------------------------- Main Code should end here --------------------------------------------//
})();