Questing helpers
Skrip ini tidak untuk dipasang secara langsung. Ini adalah pustaka skrip lain untuk disertakan dengan direktif meta // @require https://update.greasyfork.org/scripts/573215/1794669/MaraQuestHelpers.js
;(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)