// ==UserScript==
// @name Mortal Killer Plus
// @name:zh-CN Mortal Killer Plus
// @description Mortal KillerDucky GUI+
// @description:zh-CN Mortal KillerDucky GUI+
// @namespace mortal-killer-plus
// @version 1.3.4
// @author Sabertaz
// @icon https://mjai.ekyu.moe/favicon-32x32.png
// @match https://mjai.ekyu.moe/killerducky/*
// @grant GM_getValue
// @run-at document-start
// @license MIT
// ==/UserScript==
/* eslint-disable security/detect-object-injection */
/* eslint-disable ts/no-unsafe-argument */
/* eslint-disable ts/no-unsafe-assignment */
/* eslint-disable ts/no-unsafe-call */
/* eslint-disable ts/no-unsafe-member-access */
/* eslint-disable ts/no-unsafe-return */
/* eslint-disable ts/strict-boolean-expressions */
(function () {
'use strict'
const FatalErrorLimit = '1'
const NormalErrorLimit = '5'
const ArguableErrorLimit = '10'
const PlayerChoiceColor = '#abc431'
const FatalErrorColor = '#ff0000'
const NormalErrorColor = '#ff5a00'
const ArguableErrorColor = '#845ef7'
const Locales = {
'en': {
FatalErrorLimit: `${FatalErrorLimit}% Moves/total`,
NormalErrorLimit: `${NormalErrorLimit}% Moves/total`,
ArguableErrorLimit: `${ArguableErrorLimit}% Moves/total`,
},
'zh-CN': {
FatalErrorLimit: `${FatalErrorLimit}% 恶手率`,
NormalErrorLimit: `${NormalErrorLimit}% 恶手率`,
ArguableErrorLimit: `${ArguableErrorLimit}% 恶手率`,
},
}
const i18n = {
lang: 'en',
init() {
const lang = localStorage.getItem('lang') ?? 'en'
this.setLang(lang)
},
setLang(lang = 'en') {
this.lang = lang
},
translate(key) {
return Locales[this.lang][key]
},
t(key) {
return this.translate(key)
},
}
async function waitForElement(targetSelector, rootSelector = 'body', wait = undefined) {
const rootElement = document.querySelector(rootSelector)
if (!rootElement) {
return Promise.reject(new Error('root element is not exist'))
}
// check if the element is already rendered
const targetElement = rootElement.querySelector(targetSelector)
if (targetElement) {
return Promise.resolve(targetElement)
}
return new Promise((resolve) => {
const callback = function (mutationList, observer) {
const targetElement = rootElement.querySelector(targetSelector)
if (targetElement) {
// found
resolve(targetElement)
// then cancel to watch the element
observer.disconnect()
}
}
const observer = new MutationObserver(callback)
observer.observe(rootElement, {
subtree: true,
childList: true,
})
if (wait !== undefined) {
// if wait is set, then cancel to watch the element to render after wait times
setTimeout(() => {
observer.disconnect()
}, wait)
}
})
}
function addTableRow(table, key, value, color) {
const tr = table.insertRow()
const keyCell = tr.insertCell()
keyCell.textContent = `${key}`
const valueCell = tr.insertCell()
valueCell.textContent = `${value}`
if (color) {
keyCell.style.color = color
valueCell.style.color = color
}
}
async function addErrorMetadata() {
let fatalErrorNum = 0
let normalErrorNum = 0
let arguableErrorNum = 0
const urlParams = new URLSearchParams(window.location.search)
const dataParam = urlParams.get('data')
if (!dataParam) {
return
}
const response = await fetch(dataParam)
const data = await response.json()
const reviewData = data.review
for (const kyokus of reviewData.kyokus) {
for (const currentPlay of kyokus.entries) {
const mismatch = !currentPlay.is_equal
const currentPlayPoint = currentPlay.details[currentPlay.actual_index].prob * 100
if (mismatch && currentPlayPoint <= Number.parseFloat(FatalErrorLimit)) {
fatalErrorNum++
}
if (mismatch && currentPlayPoint <= Number.parseFloat(NormalErrorLimit)) {
normalErrorNum++
}
if (mismatch && currentPlayPoint <= Number.parseFloat(ArguableErrorLimit)) {
arguableErrorNum++
}
}
}
const totalReviewed = reviewData.total_reviewed
const fatalErrorRate = ((fatalErrorNum / totalReviewed) * 100).toFixed(2)
const fatalErrorStr = `${fatalErrorNum}/${totalReviewed} = ${fatalErrorRate}%`
const normalErrorRate = ((normalErrorNum / totalReviewed) * 100).toFixed(2)
const normalErrorStr = `${normalErrorNum}/${totalReviewed} = ${normalErrorRate}%`
const arguableErrorRate = ((arguableErrorNum / totalReviewed) * 100).toFixed(2)
const arguableErrorStr = `${arguableErrorNum}/${totalReviewed} = ${arguableErrorRate}%`
const metadataTable = document.querySelector('.about-metadata table')
addTableRow(metadataTable, i18n.t('FatalErrorLimit'), fatalErrorStr, FatalErrorColor)
addTableRow(metadataTable, i18n.t('NormalErrorLimit'), normalErrorStr, NormalErrorColor)
addTableRow(metadataTable, i18n.t('ArguableErrorLimit'), arguableErrorStr, ArguableErrorColor)
}
/**
* @author CiterR (Bilibili at 遥忆酒家七)
* @link https://www.bilibili.com/video/BV1SWv6eGEnq
*/
function markupPlayerChoice() {
const actionTrList = document.querySelector('.opt-info > table:last-child')?.querySelectorAll('tr')
const actionCardList = [] // 第一个是无用项
const possibilityList = []
actionTrList?.forEach((e) => {
const cardAct = e.querySelector('td:first-child > span')
let action, card
if (cardAct != null) {
action = cardAct.textContent.substring(0, 1) // 获取牌操作
}
const cardImg = e.querySelector('td:first-child > span > img')
if (cardImg != null) {
const cardURL = cardImg.getAttribute('src')
card = cardURL.substring(cardURL.lastIndexOf('/') + 1, cardURL.lastIndexOf('.')) // 获取出牌选择
}
actionCardList.push(action + card)
const possibilityTr = e.querySelector('td:last-child')
if (possibilityTr.textContent !== 'P') {
possibilityList.push(possibilityTr.textContent) // 获取概率数据
}
})
// 获取玩家选择和 Mortal 一选
const actionCard = []
const mainActionSpan = document.querySelectorAll('.opt-info > table:first-child span')
mainActionSpan.forEach((e) => {
const action = e.textContent?.substring(0, 1) // 操作
let card
const cardImg = e.querySelector('img')
if (cardImg != null) {
const 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 = PlayerChoiceColor
possibilityPlayer = Number.parseFloat(possibilityList[i - 1])
playerSelect = i - 1
break
}
}
// 判断恶手并标红, 橙, 紫, 绿.
if (actionCard[0] !== actionCard[1]) {
if (possibilityPlayer <= Number.parseFloat(FatalErrorLimit)) {
actionTrList[playerSelect + 1].style.background = FatalErrorColor
} else if (possibilityPlayer <= Number.parseFloat(NormalErrorLimit)) {
actionTrList[playerSelect + 1].style.background = NormalErrorColor
} else if (possibilityPlayer <= Number.parseFloat(ArguableErrorLimit)) {
actionTrList[playerSelect + 1].style.background = ArguableErrorColor
}
}
}
/**
* @author CiterR (Bilibili at 遥忆酒家七)
* @link https://www.bilibili.com/video/BV1SWv6eGEnq
*/
function startMortalOptionObserver() {
// 关闭状态时不设置监听
const optState = GM_getValue('mortalOptionState', true)
if (!optState) {
return
}
// 设置 Mortal 选项更新监听
const observer = new MutationObserver(
() => {
markupPlayerChoice()
},
)
const optionTable = document.querySelector('.opt-info')
if (optionTable) {
observer.observe(optionTable, { childList: true })
}
}
waitForElement('.about-metadata table').then(async () => {
i18n.init()
startMortalOptionObserver()
return addErrorMetadata()
}).then(() => {
document.querySelector('#about-modal')?.showModal()
}).catch(console.error)
})()