// ==UserScript== // @name 流放之路2网页市集价格换算 // @description 自动将物品价格换算成崇高石/神圣石, 并将金币价格也计算在内. // @version 1.0.1 // @author Yiero // @match https://poe.game.qq.com/trade2/* // @license GPL-3 // @namespace https://github.com/AliubYiero/TamperMonkeyScripts // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @tag 流放之路2 // ==/UserScript== /* ==UserConfig== 低价通货: alchemyOrb: title: 点金石单价(个/E) description: 点金石单价(个/E) type: number min: 0 unit: 个/E default: 0 augmentationOrb: title: 增幅石单价(个/E) description: 增幅石单价(个/E) type: number min: 0 unit: 个/E default: 0 transmutationOrb: title: 蜕变石单价(个/E) description: 蜕变石单价(个/E) type: number min: 0 unit: 个/E default: 0 regalOrb: title: 富豪石单价(个/E) description: 富豪石单价(个/E) type: number min: 0 unit: 个/E default: 0 vaalOrb: title: 瓦尔宝珠单价(个/E) description: 瓦尔宝珠单价(个/E) type: number min: 0 unit: 个/E default: 0 高价通货: chaosOrb: title: 混沌石单价(个/E) description: 混沌石单价(个/E) type: number min: 0 unit: 个/E default: 0 divineOrb: title: 神圣石单价(个/E) description: 神圣石单价(个/E) type: number min: 0 unit: 个/E default: 0 annulmentOrb: title: 剥离石单价(个/E) description: 剥离石单价(个/E) type: number min: 0 unit: 个/E default: 0 kalandraMirror: title: 卡兰德的魔镜单价(个/D) description: 卡兰德的魔镜单价(个/D) type: number min: 0 unit: 个/D default: 0 设置: feePer10k: title: 金币单价(10k/E) description: 金币单价(10k/E) type: number min: 0 unit: 10k/E default: 0 maxShowExaltedOrb: title: '崇高石价格显示上限 (超出上限会显示神圣石价格)' description: 崇高石价格显示上限 type: number min: 0 unit: 个 default: 300 ==/UserConfig== */ /* * @module : @yiero/gmlib * @author : Yiero * @version : 0.1.23 * @description : GM Lib for Tampermonkey * @keywords : tampermonkey, lib, scriptcat, utils * @license : MIT * @repository : git+https://github.com/AliubYiero/GmLib.git */ var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const returnElement = (selector, options, resolve, reject) => { setTimeout(() => { const element = options.parent.querySelector(selector); if (!element) { reject(new Error("Void Element")); return; } resolve(element); }, options.delayPerSecond * 1e3); }; const getElementByTimer = (selector, options, resolve, reject) => { const intervalDelay = 100; let intervalCounter = 0; const maxIntervalCounter = Math.ceil(options.timeoutPerSecond * 1e3 / intervalDelay); const timer = window.setInterval(() => { if (++intervalCounter > maxIntervalCounter) { clearInterval(timer); returnElement(selector, options, resolve, reject); return; } const element = options.parent.querySelector(selector); if (element) { clearInterval(timer); returnElement(selector, options, resolve, reject); } }, intervalDelay); }; const getElementByMutationObserver = (selector, options, resolve, reject) => { const timer = options.timeoutPerSecond && window.setTimeout(() => { observer.disconnect(); returnElement(selector, options, resolve, reject); }, options.timeoutPerSecond * 1e3); const observeElementCallback = (mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((addNode) => { if (addNode.nodeType !== Node.ELEMENT_NODE) { return; } const addedElement = addNode; const element = addedElement.matches(selector) ? addedElement : addedElement.querySelector(selector); if (element) { timer && clearTimeout(timer); returnElement(selector, options, resolve, reject); } }); }); }; const observer = new MutationObserver(observeElementCallback); observer.observe(options.parent, { subtree: true, childList: true }); return true; }; function elementWaiter(selector, options) { const elementWaiterOptions = { parent: document, timeoutPerSecond: 20, delayPerSecond: 0.5, ...options }; return new Promise((resolve, reject) => { const targetElement = elementWaiterOptions.parent.querySelector(selector); if (targetElement) { returnElement(selector, elementWaiterOptions, resolve, reject); return; } if (MutationObserver) { getElementByMutationObserver(selector, elementWaiterOptions, resolve, reject); return; } getElementByTimer(selector, elementWaiterOptions, resolve, reject); }); } const _gmMenuCommand = class _gmMenuCommand2 { constructor() { } /** * 获取一个菜单按钮 */ static get(title) { const commandButton = this.list.find((commandButton2) => commandButton2.title === title); if (!commandButton) { throw new Error("\u83DC\u5355\u6309\u94AE\u4E0D\u5B58\u5728"); } return commandButton; } /** * 创建一个带有状态的菜单按钮 */ static createToggle(details) { this.create(details.active.title, () => { this.toggleActive(details.active.title); this.toggleActive(details.inactive.title); details.active.onClick(); this.render(); }, true).create(details.inactive.title, () => { this.toggleActive(details.active.title); this.toggleActive(details.inactive.title); details.inactive.onClick(); this.render(); }, false); return _gmMenuCommand2; } /** * 手动激活一个菜单按钮 */ static click(title) { const commandButton = this.get(title); commandButton.onClick(); return _gmMenuCommand2; } /** * 创建一个菜单按钮 */ static create(title, onClick, isActive = true) { if (this.list.some((commandButton) => commandButton.title === title)) { throw new Error("\u83DC\u5355\u6309\u94AE\u5DF2\u5B58\u5728"); } this.list.push({ title, onClick, isActive, id: 0 }); return _gmMenuCommand2; } /** * 删除一个菜单按钮 */ static remove(title) { this.list = this.list.filter((commandButton) => commandButton.title !== title); return _gmMenuCommand2; } /** * 修改两个菜单按钮的顺序 */ static swap(title1, title2) { const index1 = this.list.findIndex((commandButton) => commandButton.title === title1); const index2 = this.list.findIndex((commandButton) => commandButton.title === title2); if (index1 === -1 || index2 === -1) { throw new Error("\u83DC\u5355\u6309\u94AE\u4E0D\u5B58\u5728"); } [this.list[index1], this.list[index2]] = [this.list[index2], this.list[index1]]; return _gmMenuCommand2; } /** * 修改一个菜单按钮 */ static modify(title, details) { const commandButton = this.get(title); details.onClick && (commandButton.onClick = details.onClick); details.isActive && (commandButton.isActive = details.isActive); return _gmMenuCommand2; } /** * 切换菜单按钮激活状态 */ static toggleActive(title) { const commandButton = this.get(title); commandButton.isActive = !commandButton.isActive; return _gmMenuCommand2; } /** * 渲染所有激活的菜单按钮 */ static render() { this.list.forEach((commandButton) => { GM_unregisterMenuCommand(commandButton.id); if (commandButton.isActive) { commandButton.id = GM_registerMenuCommand(commandButton.title, commandButton.onClick); } }); } }; __publicField(_gmMenuCommand, "list", []); class GmStorage { constructor(key, defaultValue) { __publicField(this, "listenerId", 0); this.key = key; this.defaultValue = defaultValue; this.key = key; this.defaultValue = defaultValue; } /** * 获取当前存储的值 * * @alias get() */ get value() { return this.get(); } /** * 获取当前存储的值 */ get() { return GM_getValue(this.key, this.defaultValue); } /** * 给当前存储设置一个新值 */ set(value) { return GM_setValue(this.key, value); } /** * 移除当前键 */ remove() { GM_deleteValue(this.key); } /** * 监听元素更新, 同时只能存在 1 个监听器 */ updateListener(callback) { this.removeListener(); this.listenerId = GM_addValueChangeListener(this.key, (key, oldValue, newValue, remote) => { callback({ key, oldValue, newValue, remote }); }); } /** * 移除元素更新回调 */ removeListener() { GM_removeValueChangeListener(this.listenerId); } } const unitMapper = { "\u5D07\u9AD8\u77F3": "exaltedOrb", "\u795E\u5723\u77F3": "divineOrb", "\u70B9\u91D1\u77F3": "alchemyOrb", "\u91D1\u5E01": "feePer10k", "\u589E\u5E45\u77F3": "augmentationOrb", "\u8715\u53D8\u77F3": "transmutationOrb", "\u5BCC\u8C6A\u77F3": "regalOrb", "\u6DF7\u6C8C\u77F3": "chaosOrb", "\u74E6\u5C14\u5B9D\u73E0": "vaalOrb", "\u5265\u79BB\u77F3": "annulmentOrb", "\u5361\u5170\u5FB7\u7684\u9B54\u955C": "kalandraMirror" }; const maxShowExaltedOrb = new GmStorage("\u8BBE\u7F6E.maxShowExaltedOrb", 300).get(); const storeKeyList = [ "\u9AD8\u4EF7\u901A\u8D27.annulmentOrb", "\u9AD8\u4EF7\u901A\u8D27.kalandraMirror", "\u9AD8\u4EF7\u901A\u8D27.chaosOrb", "\u9AD8\u4EF7\u901A\u8D27.divineOrb", "\u4F4E\u4EF7\u901A\u8D27.alchemyOrb", "\u4F4E\u4EF7\u901A\u8D27.augmentationOrb", "\u4F4E\u4EF7\u901A\u8D27.regalOrb", "\u4F4E\u4EF7\u901A\u8D27.transmutationOrb", "\u4F4E\u4EF7\u901A\u8D27.vaalOrb", "\u8BBE\u7F6E.feePer10k", "\u8BBE\u7F6E.exaltedOrb" ]; const storeEntryList = storeKeyList.map((storeKey) => { const key = storeKey.replace(/^(高价通货|低价通货|常见通货|设置)\./, ""); const value = new GmStorage(storeKey, 0).get(); return [key, value]; }); const unitPrice = Object.fromEntries(storeEntryList); const calculateItemPrice = (itemContainer) => { const resultItemPrice = { totalPrice: 0, unit: "\u5D07\u9AD8\u77F3", goldCoinPrice: 0, origin: { price: 0, unit: "", goldCoin: 0 } }; const [priceElement, _, unitElement] = Array.from( itemContainer.querySelectorAll('.right [data-field="price"] > span:not(.price-label.buyout-price)') ); const feeElement = itemContainer.querySelector('.right [data-field="fee"] > span:last-child'); const feeNumber = feeElement ? Number((feeElement.textContent || "").replace(/,/g, "")) : 0; const feePricePerExaltedOrb = feeNumber / 1e4 * unitPrice.feePer10k; resultItemPrice.origin.goldCoin = feeNumber; resultItemPrice.goldCoinPrice = feePricePerExaltedOrb; let originPrice = priceElement ? Number(priceElement.textContent) : 0; resultItemPrice.origin.price = originPrice; const unit = unitElement ? unitElement.textContent : ""; if (!unit) { console.warn("[poe2-price-calc]", "\u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u7269\u54C1\u7684\u4EF7\u683C\u5355\u4F4D", itemContainer); return null; } const unitKey = unitMapper[unit]; if (!unitKey) { console.warn("[poe2-price-calc]", "\u672A\u8BC6\u522B\u5230\u5F53\u524D\u5355\u4F4D", unit); return null; } resultItemPrice.origin.unit = unit; const divineOrbUnitPrice = unitPrice.divineOrb; if (unit === "\u5361\u5170\u5FB7\u7684\u9B54\u955C" && unitPrice.kalandraMirror) { resultItemPrice.unit = "\u795E\u5723\u77F3"; resultItemPrice.totalPrice = originPrice / unitPrice.kalandraMirror + feePricePerExaltedOrb * divineOrbUnitPrice; return resultItemPrice; } if (unit === "\u5D07\u9AD8\u77F3") { resultItemPrice.totalPrice = originPrice + feePricePerExaltedOrb; } if (unit !== "\u5D07\u9AD8\u77F3" && unitPrice[unitKey] === 0) { console.warn("[poe2-price-calc]", "\u5F53\u524D\u5355\u4F4D\u672A\u586B\u5199\u5355\u4EF7", unit); return null; } const currentUnitPrice = unit === "\u5D07\u9AD8\u77F3" ? 1 : unitPrice[unitKey]; const resultExaltedOrbPrice = originPrice / currentUnitPrice + feePricePerExaltedOrb; resultItemPrice.totalPrice = resultExaltedOrbPrice; if (resultExaltedOrbPrice >= maxShowExaltedOrb) { resultItemPrice.unit = "\u795E\u5723\u77F3"; resultItemPrice.totalPrice = resultExaltedOrbPrice * divineOrbUnitPrice; } return resultItemPrice; }; const appendPriceToItem = (itemContainer, itemPrice) => { const imageHtml = { "\u5D07\u9AD8\u77F3": `exalted`, "\u795E\u5723\u77F3": `divine` }; const calcPriceElement = document.createElement("span"); calcPriceElement.className = "s sorted"; calcPriceElement.style.borderTop = "2px solid #a38d6d"; calcPriceElement.innerHTML = ` \u603B\u4EF7:
${itemPrice.totalPrice.toFixed(2)} \xD7 ${imageHtml[itemPrice.unit]} ${itemPrice.unit} `; const appendParentContainer = itemContainer.querySelector(".price"); const referenceElement = itemContainer.querySelector('span[data-field="fee"]'); if (!appendParentContainer || !referenceElement) { return; } appendParentContainer.insertBefore(calcPriceElement, referenceElement); itemContainer.dataset.calculated = "true"; }; const handleAppendPrice = (itemElement) => { if (itemElement.dataset.calculated || itemElement.classList.contains("controls")) { return; } const itemPrice = calculateItemPrice(itemElement); if (!itemPrice) { console.warn("[poe2-price-calc]", "\u65E0\u6CD5\u8BA1\u7B97\u4EF7\u683C", itemElement); return; } appendPriceToItem(itemElement, itemPrice); }; const createAddedElementMutationObserver = (callback) => { return new MutationObserver((records) => { for (let record of records) { for (let addedNode of record.addedNodes) { if (addedNode.nodeType !== Node.ELEMENT_NODE) { continue; } const addedElement = addedNode; callback(addedElement); } } }); }; const main = async () => { const itemResultObserver = createAddedElementMutationObserver(handleAppendPrice); const startItemLoader = async () => { const resultSetContainer = await elementWaiter(".resultset", { delayPerSecond: 0 }); const itemElementList = Array.from(resultSetContainer.children); itemElementList.forEach(handleAppendPrice); itemResultObserver.observe(resultSetContainer, { childList: true }); }; startItemLoader().catch(console.error); const results = await elementWaiter(".results"); const itemChangeObserver = createAddedElementMutationObserver((addElement) => { if (!addElement.classList.contains("resultset")) { return; } itemResultObserver.disconnect(); startItemLoader().catch(console.error); }); itemChangeObserver.observe(results, { childList: true }); }; main().catch((error) => { console.error(error); });