Mortal界面美化、功能增强

美化界面,允许自定义背景、牌背等,添加恶手率、牌效计算等

  1. // ==UserScript==
  2. // @name Mortal GUI Appeareance Improvement
  3. // @name:zh Mortal界面美化、功能增强
  4. // @name:zh-CN Mortal界面美化、功能增强
  5. // @name:zh-TW Mortal界麵美化、功能增強
  6. // @description Improve the appearance of mortal killerducky GUI
  7. // @description:zh 美化界面,允许自定义背景、牌背等,添加恶手率、牌效计算等
  8. // @description:zh-CN 美化界面,允许自定义背景、牌背等,添加恶手率、牌效计算等
  9. // @description:zh-TW 美化界麵,允許自定義背景、牌背等,添加噁手率、牌效計算等
  10. // @version 2.0.1
  11. // @namespace Mortal Appearance
  12. // @author CiterR
  13. // @icon https://mjai.ekyu.moe/favicon-32x32.png
  14. // @match *://mjai.ekyu.moe/killerducky/*
  15. // @grant GM_addStyle
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // @grant unsafeWindow
  19. // @license MIT
  20. // ==/UserScript==
  21.  
  22. /*
  23. --------------------------- BUG ---------------------------
  24. ☑1.暗杠dora显示问题,和.float元素div的overflow有关
  25. ☑2.牌效计算时提前计入了未开宝指
  26. ☑3.牌效悬浮窗没响应鼠标移出事件时导致的驻留
  27.  
  28. -------------------------- TODO ---------------------------
  29. 1.牌效不计算7对
  30. 2.计算改良(性能允许情况)
  31. 3.计算一向听的好型率
  32. ☑4.优化计算恶手率的启动方式
  33. 5.添加吃碰牌时的牌效计算
  34. */
  35.  
  36. //-------------------------------------------- CSS Part should start here --------------------------------------------//
  37.  
  38. function mortalAddStyle() {
  39. let css = `
  40. /*All the URL in this script shouldn't provide to users*/
  41.  
  42. .grid-main {
  43. background-position: center;/*桌布居中*/
  44. /*background-position-x: 0px;/*水平调整*/
  45. /*background-position-y: 0px;/*垂直调整*/
  46. /*background-size: 145%; /*桌布缩放控制*/
  47. border-radius: 15px;
  48. border: 2px solid pink;
  49. }/*添加桌布*/
  50.  
  51. .grid-info {
  52. border: 2px;
  53. border-color: white;
  54. border-style: solid;
  55. border-radius: 24px;
  56. background: #93adae;
  57. z-index: 3; /*和牌顶3D模拟配合使用*/
  58. }/*中央信息板 */
  59.  
  60. .killer-call-img {
  61. position: relative;
  62. top: 50px;
  63. scale: 1.2;
  64. }/*Mortal外观调整*/
  65.  
  66. html {
  67. height: 98%;
  68. }/*避免滚动条*/
  69.  
  70. body {
  71. /*background: white;*/
  72. background: linear-gradient(90deg, #2351ff8a, #0bfff7, #fff, #e7eaa7c9, #ff3e4f);
  73. height: 98%;
  74. }/*网页颜色更改*/
  75.  
  76. .outer {
  77. margin-left: -100px;
  78. }/*主页面左偏置*/
  79.  
  80. .opt-info {
  81. margin-left: 90px;
  82. }/*指示栏偏置*/
  83.  
  84. .opt-info table {
  85. border-radius: 15px;
  86. background: #74abb6;
  87. box-shadow: 4px -4px 6px 1px #f6f6f6;
  88. }/*指示栏样式调整*/
  89.  
  90. /* img[src="media/Regular_shortnames/back.svg"]{
  91. content: url('');
  92. }/*牌背设置*/
  93.  
  94. .grid-hand {
  95. background: hsl(0deg 0% 100% / 0%);
  96. }/*对手手牌区透明化*/
  97.  
  98. .grid-hand-p3 {
  99. height: 530px;
  100. }/*上家鸣牌位置调整*/
  101.  
  102. .grid-hand-p0-container{
  103. background: hsl(0deg 0% 100% / 0%);
  104. scale: 1.15;
  105. width: 555px; /*手牌宽度减少,避免穿模,可能会引发问题*/
  106. position: relative;
  107. left: -15px;
  108. top: 50px;
  109. }/*手牌区透明化及放大调整*/
  110.  
  111. .tileImg{
  112. border-radius: 4px;
  113. /*border-top: 3px groove #bbc9d9;/*牌顶3D模拟,效果不好*/
  114. }/*麻将牌修改:圆角*/
  115.  
  116. .killer-call-bars > svg > rect, .discard-bars > svg > rect {
  117. rx: 2px;
  118. }/*绿条切割矩形:圆角*/
  119.  
  120. main{
  121. /*scale: 1.2;*/
  122. top: 50px;
  123. position: relative;
  124. }/*主页面放大*/
  125.  
  126. .info-doras {
  127. scale: 1.4;
  128. }/*Dora显示加大*/
  129.  
  130. .info-round {
  131. background: hsl(192.97deg 17.21% 42.16%);
  132. border-color: transparent;
  133. border-radius: 15px;
  134. }/*场次切换器*/
  135.  
  136. .info-this-round-modal{
  137. background: hsl(190deg 100% 20%);
  138. border-width: 3px;
  139. border-radius: 10px;
  140. border-style:solid;
  141. border-color:unset;
  142. }/*对局报告器*/
  143.  
  144. .close {
  145. background: red;
  146. scale: 1.2;
  147. border: 0px;
  148. border-radius: 50%;
  149. right: 5px;
  150. width: 20px;
  151. height: 20px;
  152. }/*对局报告器关闭按钮*/
  153.  
  154. .killer-call-bars{
  155. scale: 1.5;
  156. position: relative;
  157. left: 20px;
  158. top: 20px;
  159. border-radius: 20px;
  160. background:hsl(190deg 31.45% 58.49%);
  161. box-shadow: 5px 5px 6px 1px #f6f6f6;
  162. }/*何切栏放大*/
  163.  
  164. .killer-call-bars > svg > text:nth-child(2) {
  165. fill: #f72727;
  166. }/*第一选标红*/
  167.  
  168. /*.killer-call-bars > svg > rect[x="4.5"], rect[x="14.5"], rect[x="24.5"] {
  169. stroke: red;
  170. }
  171. .killer-call-bars > svg {
  172. height: 116px;
  173. }/*第一选红框显示*/
  174.  
  175. .sidebar{
  176. margin-left: 60px;
  177. justify-content: flex-start;
  178. align-content; center;
  179. flex-direction: column;
  180. }
  181. .sidebar > * {
  182. margin:5px;
  183. }/*右侧栏样式*/
  184.  
  185. .controls {
  186. background: hsl(190deg 49.75% 89.34% / 36%);
  187. border-radius: 20px;
  188. height: 325px;
  189. box-shadow: 5px 5px 6px 1px #f6f6f6;
  190. }/*右侧控制板*/
  191.  
  192. .controls > * {
  193. margin: 5px;
  194. color: black;
  195. border-color: white;
  196. border-radius: 15px;
  197. background: #74abb6;
  198. width: 115px;
  199. }/*控制板按钮样式*/
  200.  
  201. .tileImg:hover {
  202. background: #cdcbcb;
  203. }/*悬浮选牌*/
  204.  
  205. .modal, button {
  206. border-radius: 10px;
  207. }/*选项、关于窗口*/
  208.  
  209. #about-modal {
  210. background: linear-gradient(45deg, hsl(190deg 100% 20%), hsl(190 100% 30% / 1), hsl(190 100% 40% / 1));
  211. }/*关于窗口背景*/
  212.  
  213. .newSetting {
  214. height: 50px;
  215. width: 150px;
  216. }/*新添加按钮调整*/
  217.  
  218. .opt-info table .tileImg {
  219. width: calc(var(--tile-img-width)*0.7);
  220. height: auto;
  221. position: relative;
  222. top: 5px;
  223. }/*调整Mortal候选牌大小*/
  224.  
  225. .wider-table td {
  226. height: 36px;
  227. padding-top: 2px;
  228. padding-bottom: 2px;
  229. }/*配合上一条缩窄排版*/
  230.  
  231. #about-body-0 > li:last-child > span {
  232. display: none;
  233. }
  234. #about-body-0 > li:last-child:after {
  235. content: '如有BUG,关闭此脚本 / Disable Script When BUG';
  236. }/*声明修改*/
  237. `
  238. GM_addStyle(css)
  239. }
  240. //-------------------------------------------- CSS Part should end here --------------------------------------------//
  241.  
  242.  
  243. //-------------------------------------------- Extra Functions should start here --------------------------------------------//
  244.  
  245. /*全局变量*/
  246. const standardTileHeight = 20; //牌张大小常数
  247. const standardTileWidth = standardTileHeight / 4 * 3;
  248. let timer = null; //消抖定时器
  249.  
  250. function listenerAdder(strips) { //给出高度条相对百分比
  251. let maxStripHeight = 1;
  252. strips.forEach(e=>{
  253. if(e.getAttribute('width') !== '20') {
  254. maxStripHeight = Math.max(e.getAttribute('height'), maxStripHeight);
  255. }
  256. });
  257.  
  258. strips.forEach(e=>{
  259. if (e.getAttribute('width') !== '10') return;
  260.  
  261. const showHoverWin = ()=>{ //对高度条设置鼠标悬浮响应事件
  262. let p0Element = document.querySelector(".opt-info > table:last-child tr:nth-of-type(2) > td:last-child");
  263. let p0 = parseFloat(p0Element.innerText) / 100;
  264. let normProb = e.getAttribute('height') / maxStripHeight; //归一化公式 n(x)=sqrt(x/p0)
  265. let realProb = p0 * (normProb ** 2); //逆操作还原
  266. let pos = e.getBoundingClientRect();
  267.  
  268. let tooltip = document.createElement('div');
  269. tooltip.className = 'hoverInfo';
  270. tooltip.style.position = 'absolute';
  271. tooltip.style.backgroundColor = '#7dbcc980';
  272. tooltip.style.border = '1px solid white';
  273. tooltip.style.padding = '5px';
  274. tooltip.style.borderRadius = '5px'; // 设置悬浮窗的样式 Hover window styles
  275. tooltip.textContent = (realProb * 100).toFixed(2) + '%';
  276. tooltip.style.top = `${pos.y - 40}px`;
  277. tooltip.style.left = `${pos.x - 25}px`;
  278. e.style.opacity = '0.6';
  279. document.body.appendChild(tooltip); // 将悬浮窗添加到页面中
  280.  
  281. const deleteTooltip = ()=>{ // 给悬浮窗绑定鼠标移出事件
  282. e.style.opacity = '1';
  283. tooltip.remove(); // 移除悬浮窗
  284. e.removeEventListener('mouseout', deleteTooltip);
  285. }
  286. e.addEventListener('mouseout', deleteTooltip);
  287. }
  288.  
  289. e.addEventListener('mouseover', showHoverWin);
  290. });
  291. };
  292.  
  293. function mortalOptionColorize(errTolerance = [ 1, 5, 10, -1 ]) { //最后一个参数-1,为绝对值恶手,>0为比值恶手
  294. let actionTable = document.querySelector(".opt-info > table:last-child");
  295. let actionTrList = actionTable.querySelectorAll("tr");
  296.  
  297. let actionCardList = new Array(); // 第一个是无用项
  298. let possibilityList = new Array();
  299.  
  300. let lastTr = actionTrList[actionTrList.length - 1];
  301. lastTr.querySelector("td:first-child").style.borderBottomLeftRadius = "15px";
  302. lastTr.querySelector("td:last-child").style.borderBottomRightRadius = "15px";
  303. // 设置表格底部圆角
  304.  
  305. actionTrList.forEach(e=>{
  306. let cardAct = e.querySelector("td:first-child > span");
  307. let action, card;
  308. if (cardAct != null) {
  309. action = cardAct.textContent.substring(0, 1); //获取牌操作
  310. }
  311.  
  312. let cardImg = e.querySelector("td:first-child > span > img");
  313. if (cardImg != null) {
  314. let cardURL = cardImg.getAttribute('src');
  315. card = cardURL.substring(
  316. cardURL.lastIndexOf('/')+1, cardURL.lastIndexOf('.')); //获取出牌选择
  317. }
  318.  
  319. actionCardList.push(action + card);
  320.  
  321. let possibilityTr = e.querySelector("td:last-child");
  322. if (possibilityTr.textContent != 'P') {
  323. possibilityList.push(possibilityTr.textContent);// 获取概率数据
  324. }
  325. });
  326.  
  327. //获取玩家选择和Mortal一选
  328. let actionCard = new Array();
  329. let mainActionSpan = document.querySelectorAll(".opt-info > table:first-child span");
  330. mainActionSpan.forEach(e=>{
  331. let action = e.textContent.substring(0, 1);//操作
  332. let card;
  333. let cardImg = e.querySelector('img');
  334. if (cardImg != null) {
  335. let cardURL = cardImg.getAttribute('src');
  336. card = cardURL.substring(cardURL.lastIndexOf('/')+1, cardURL.lastIndexOf('.'));//牌张
  337. }
  338. actionCard.push(action + card);
  339. });
  340.  
  341. let possibilityPlayer = 0;
  342. let playerSelect = 0;
  343. //给玩家选择进行标记
  344. for (let i = 1; i < actionCardList.length; i++) {
  345. if (actionCardList[i] == actionCard[0]) {
  346. actionTrList[i].style.background = "rgb(171, 196, 49)";
  347. possibilityPlayer = parseFloat(possibilityList[i - 1]);
  348. playerSelect = i - 1;
  349. break;
  350. }
  351. }
  352.  
  353. //判断恶手并标红
  354. let fatalErr = parseFloat(errTolerance[0]);
  355. let normalErr = parseFloat(errTolerance[1]);
  356. let arguableErr = parseFloat(errTolerance[2]);
  357. let fatalErrEdge = parseFloat(errTolerance[3]);
  358. let pRatio= parseFloat(possibilityPlayer) / parseFloat(possibilityList[0]);
  359. let colorChoice = -1; //分别为红0、橙1、蓝2,及上方标记的黄绿-1
  360.  
  361. if (actionCard[0] != actionCard[1]) {
  362. if (fatalErrEdge < 0) { //绝对值恶手
  363. if (possibilityPlayer < fatalErr) colorChoice = 0;
  364. else if (possibilityPlayer < normalErr) colorChoice = 1;
  365. else if (possibilityPlayer < arguableErr) colorChoice = 2;
  366. } else if (fatalErrEdge > 0) { //比值恶手
  367. if (possibilityPlayer < fatalErrEdge) colorChoice = 0; //权重过小,直接判断恶手
  368. else if (pRatio < fatalErr) colorChoice = 0;
  369. else if (pRatio < normalErr) colorChoice = 1;
  370. else if (pRatio < arguableErr) colorChoice = 2;
  371. }
  372. }
  373.  
  374. let playerSelectInMain = document.querySelectorAll('.discard-bars-svg > rect[width="20"]');
  375. switch (colorChoice) {
  376. case 0 :
  377. actionTrList[playerSelect + 1].style.background = "red";
  378. playerSelectInMain.forEach(e=>{ e.style.fill = "red"; });
  379. break;
  380. case 1 :
  381. actionTrList[playerSelect + 1].style.background = "#ff5a00";
  382. playerSelectInMain.forEach(e=>{ e.style.fill = "#ff5a00"; });
  383. break;
  384. case 2 :
  385. actionTrList[playerSelect + 1].style.background = "blue";
  386. playerSelectInMain.forEach(e=>{ e.style.fill = "blue"; });
  387. break;
  388. }
  389. }
  390.  
  391. function createButtonBox(){
  392. let settingOption = document.querySelector('.options-div');
  393. let buttonBox = document.createElement('div');
  394. buttonBox.style.display = 'flex';
  395. buttonBox.className = 'buttonBox-div';
  396. buttonBox.style.flexWrap = 'wrap';
  397. buttonBox.style.width = '500px';
  398. buttonBox.style.justifyContent = 'space-evenly';
  399. settingOption.appendChild(buttonBox);
  400. }
  401.  
  402. function backgroundSetting(){
  403. let buttonBox = document.querySelector('.buttonBox-div');
  404. let setBackgroundButton = document.createElement('button');
  405. let backgroundURL = GM_getValue('backgroundPicUrl', 'https://backgroundURL.example');
  406. let backgroundImg = document.createElement('img');
  407. setBackgroundButton.className = 'newSetting';
  408. buttonBox.appendChild(setBackgroundButton); //插入按钮
  409. setBackgroundButton.textContent = '修改背景图';
  410. setBackgroundButton.addEventListener('click', ()=>{
  411. let inputURL = prompt('输入背景图URL', backgroundURL);
  412. if (inputURL !== null) {
  413. backgroundURL = inputURL.trim();
  414. backgroundImg.src = backgroundURL;
  415. GM_setValue('backgroundPicUrl', backgroundURL); //存储背景图链接
  416. }
  417. document.querySelector('.grid-main').style.backgroundImage = `url(${backgroundURL})`;
  418. });
  419. document.querySelector('.grid-main').style.backgroundImage = `url(${backgroundURL})`;
  420. backgroundImg.src = backgroundURL; //设置被存储好的背景
  421. backgroundImg.style.maxWidth = '200px';
  422. backgroundImg.style.maxHeight = '200px';
  423. backgroundImg.style.marginTop = '30px';
  424. backgroundImg.style.justifySelf = 'center';
  425. backgroundImg.onload = ()=>{ document.querySelector('.options-div').appendChild(backgroundImg); }
  426. backgroundImg.onerror = ()=> {
  427. console.log('Can not to load background pic');
  428. document.querySelector('.grid-main').style.background = 'green';
  429. }
  430. }
  431.  
  432. function tileBackSetting(){
  433. let buttonBox = document.querySelector('.buttonBox-div');
  434. let setTileBackButton = document.createElement('button');
  435. let tileBackURL = GM_getValue('tileBackPicURL', 'https://tilebackURL.example');
  436. let tileBackImg = document.querySelectorAll('img[src="media/Regular_shortnames/back.svg"]');
  437. setTileBackButton.className = 'newSetting';
  438. buttonBox.appendChild(setTileBackButton); //插入按钮
  439. setTileBackButton.textContent = '设置牌背';
  440. setTileBackButton.addEventListener('click', ()=>{
  441. let inputURL = prompt('输入牌背URL', tileBackURL);
  442. if (inputURL !== null) {
  443. tileBackURL = inputURL.trim();
  444. GM_setValue('tileBackPicURL', tileBackURL); //存储牌背链接
  445. }
  446. let tilebackStyle = `img[src="media/Regular_shortnames/back.svg"]{
  447. content: url('${tileBackURL}'); }`
  448. GM_addStyle(tilebackStyle);
  449. });
  450. if (tileBackURL == 'https://tilebackURL.example') return;
  451. let tilebackStyle = `img[src="media/Regular_shortnames/back.svg"]{
  452. content: url('${tileBackURL}'); }`
  453. GM_addStyle(tilebackStyle); //使用CSS添加
  454. }
  455.  
  456. function logoSetting(){
  457. let buttonBox = document.querySelector('.buttonBox-div');
  458. let setLogoButton = document.createElement('button');
  459. let logoURL = GM_getValue('logoURL', 'https://logoURL.example');
  460. setLogoButton.className = 'newSetting';
  461. buttonBox.appendChild(setLogoButton); //插入按钮
  462. setLogoButton.textContent = '修改形象图';
  463. setLogoButton.addEventListener('click', ()=>{
  464. let inputURL = prompt('输入形象图URL', logoURL);
  465. if (inputURL !== null) {
  466. logoURL = inputURL.trim();
  467. GM_setValue('logoURL', logoURL); //存储形象图链接
  468. }
  469. document.querySelector('.killer-call-img').src = `url(${logoURL})`;
  470. });
  471. if (logoURL !== 'https://logoURL.example') {
  472. let logoStyle = `
  473. .killer-call-img {
  474. content: url('${logoURL}');
  475. position: relative;
  476. top: 50px;
  477. scale: 1.2;
  478. }`;
  479. GM_addStyle(logoStyle);
  480. }
  481. }
  482.  
  483. function optInfoSwitch(){
  484. let buttonBox = document.querySelector('.buttonBox-div');
  485. let mortalOptionSwitch = document.createElement('button');
  486. let mortalOpt = document.querySelector('.opt-info');
  487. let outer = document.querySelector('.outer');
  488. let state = GM_getValue('mortalOptionState', true);
  489. mortalOptionSwitch.className = 'newSetting';
  490. buttonBox.appendChild(mortalOptionSwitch); //插入按钮
  491. if (!state) { //初始化按钮对应的状态
  492. mortalOptionSwitch.textContent = '开启Mortal选项面板';
  493. mortalOpt.style.display = 'none';
  494. outer.style.marginLeft = '0px';
  495. } else {
  496. mortalOptionSwitch.textContent = '关闭Mortal选项面板';
  497. mortalOpt.style.display = 'initial';
  498. outer.style.marginLeft = '-100px';
  499. }
  500. mortalOptionSwitch.addEventListener('click', ()=>{
  501. state = !state;
  502. if (!state) {
  503. mortalOptionSwitch.textContent = '开启Mortal选项面板';
  504. mortalOpt.style.display = 'none';
  505. outer.style.marginLeft = '0px';
  506. } else {
  507. mortalOptionSwitch.textContent = '关闭Mortal选项面板';
  508. mortalOpt.style.display = 'initial';
  509. outer.style.marginLeft = '-100px';
  510. }
  511. GM_setValue('mortalOptionState', state); //存储状态
  512. });
  513. }
  514.  
  515. function fullScreenEnlarge(){
  516. let scaleArray = GM_getValue('scaleStr', '1.2, 1.35');
  517. let scale = scaleArray.split(',');
  518. let defaultScale = parseFloat(scale[0]);
  519. let fullScreenScale = parseFloat(scale[1]);
  520.  
  521. addEventListener('keydown', (e)=>{ //进入全屏放大
  522. if (e.key === 'F11') {
  523. event.preventDefault();
  524. document.documentElement.requestFullscreen();
  525. }
  526. });
  527. addEventListener('fullscreenchange',()=>{
  528. let mainInFull = document.querySelector('main');
  529. if (!document.fullscreen) { //退出全屏重置
  530. mainInFull.style.scale = `${defaultScale}`;
  531. mainInFull.style.top = '50px';
  532. } else {
  533. mainInFull.style.scale = `${fullScreenScale}`;
  534. mainInFull.style.top = '110px';
  535. }
  536. });
  537. document.querySelector('.killer-call-img').addEventListener('click', ()=>{ //快捷全屏
  538. if (!document.fullscreen){
  539. document.documentElement.requestFullscreen();
  540. } else {
  541. document.exitFullscreen();
  542. }
  543. });
  544. }
  545.  
  546. function createStripsHoverWindow() {
  547. let bars = document.querySelector('#discard-bars'); //设置监听svg监听
  548. let observer = new MutationObserver((mutationList, observer)=>{
  549. let strips = bars.querySelectorAll('.discard-bars-svg>rect');
  550. listenerAdder(strips);
  551. })
  552. if (bars === null) {
  553. console.log('SelectorError!');
  554. } else {
  555. observer.observe(bars, {childList: true, subtree: true}); //当svg被重置时更新选择器strips
  556. }
  557.  
  558. let callBars = document.querySelector('.killer-call-bars');
  559. let observerAdviser = new MutationObserver((mutationList, observerAdviser)=>{
  560. mutationList.forEach(e=>{
  561. if (e.type === 'childList') {
  562. let stripsAdviser = document.querySelectorAll('.killer-call-bars>svg>rect');
  563. listenerAdder(stripsAdviser);
  564. let remainWindow = document.querySelectorAll(".hoverInfo");
  565. remainWindow.forEach(w=>{ w.remove() }); //svg更新时清空浮窗
  566. }
  567. });
  568. });
  569. observerAdviser.observe(callBars, {childList: true});
  570. }
  571.  
  572. function startMortalOptionObserver(errTolerance) {
  573. let optState = GM_getValue('mortalOptionState', true);
  574. // if (!optState) return; //关闭状态不设置监听
  575. let optInfo = document.querySelector('.opt-info');
  576. let observerInfo = new MutationObserver(
  577. (mutationList, observerInfo)=>{
  578. mortalOptionColorize(errTolerance);
  579. }
  580. ); //设置mortal选项更新监听
  581. observerInfo.observe(optInfo, {childList: true});
  582. }
  583.  
  584. function setCustomErrTolerance() {
  585. let buttonBox = document.querySelector('.buttonBox-div');
  586. let setErrToleranceButton = document.createElement('button');
  587. let errToleranceStr = GM_getValue('errToleranceStr', '1, 5, 10, -1');
  588. let errTolerance = errToleranceStr.split(',');
  589.  
  590. setErrToleranceButton.className = 'newSetting';
  591. buttonBox.appendChild(setErrToleranceButton); //插入按钮
  592. setErrToleranceButton.textContent = '自定义恶手率';
  593. setErrToleranceButton.addEventListener('click', ()=>{
  594. let explainText ='输入恶手率组合,四个参数 (刷新后生效)\n' +
  595. 'x4=-1为绝对模式,低于权重直接判定\n' +
  596. 'x4> 0为比值模式,与一选相除再判定'
  597. let inputStr = prompt(explainText, errToleranceStr);
  598. if (inputStr !== null) {
  599. let input = inputStr.replace(',',','); //替换中文逗号
  600. let numArray = input.split(',');
  601. let newErrTolerance = numArray.map(Number);
  602. if (newErrTolerance.length !== 4) {
  603. alert('参数数量不一致!');
  604. return;
  605. }
  606. GM_setValue('errToleranceStr', inputStr); //存储恶手率字符串
  607. errToleranceStr = inputStr;
  608. }
  609. });
  610. return errTolerance;
  611. }
  612.  
  613. function addTableRow(table, str, value) {
  614. const tr = table.insertRow();
  615. let cell = tr.insertCell();
  616. cell.textContent = `${str}`;
  617. cell = tr.insertCell();
  618. cell.textContent = `${value}`;
  619. }
  620.  
  621. function setMainAreaEnlarge() {
  622. let buttonBox = document.querySelector('.buttonBox-div');
  623. let scaleButton = document.createElement('button');
  624. let scaleStr = GM_getValue('scaleStr', '1.2, 1.35');
  625. let scaleArray = scaleStr.split(',');
  626.  
  627. document.querySelector('main').style.scale = `${scaleArray[0]}`;//应用放大
  628.  
  629. scaleButton.className = 'newSetting';
  630. buttonBox.appendChild(scaleButton); //插入按钮
  631. scaleButton.textContent = '界面放大倍数';
  632. scaleButton.addEventListener('click', ()=>{
  633. let explainText ='输入放大倍数组合,四个参数 (刷新后生效)\n' +
  634. '第一个参数,非全屏状态的放大倍数\n' +
  635. '第二个参数,全屏状态下的放大倍数'
  636. let inputStr = prompt(explainText, scaleStr);
  637. if (inputStr !== null) {
  638. let input = inputStr.replace(',',','); //替换中文逗号
  639. let numArray = input.split(',');
  640. let newScaleArray = numArray.map(Number);
  641. if (newScaleArray.length !== 2) {
  642. alert('参数数量不一致!');
  643. return;
  644. }
  645. GM_setValue('scaleStr', inputStr); //存储倍数字符串
  646. scaleStr = inputStr;
  647. }
  648. });
  649. return scaleArray;
  650. }
  651.  
  652. async function errCalculate(errTolerance) {
  653. let fatalErrCnt = 0;
  654. let normalErrCnt = 0;
  655. let arguableErrCnt = 0;
  656. /* 感谢脚本Mortal Killer Plus作者sabertaz的数据获取思路
  657. const urlParams = new URLSearchParams(window.location.search);
  658. const dataURL = urlParams.get("data");
  659. const response = await fetch(dataURL);
  660. const data = await response.json();
  661. const reviewData = data.review; */
  662.  
  663. async function waitReview() {
  664. return new Promise((resolve) => {
  665. const check = setInterval(() => {
  666. if (unsafeWindow.MM.GS.fullData.review) {
  667. clearInterval(check); // 停止轮询
  668. resolve(unsafeWindow.MM.GS.fullData.review); // 返回数据
  669. } }, 500); //间隔500ms
  670. });
  671. }
  672. const reviewData = await waitReview(); //由killerducky作者挂载的Debug信息
  673.  
  674. for (const kyokus of reviewData.kyokus) {
  675. for (const curRound of kyokus.entries) {
  676. const mismatch = !curRound.is_equal;
  677. const pPlayer = curRound.details[curRound.actual_index].prob * 100;
  678. const pMortal = curRound.details[0].prob * 100;
  679. if (mismatch && parseFloat(errTolerance[3]) < 0) { //绝对值恶手
  680. if (pPlayer <= parseFloat(errTolerance[0])) fatalErrCnt++;
  681. if (pPlayer <= parseFloat(errTolerance[1])) normalErrCnt++;
  682. if (pPlayer <= parseFloat(errTolerance[2])) arguableErrCnt++;
  683. } else if (mismatch && parseFloat(errTolerance[3]) > 0) { //比值恶手
  684. const pRate = parseFloat(pPlayer) / parseFloat(pMortal);
  685. if (pPlayer <= parseFloat(errTolerance[3])) {
  686. fatalErrCnt++;
  687. normalErrCnt++;
  688. arguableErrCnt++;
  689. continue;
  690. }
  691. if (pRate <= parseFloat(errTolerance[0])) fatalErrCnt++;
  692. if (pRate <= parseFloat(errTolerance[1])) normalErrCnt++;
  693. if (pRate <= parseFloat(errTolerance[2])) arguableErrCnt++;
  694. }
  695. }
  696. }
  697.  
  698.  
  699. const totalReviewed = reviewData.total_reviewed;
  700.  
  701. const fatalErrRate = ((fatalErrCnt / totalReviewed) * 100).toFixed(2);
  702. const fatalErrStr = `${fatalErrCnt}/${totalReviewed} = ${fatalErrRate}%`;
  703. const normalErrRate = ((normalErrCnt / totalReviewed) * 100).toFixed(2);
  704. const normalErrStr = `${normalErrCnt}/${totalReviewed} = ${normalErrRate}%`;
  705. const arguableErrRate = ((arguableErrCnt / totalReviewed) * 100).toFixed(2);
  706. const arguableErrStr = `${arguableErrCnt}/${totalReviewed} = ${arguableErrRate}%`;
  707.  
  708. let metadataTable = document.querySelector(".about-metadata table:first-child");
  709. let errRateZH = " 恶手率";
  710. if (parseFloat(errTolerance[3]) < 0) errRateZH = "% 恶手率";
  711. addTableRow(metadataTable, `${errTolerance[0]}${errRateZH}`, fatalErrStr);
  712. addTableRow(metadataTable, `${errTolerance[1]}${errRateZH}`, normalErrStr);
  713. addTableRow(metadataTable, `${errTolerance[2]}${errRateZH}`, arguableErrStr);
  714. }
  715.  
  716. function addDoraFlash(doraIndicators, state) { //同时负责上一次dora特效的关闭:state=0
  717. let doras = new Array();
  718. doraIndicators.forEach(e =>{
  719. let doraStr = '';
  720. switch(e[1]) {
  721. case 'z':
  722. if(parseInt(e[0]) < 5) {
  723. doraStr = `${ parseInt(e[0]) % 4 + 1 }z`; //东南西北
  724. } else {
  725. doraStr = `${ (parseInt(e[0]) - 4 ) % 3 + 5}z`; //白发中
  726. } break;
  727. default:
  728. if (parseInt(e[0]) === 0) {
  729. doraStr = `6${e[1]}`;//赤宝牌的特例
  730. } else {
  731. doraStr = `${ parseInt(e[0]) % 9 + 1 }${e[1]}`;
  732. } break;
  733. }
  734. doras.push(doraStr);
  735. });
  736.  
  737. if(state) doras.push('0m', '0p', '0s');
  738. for (const dora of doras) {
  739. let doraStyle;
  740. if (state) {
  741. doraStyle = `
  742. .tileDiv:has(img[src="media/Regular_shortnames/${dora}.svg"]) {
  743. position: relative;
  744. overflow: hidden;
  745. border-radius: 5px;
  746. }
  747.  
  748. .tileDiv:has(img[src="media/Regular_shortnames/${dora}.svg"])::after {
  749. content: '';
  750. position: absolute;
  751. inset: -40%;
  752. background: linear-gradient(45deg, rgba(255,255,255,0) 40%, rgba(255, 255, 255, 0.7), rgba(255,255,255,0) 60%);
  753. animation: doraFlash 2s infinite;
  754. transform: translateY(-100%);
  755. z-index: 1; /*解决失焦问题*/
  756. }
  757.  
  758. @keyframes doraFlash {
  759. to {
  760. transform: translateY(100%);
  761. }
  762. }`;
  763. } else { //关闭伪元素显示和动画
  764. doraStyle = `
  765. .tileDiv:has(img[src="media/Regular_shortnames/${dora}.svg"]){
  766. overflow: visible;
  767. }
  768. .tileDiv:has(img[src="media/Regular_shortnames/${dora}.svg"])::after {
  769. content: none;
  770. background: transparent;
  771. animation: none;
  772. }`;
  773. }
  774. GM_addStyle(doraStyle);
  775. }
  776.  
  777. if (typeof addDoraFlash.executed === "undefined" || !addDoraFlash.executed) {
  778. addDoraFlash.executed = false;
  779. let rotatedDoraFix = `
  780. .pov-p0 > div:has(.rotate) {
  781. height: var(--tile-width);
  782. align-self: flex-end;
  783. }
  784. .pov-p0 > div > .rotate {
  785. transform: rotate(90deg) translate(calc(-1 * var(--tile-height)), 0px);
  786. }
  787. /*自家鸣牌立直调整*/
  788.  
  789. .pov-p1 > div:has(.rotate) {
  790. width: var(--tile-width);
  791. align-self: flex-end;
  792. }/*下家鸣牌立直调整*/
  793.  
  794. .pov-p2 > div:has(.rotate) {
  795. height: var(--tile-width);
  796. }
  797. .grid-discard-p2 > div:has(.rotate) {
  798. align-self: flex-end;
  799. }/*对家鸣牌立直调整*/
  800.  
  801. .pov-p3 > div:has(.rotate) {
  802. width: var(--tile-width);
  803. }
  804. .grid-discard-p3 > div:has(.rotate) {
  805. align-self: flex-end;
  806. }/*上家鸣牌立直调整*/
  807. .tileDiv:has(.tileImg.rotate.float) {
  808. overflow:visible;
  809. }/*修复自杠Dora4显示*/
  810. `;
  811. GM_addStyle(rotatedDoraFix);
  812. }
  813. }
  814.  
  815. function startDoraObserver(doraCheck_ms = 1500) { //事实上网页上挂载了window.MM.GS.gs.dora,但貌似无法监听
  816. let preDoraIndicator = new Array();
  817. const checkInterval = doraCheck_ms; //每隔x毫秒查询dora指示牌
  818. const interval = setInterval(() => {
  819. let doraInfo = document.querySelectorAll('.info-doras > div > img');
  820. let doraIndicator = new Array();
  821. doraInfo.forEach(e=>{
  822. let cardURL = e.getAttribute('src');//获取doraIdr
  823. let doraStr = cardURL.substring(cardURL.lastIndexOf('/')+1, cardURL.lastIndexOf('.'));
  824. if (doraStr !== 'back') doraIndicator.push(doraStr);
  825. });
  826.  
  827. if (!(function(doraIndicator, preDoraIndicator) { //数组相等的匿名函数
  828. if (doraIndicator.length !== preDoraIndicator.length) return false;
  829. for (let i = 0; i < doraIndicator.length; i++) {
  830. if (doraIndicator[i] !== preDoraIndicator[i]) { return false; }};
  831. return true;
  832. })(doraIndicator, preDoraIndicator)) {
  833. addDoraFlash(preDoraIndicator, false);//清除上一次的dora特效;
  834. addDoraFlash(doraIndicator, true);
  835. //console.log(preDoraIndicator, '----Updated To--->', doraIndicator);
  836. preDoraIndicator = []; //保存当前dora
  837. doraIndicator.forEach(d=>{ preDoraIndicator.push(d); });
  838. }
  839. }, checkInterval);
  840. }
  841.  
  842. function startEfficencyCalc(calcDelay_ms = 800) {
  843. let effEnable = GM_getValue('effEnable', true);
  844. if (!effEnable) return;
  845.  
  846. const calcDelay = (mutationsList) => {
  847. let effHover = document.querySelectorAll('.eff-hover');
  848. effHover.forEach((e)=>{ e.remove(); }); //清除未正确移除的浮窗
  849. if (mutationsList.length <= 1) return; //非摸牌更新
  850. if (timer) clearTimeout(timer); //等待时间过短
  851. timer = setTimeout(()=>{
  852. timer = null;
  853. calcEfficency(); //延迟后计算牌效,跳过快速浏览
  854. }, calcDelay_ms);
  855. };
  856.  
  857. const svgBarsDetector = new MutationObserver((mutations, observer) => {
  858. const target = document.querySelector('.discard-bars-svg');
  859. if (target) {
  860. const startCalc = new MutationObserver(calcDelay);
  861. startCalc.observe(target, { childList: true, subtree: false });
  862. svgBarsDetector.disconnect();
  863. }
  864. });
  865. svgBarsDetector.observe(document.body, { childList: true, subtree: true }); //等待bars-svg加载
  866. }
  867.  
  868. function calcEfficency() { //负责函数调用
  869. let cardInfo = getCardInfo();
  870. if(!cardInfo) return; //不摸牌不计算
  871. let shantenCnt = shanten(cardInfo.handset);
  872. if(shantenCnt === -1) return; //和牌返回
  873.  
  874. let ukeireSet = kiruEfficency(cardInfo.handset, cardInfo.seenTiles);
  875. addEffCardset(ukeireSet, shantenCnt);
  876. }
  877.  
  878. function getCardInfo() {
  879. let handcard = unsafeWindow.MM.GS.gs.hands[unsafeWindow.MM.GS.heroPidx];
  880. let tsumocard = unsafeWindow.MM.GS.gs.drawnTile[unsafeWindow.MM.GS.heroPidx];
  881. if (!tsumocard) return; //没有自摸牌
  882. handcard.push(tsumocard);
  883. let handset = new Array(5).fill().map(() => new Array(10).fill(0)); //矩阵化手牌
  884. handcard.forEach(e=>{
  885. let idx = Math.floor(e / 10), idy = e % 10; //添菜idy
  886. if (idx === 5) {
  887. idx = idy;
  888. idy = 5;
  889. } //处理红五
  890. handset[idx][idy]++;
  891. });
  892.  
  893. let seenTiles = new Array(5).fill().map(() => new Array(10).fill(0)); //已现牌组,不包括手牌
  894. let calls = unsafeWindow.MM.GS.gs.calls;
  895. let discardPond = unsafeWindow.MM.GS.gs.discardPond;
  896. let doraIdr = unsafeWindow.MM.GS.gs.doraIndicator;
  897.  
  898. let doraInfo = document.querySelectorAll('.info-doras > div > img');
  899. let doraCnt = 0;
  900. doraInfo.forEach(e=>{
  901. let cardURL = e.getAttribute('src');
  902. let doraStr = cardURL.substring(cardURL.lastIndexOf('/')+1, cardURL.lastIndexOf('.'));
  903. if (doraStr !== 'back') doraCnt++;
  904. }); //获取dora数量,修正计入未开指示牌问题
  905.  
  906. for (let card of calls) {
  907. if (typeof card !== 'number') continue;
  908. let idx = Math.floor(card / 10), idy = card % 10;
  909. if (idx === 5) {
  910. idx = idy;
  911. idy = 5;
  912. }
  913. seenTiles[idx][idy]++;
  914. }
  915. for (let ply of discardPond) {
  916. ply.forEach(e=>{
  917. let idx = Math.floor(e.tile / 10), idy = e.tile % 10;
  918. if (idx === 5) {
  919. idx = idy;
  920. idy = 5;
  921. }
  922. seenTiles[idx][idy]++;
  923. })
  924. }
  925. for (let i = 0; i < doraCnt; i++) {
  926. let idr = doraIdr[i];
  927. let idx = Math.floor(idr / 10), idy = idr % 10;
  928. if (idx === 5) {
  929. idx = idy;
  930. idy = 5;
  931. }
  932. seenTiles[idx][idy]++;
  933. }
  934.  
  935. for (let i = 1; i <= 4; i++) handset[i][0] = seenTiles[i][0] = i;
  936. return { handset: handset, seenTiles: seenTiles };
  937. }
  938.  
  939. function breakdown(A, depth) {
  940. if (depth >= 4) return 0; //四张孤张剪枝
  941. let ret = 0, i = 1;
  942. while (i <= 9 && !A[i]) i++; //定位第一张
  943. if (i > 9) return 0; //空白返回
  944. if (i + 2 <= 9 && A[i] && A[i + 1] && A[i + 2] && A[0] != 4) {
  945. A[i]--; A[i + 1]--; A[i + 2]--;
  946. ret = Math.max(ret, breakdown(A, depth) + 2100);
  947. A[i]++; A[i + 1]++; A[i + 2]++;
  948. }
  949. else {
  950. if (i + 2 <= 9 && A[i] && A[i + 2] && A[0] != 4) {
  951. A[i]--; A[i + 2]--;
  952. ret = Math.max(ret, breakdown(A, depth) + 1001);
  953. A[i]++; A[i + 2]++;
  954. }
  955. if (i + 1 <= 9 && A[i] && A[i + 1] && A[0] != 4) {
  956. A[i]--; A[i + 1]--;
  957. ret = Math.max(ret, breakdown(A, depth) + 1001);
  958. A[i]++; A[i + 1]++;
  959. }
  960. }
  961.  
  962. if (A[i] >= 3) {
  963. A[i] -= 3;
  964. ret = Math.max(ret, breakdown(A, depth) + 2100);
  965. A[i] += 3;
  966. }
  967. if (A[i] >= 2) {
  968. A[i] -= 2;
  969. ret = Math.max(ret, breakdown(A, depth) + 1010);
  970. A[i] += 2;
  971. }
  972. A[i]--;
  973. ret = Math.max(ret, breakdown(A, depth + 1));
  974. A[i]++;
  975. return ret;
  976. }
  977.  
  978. function shantenStandard(S) {
  979. let analysis = 0, cardnum = 0;
  980. for (let A of S) {
  981. let ret = 0;
  982. for (let i = 1; i <= 9; i++) {
  983. ret += A[i];
  984. cardnum += A[i];
  985. }
  986. if (!ret) continue; //该类牌空
  987. analysis += breakdown(A, 0);
  988. }
  989.  
  990. let block = Math.floor(analysis % 1000 / 100);
  991. let pair = Math.floor(analysis % 100 / 10);
  992. let dazi = analysis % 10;
  993.  
  994. block += Math.floor((14 - cardnum) / 3); //处理鸣牌
  995. if (pair > 1) {
  996. dazi += pair - 1;
  997. pair = 1;
  998. } // 对搭转换
  999. while (block + dazi > 4 && dazi > 0) dazi--; // 4N+2规则
  1000.  
  1001. return 8 - (2 * block + dazi + pair);
  1002. }
  1003.  
  1004. function shantenChiitoi(S) {
  1005. let pair = 0;
  1006. for (let A of S) {
  1007. for (let i = 1; i <= 9; i++) {
  1008. if (A[i] >= 2) pair++;
  1009. }
  1010. }
  1011. return 6 - pair;
  1012. }
  1013.  
  1014. function shanten(S) { return Math.min(shantenStandard(S), shantenChiitoi(S)); }
  1015.  
  1016. function ukeire(S, curShanten) {
  1017. let vaildcard = new Array(5).fill().map(() => new Array(10).fill(0)); //进张
  1018. for (let i = 0; i <= 4; i++) vaildcard[i][0] = i; //初始化
  1019.  
  1020. // let curShanten = shanten(S); /*外部传入向听数节省调用时间 */
  1021. for (let i = 1; i <= 3; i++) { //数牌
  1022. for (let j = 1; j <= 9; j++) {
  1023. let k = j - 2, acc = 0;
  1024. while (k < 1) k++;
  1025. while (k <= j + 2) {
  1026. if (k > 9) break;
  1027. acc += S[i][k++];
  1028. }
  1029. if (!acc) continue; //不摸孤张
  1030. S[i][j]++;
  1031. if (shanten(S) < curShanten) vaildcard[i][j]++;
  1032. S[i][j]--;
  1033. }
  1034. }
  1035. for (let j = 1; j <= 7; j++) { //字牌
  1036. if (!S[4][j]) continue;
  1037. S[4][j]++;
  1038. if (shanten(S) < curShanten) vaildcard[4][j]++;
  1039. S[4][j]--;
  1040. }
  1041. return vaildcard;
  1042. }
  1043.  
  1044. function kiruEfficency(S, seen) {
  1045. let ret = [];
  1046. let curShanten = shanten(S);
  1047. for (let i = 1; i <= 4; i++) {
  1048. for (let j = 1; j <= 9; j++) {
  1049. if (!S[i][j]) continue;
  1050. let pai, num = j.toString();
  1051. switch (i) {
  1052. case 1: pai = num + "m"; break;
  1053. case 2: pai = num + "p"; break;
  1054. case 3: pai = num + "s"; break;
  1055. case 4: pai = num + "z"; break;
  1056. }
  1057.  
  1058. S[i][j]--;
  1059. if (shanten(S) == curShanten) { //出牌向听不减
  1060. let vaild = ukeire(S, curShanten);
  1061. let left = tileleft(S, vaild, seen);
  1062. let vaildstr = convertToStr(vaild);
  1063. ret.push({ pai: pai, left: left, ukeStr: vaildstr, uke: vaild });
  1064. }
  1065. S[i][j]++;
  1066. }
  1067. }
  1068. ret.sort((a,b)=> b.left.leftNor - a.left.leftNor);
  1069. return ret;
  1070. }
  1071.  
  1072. function tileleft(S, uke, seen) {
  1073. let leftNor = 0, leftPure = 0;
  1074. for (let i = 1; i <= 4; i++) {
  1075. for (let j = 1; j <= 9; j++) {
  1076. if (!uke[i][j]) continue;
  1077. leftNor += 4 - S[i][j] - seen[i][j];
  1078. leftPure += 4 - S[i][j];
  1079. }
  1080. }
  1081. return { leftNor, leftPure };
  1082. }
  1083.  
  1084. function convertToStr(S) {
  1085. let str = '';
  1086. for (let i = 1; i <= 4; i++) {
  1087. let acc = 0;
  1088. for (let j = 1; j <= 9; j++) {
  1089. let tmp = S[i][j];
  1090. acc += tmp;
  1091. while (tmp--) str += j.toString();
  1092. }
  1093. if (!acc) continue;
  1094. switch (i) {
  1095. case 1: str += 'm'; break;
  1096. case 2: str += 'p'; break;
  1097. case 3: str += 's'; break;
  1098. case 4: str += 'z'; break;
  1099. }
  1100. }
  1101. return str;
  1102. }
  1103.  
  1104. function addEffCardset(ukeireSet, shantenCnt) {
  1105. let effWindow = document.querySelector('.efficency-call-div');
  1106. if (!effWindow) return; //没有创建窗口则取消
  1107. effWindow.innerHTML = ''; //清空内容
  1108.  
  1109. let shantenText = `${shantenCnt} 向听`;
  1110. if(!shantenCnt) shantenText = '听牌';
  1111. let showShanten = document.createElement('text');
  1112. showShanten.textContent = shantenText;
  1113. showShanten.style.textAlign = 'center';
  1114. showShanten.style.width = '100%';
  1115. showShanten.marginTop = '1%';
  1116. effWindow.appendChild(showShanten);
  1117.  
  1118. for (let ukeInfo of ukeireSet) {
  1119. let pai = ukeInfo.pai;
  1120. let tile = document.createElement('img');
  1121. let leftText = ukeInfo.left.leftNor.toString().padStart(2, '0') + ':'
  1122. + ukeInfo.left.leftPure.toString().padStart(2, '0');
  1123. let showLeftText = document.createElement('text');
  1124. let wrapDiv = document.createElement('div');
  1125.  
  1126. tile.src = `media/Regular_shortnames/${pai}.svg`;
  1127. tile.className = 'tileImg effTile';
  1128. showLeftText.style.fontSize = 'xx-small';
  1129. showLeftText.style.lineHeight = '2';
  1130. showLeftText.style.marginLeft = '2px';
  1131. showLeftText.textContent = leftText;
  1132. wrapDiv.style.display = 'flex';
  1133. wrapDiv.style.marginLeft = '3%';
  1134. wrapDiv.style.marginTop = '1%'; /*样式设定*/
  1135.  
  1136. tile.addEventListener('mouseover', ()=> { //添加浮窗
  1137. let effHover = document.createElement('div');
  1138.  
  1139. let hoverPai, cnt = 0;
  1140. for (let i = 1; i <= 4; i++) {
  1141. for (let j = 1; j <= 9; j++) {
  1142. if (!ukeInfo.uke[i][j]) continue;
  1143. switch(i) {
  1144. case 1: hoverPai = j.toString() + 'm'; break;
  1145. case 2: hoverPai = j.toString() + 'p'; break;
  1146. case 3: hoverPai = j.toString() + 's'; break;
  1147. case 4: hoverPai = j.toString() + 'z'; break;
  1148. }
  1149. cnt++;
  1150. let hoverTile = document.createElement('img');
  1151. hoverTile.src = `media/Regular_shortnames/${hoverPai}.svg`;
  1152. hoverTile.className = 'tileImg hoverTile';
  1153. effHover.appendChild(hoverTile); //向浮窗添加进张
  1154. }
  1155. }
  1156.  
  1157. let posParent = effWindow.getBoundingClientRect();
  1158. let maxWidthcCnt = Math.min(13, cnt);
  1159. let posX = ( posParent.left + posParent.right - (standardTileWidth + 4) * maxWidthcCnt ) / 2;
  1160. let posY = posParent.top - Math.ceil(cnt / 13) * (standardTileHeight + 4) - 10;
  1161. effHover.style.width = `${maxWidthcCnt * (standardTileWidth + 4)}px`; //.tileImg样式中还有2px的padding
  1162. effHover.style.left = `${posX}px`
  1163. effHover.style.top = `${posY}px`
  1164. effHover.className = 'eff-hover';
  1165. document.body.appendChild(effHover);
  1166.  
  1167. const deleteEffHover = ()=>{ //清除监听器和窗口
  1168. effHover.remove();
  1169. tile.removeEventListener('mouseout', deleteEffHover);
  1170. };
  1171. tile.addEventListener('mouseout', deleteEffHover);
  1172. });
  1173.  
  1174. wrapDiv.appendChild(tile);
  1175. wrapDiv.appendChild(showLeftText);
  1176. effWindow.appendChild(wrapDiv);
  1177. }
  1178. }
  1179.  
  1180. function addEffWindow() { //添加牌效窗口
  1181. let buttonBox = document.querySelector('.buttonBox-div');
  1182. let efficencySwitch = document.createElement('button');
  1183. let effEnable = GM_getValue('effEnable', true);
  1184.  
  1185. efficencySwitch.className = 'newSetting';
  1186. buttonBox.appendChild(efficencySwitch); //插入按钮
  1187.  
  1188. if (!effEnable) { //初始化按钮对应的状态
  1189. efficencySwitch.textContent = '开启牌效计算';
  1190. } else {
  1191. efficencySwitch.textContent = '关闭牌效计算';
  1192. }
  1193.  
  1194. efficencySwitch.addEventListener('click', ()=>{
  1195. effEnable = !effEnable;
  1196. if (!effEnable) {
  1197. efficencySwitch.textContent = '开启牌效计算';
  1198. } else {
  1199. efficencySwitch.textContent = '关闭牌效计算';
  1200. }
  1201. GM_setValue('effEnable', effEnable); //存储状态
  1202. });
  1203. if (!effEnable) return; //不开启牌效计算,则仅添加按钮
  1204.  
  1205. let effDiv = document.createElement('div');
  1206. let killerCallDiv = document.querySelector('.killer-call-div');
  1207. let effCss = `
  1208. .efficency-call-div {
  1209. scale: 1.4;
  1210. width: calc(var(--zoom)*245px);
  1211. height: calc(var(--zoom)*110px);
  1212. background: hsl(190deg 31.45% 58.49%);
  1213. box-shadow: 5px 5px 6px 1px #f6f6f6;
  1214. border-radius: 20px;
  1215. margin-top: 34%;
  1216. margin-left: 14%;
  1217. display: flex;
  1218. flex-wrap: wrap;
  1219. align-content: flex-start;
  1220. }
  1221.  
  1222. .eff-hover {
  1223. position: absolute;
  1224. display: flex;
  1225. flex-wrap: wrap;
  1226. scale: 1.5;
  1227. background: #00c0ff80;
  1228. box-shadow: 0px 0px 5px 5px #0090ff;
  1229. border-radius: 5px;
  1230. }
  1231.  
  1232. .effTile {
  1233. filter: none;
  1234. width: ${standardTileWidth}px;
  1235. height: ${standardTileHeight}px;
  1236. box-shadow: inset 0 0 2px #880000;
  1237. margin-left: 3%;
  1238. }
  1239.  
  1240. .hoverTile {
  1241. filter: none;
  1242. width: ${standardTileWidth}px;
  1243. height: ${standardTileHeight}px;
  1244. box-shadow: inset 0 0 2px #880000;
  1245. }`;
  1246. /* 分别是牌效窗口、进张悬浮窗、牌效张、浮窗张
  1247. 在css字符串内加注释,会有bug,很神奇吧js */
  1248. GM_addStyle(effCss);
  1249.  
  1250. document.querySelector('.killer-call-img').style.display = 'none'; //关闭logo
  1251. effDiv.addEventListener('click', ()=>{ //代替logo的快捷全屏
  1252. if (!document.fullscreen) document.documentElement.requestFullscreen();
  1253. else document.exitFullscreen();
  1254. });
  1255. effDiv.className = 'efficency-call-div';
  1256. killerCallDiv.appendChild(effDiv);
  1257. }
  1258.  
  1259. //-------------------------------------------- Extra Functions should end here --------------------------------------------//
  1260.  
  1261.  
  1262. (function() {
  1263. //-------------------------------------------- Main Code should start here --------------------------------------------//
  1264. 'use strict';
  1265.  
  1266. //↓↓↓一系列按钮及其功能
  1267. createButtonBox();
  1268. backgroundSetting();
  1269. tileBackSetting();
  1270. logoSetting();
  1271. optInfoSwitch();
  1272. setMainAreaEnlarge();
  1273. addEffWindow();
  1274. let errTolerance = setCustomErrTolerance();
  1275. //↑↑↑一系列按钮及其功能
  1276.  
  1277. mortalAddStyle(); //应用CSS样式
  1278. fullScreenEnlarge(); //全屏时缩放调整
  1279. createStripsHoverWindow(); //创建绿条悬浮窗
  1280. startMortalOptionObserver(errTolerance); //mortal选项染色
  1281. errCalculate(errTolerance); //计算恶手率
  1282. startDoraObserver(); //添加宝牌特效
  1283. startEfficencyCalc(); //启动牌效计算
  1284.  
  1285. //-------------------------------------------- Main Code should end here --------------------------------------------//
  1286. })();

QingJ © 2025

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