Mortal Killer Plus

Mortal KillerDucky GUI+

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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           2.0.0
// @author            Sabertaz
// @icon              https://mjai.ekyu.moe/favicon-32x32.png
// @match             https://mjai.ekyu.moe/*
// @grant             GM_getValue
// @run-at            document-start
// @license           MIT
// ==/UserScript==

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

  // 默认勾选 Rating
  waitForElement('[name="show-rating"]').then(() => {
    document.querySelectorAll('[name="show-rating"]').forEach((el) => {
      el.checked = true
    })
  }).catch(console.error)

  waitForElement('.about-metadata table').then(async () => {
    i18n.init()
    startMortalOptionObserver()
    return addErrorMetadata()
  }).then(() => {
    document.querySelector('#about-modal')?.showModal()
  }).catch(console.error)
})()