Mortal界麵美化、功能增強

美化界麵,允許自定義背景、牌背等,添加噁手率、牌效計算等

// ==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  --------------------------------------------//
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址