Mortal Killer Plus

Mortal KillerDucky GUI+

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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