// ==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": `
`,
"\u795E\u5723\u77F3": `
`
};
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);
});