MaraQuestHelpers

Questing helpers

Detta skript bör inte installeras direkt. Det är ett bibliotek för andra skript att inkludera med meta-direktivet // @require https://update.greasyfork.org/scripts/573215/1794669/MaraQuestHelpers.js

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!)

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