MaraQuestHelpers

Questing helpers

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greasyfork.org/scripts/573215/1794669/MaraQuestHelpers.js

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
;(function (global) {
  'use strict'

  /** Returns the first matching element for a selector within the given root. */
  function query(selector, root = document) {
    return root?.querySelector(selector) ?? null
  }

  /** Returns all matching elements for a selector as a plain array. */
  function queryAll(selector, root = document) {
    return [...(root?.querySelectorAll(selector) ?? [])]
  }

  /** Returns the first element found from a list of selectors. */
  function queryFirst(selectors, root = document) {
    for (const selector of selectors) {
      const element = query(selector, root)
      if (element) {
        return element
      }
    }

    return null
  }

  /** Clicks the first element matching a selector and returns it. */
  function clickElement(selector, root = document) {
    const element = query(selector, root)
    element?.click()
    return element
  }

  /** Extracts the item id from a price-check link. */
  function getItemIdFromPriceCheck(priceCheck = query('.dopricecheck')) {
    return priceCheck?.getAttribute('data-id') ?? ''
  }

  /** Clicks a price-check link immediately or after an optional delay. */
  function clickPriceCheck({
    selector = '.dopricecheck',
    itemId = '',
    delay = 0,
    root = document,
  } = {}) {
    const priceCheck = itemId
      ? query(`a[data-id='${itemId}']`, root)
      : query(selector, root)

    if (!priceCheck) {
      return null
    }

    setTimeout(() => {
      priceCheck.click()
    }, delay)

    return priceCheck
  }

  /** Reads the price-check stock row and returns the element, text, quantity, and URL. */
  function getStockInfo(
    selectors = ['.pricechecktable .sitedate', '.pricechecktable .sitedate.same.italic']
  ) {
    const stockElement = Array.isArray(selectors)
      ? queryFirst(selectors)
      : query(selectors)
    const stockText = stockElement?.innerText ?? ''
    const stockMatch = stockText.match(/(\d+)\s+in\s+stock/)

    return {
      element: stockElement,
      text: stockText,
      quantity: stockMatch ? parseInt(stockMatch[1]) : null,
      url: stockElement?.parentElement?.href ?? null,
    }
  }

  /** Returns the shop URL when the price check reports stock is available. */
  function getInStockShopUrl(
    selectors = ['.pricechecktable .sitedate', '.pricechecktable .sitedate.same.italic']
  ) {
    const stockInfo = getStockInfo(selectors)
    return stockInfo.quantity > 0 ? stockInfo.url : null
  }

  /** Returns the linked user shop URL from the price-check panel. */
  function getUserShopUrl(selector = '.pricechecktable .alsotry.same.strong') {
    const userShopLink = query(selector)
    return userShopLink?.parentElement?.href ?? null
  }

  /** Returns true when the price-check panel marks the item as retired. */
  function isPriceCheckRetired({
    selectors = [
      '.pricechecktable .banned.same.italic',
      '.pricechecktable .offline.same.italic',
    ],
  } = {}) {
    return selectors.some((selector) => Boolean(query(selector)))
  }

  /** Returns the number of copies shown in attic results. */
  function getAtticItemCount(
    selectors = ['.pricecheckcontent .offline.same', '.offline.same']
  ) {
    const atticElement = Array.isArray(selectors)
      ? queryFirst(selectors)
      : query(selectors)
    const countText = atticElement?.textContent?.split(' ')[0] ?? '0'
    const quantity = parseInt(countText, 10)

    return Number.isNaN(quantity) ? 0 : quantity
  }

  /** Returns the attic URL when at least one copy of the item is available there. */
  function getAtticUrl(
    selectors = ['.pricecheckcontent .offline.same', '.offline.same']
  ) {
    const atticElement = Array.isArray(selectors)
      ? queryFirst(selectors)
      : query(selectors)
    return getAtticItemCount(selectors) > 0
      ? atticElement?.parentElement?.href ?? null
      : null
  }

  /** Compares displayed prices and returns true when the user shop is cheaper. */
  function isUserShopCheaper({
    userSelector = '.alsotry.same.strong',
    shopSelector = 'span.sitedate.same.italic',
  } = {}) {
    const userText = query(userSelector)?.innerText ?? ''
    const shopText = query(shopSelector)?.innerText ?? ''

    const userPrice = parseInt(
      userText.split(' ')[2]?.split('MP')[0]?.replace(/,/g, '') ?? '',
      10
    )
    const shopPrice = parseInt(
      shopText.split(' ').pop()?.split('MP')[0]?.replace(/,/g, '') ?? '',
      10
    )

    if (Number.isNaN(userPrice) || Number.isNaN(shopPrice)) {
      return false
    }

    return shopPrice > userPrice
  }

  /** Chooses the best destination URL from attic, main shop, or user shop results. */
  function choosePriceCheckUrl() {
    const atticUrl = getAtticUrl()
    if (atticUrl) {
      return atticUrl
    }

    const inStockUrl = getInStockShopUrl()
    const userShopUrl = getUserShopUrl()

    if (isPriceCheckRetired() || !inStockUrl) {
      return userShopUrl
    }

    return isUserShopCheaper() ? userShopUrl : inStockUrl
  }

  /** Navigates to the chosen price-check result after an optional delay. */
  function goToPriceCheckResult(delay = 0, getUrl = choosePriceCheckUrl) {
    setTimeout(() => {
      const url = getUrl()
      if (url) {
        location.href = url
      }
    }, delay)
  }

  /** Clicks a shop stock item using the site's `eachitemdiv{id}` wrapper. */
  function clickShopItemById(itemId, root = document) {
    const itemToClick = query(`#eachitemdiv${itemId} a`, root)
    itemToClick?.click()
    return itemToClick
  }

  /** Clicks an inventory item using the site's `eachitemdiv{id}` wrapper. */
  function clickInventoryItemById(itemId, root = document) {
    const itemToClick = query(`#eachitemdiv${itemId} a img`, root)?.parentElement
    itemToClick?.click()
    return itemToClick
  }

  /** Returns true after buying an item, on the generic shop confirmation page. */
  function isBoughtItemPage(url = document.URL) {
    return url.includes('/shop.php') && !url.includes('id=')
  }

  /** Returns true on the shop purchase confirmation form page. */
  function isBuyItemPage(url = document.URL) {
    return url.includes('/shop.php?do=buy&id=')
  }

  /** Returns true after the attic has already removed the item. */
  function isAtticRemovePage(url = document.URL) {
    return url.includes('remove=1')
  }

  /** Moves one or more copies of an item from attic to inventory. */
  function moveItemFromAttic({
    amount = 1,
    amountSelector = "[name='amountmove']",
    submitSelector = "[value='Inventory']",
  } = {}) {
    const amountInput = query(amountSelector)
    const submitButton = query(submitSelector)

    if (!amountInput || !submitButton) {
      return false
    }

    amountInput.value = amount
    submitButton.click()
    return true
  }

  /** Clicks the button immediately or waits for a 6-digit captcha entry first. */
  function handleCaptcha(captchaElement, buttonElement) {
    if (!buttonElement) {
      return false
    }

    if (!captchaElement) {
      buttonElement.click()
      return true
    }

    captchaElement.focus()
    captchaElement.oninput = () => {
      if (captchaElement.value.length === 6) {
        buttonElement.click()
      }
    }

    return false
  }

  /** Runs a callback after a random delay and returns the chosen timeout value. */
  function withRandomDelay(callback, min = 300, max = 500) {
    const timeout = Math.random() * (max - min) + min
    setTimeout(callback, timeout)
    return timeout
  }

  global.MaraQuestHelpers = {
    choosePriceCheckUrl,
    clickElement,
    clickInventoryItemById,
    clickPriceCheck,
    clickShopItemById,
    getAtticItemCount,
    getAtticUrl,
    getInStockShopUrl,
    getItemIdFromPriceCheck,
    getStockInfo,
    getUserShopUrl,
    goToPriceCheckResult,
    handleCaptcha,
    isAtticRemovePage,
    isBoughtItemPage,
    isBuyItemPage,
    isPriceCheckRetired,
    isUserShopCheaper,
    moveItemFromAttic,
    query,
    queryAll,
    queryFirst,
    withRandomDelay,
  }
})(globalThis)