POE Trade Improvements

Improvements to the trade site. Adds a button for "add resistances" and "add attributes" to create stat weight groups

目前为 2025-04-04 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name POE Trade Improvements
  3. // @namespace Kylixen
  4. // @version 2025-01-27
  5. // @description Improvements to the trade site. Adds a button for "add resistances" and "add attributes" to create stat weight groups
  6. // @author Kylixen
  7. // @match https://www.pathofexile.com/trade2/search/poe2/Standard
  8. // @match https://www.pathofexile.com/trade2/search/poe2/Standard/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=pathofexile.com
  10. // @grant none
  11. // @license GNU GPLv3
  12. // ==/UserScript==
  13.  
  14. // debugger;
  15.  
  16. var typingSpeed = 100; // milliseconds per character
  17. async function simulateTyping(element, text, delay = 100) {
  18. let index = 0;
  19.  
  20. function fireEvent(eventType, key) {
  21. const event = new KeyboardEvent(eventType, {
  22. key: key,
  23. code: key.charCodeAt(0),
  24. bubbles: true,
  25. cancelable: true
  26. });
  27. element.dispatchEvent(event);
  28. }
  29.  
  30. function fireInputEvent() {
  31. const inputEvent = new InputEvent("input", {
  32. bubbles: true,
  33. cancelable: true
  34. });
  35. element.dispatchEvent(inputEvent);
  36. }
  37.  
  38. function fireChangeEvent() {
  39. const inputEvent = new InputEvent("change", {
  40. bubbles: true,
  41. cancelable: true
  42. });
  43. element.dispatchEvent(inputEvent);
  44. }
  45.  
  46. async function typeNextChar() {
  47. if (index < text.length) {
  48. const char = text[index];
  49. fireEvent("keydown", char);
  50. fireEvent("keypress", char);
  51.  
  52. if (element.isContentEditable) {
  53. element.textContent += char;
  54. } else {
  55. element.value += char;
  56. }
  57.  
  58. fireInputEvent();
  59. fireEvent("keyup", char);
  60.  
  61. index++;
  62. await setTimeout(null, delay)
  63. await typeNextChar();
  64. } else {
  65. fireChangeEvent();
  66. }
  67. }
  68.  
  69. await typeNextChar();
  70. }
  71.  
  72. function errorOnUndef(f, errorMessage) {
  73. return function (...args) {
  74. var res = f(...args)
  75. if(res === undefined)
  76. throw errorMessage
  77. return res;
  78. }
  79. }
  80.  
  81. function errorHandled(f) {
  82. return function(...args) {
  83. try {
  84. return f(...args);
  85. } catch(error) {
  86. console.error(error);
  87. }
  88. }
  89. }
  90.  
  91. function asyncErrorOnUndef(f, errorMessage) {
  92. return async function (...args) {
  93. var res = await f(...args)
  94. if(res === undefined)
  95. throw errorMessage
  96. return res;
  97. }
  98. }
  99.  
  100. function asyncErrorHandled(f) {
  101. return async function(...args) {
  102. try {
  103. return await f(...args);
  104. } catch(error) {
  105. console.error(error);
  106. }
  107. }
  108. }
  109.  
  110. function first(iterable) {
  111. if(!iterable || iterable.length == 0)
  112. return undefined;
  113. return iterable[0];
  114. }
  115.  
  116. function last(iterable) {
  117. if(!iterable || iterable.length == 0)
  118. return undefined;
  119. return iterable[iterable.length -1 ];
  120. }
  121.  
  122. const getAddStatGroupDiv = errorOnUndef(function() {
  123. return document.querySelector(".filter-group-select")
  124. }, "Couldn't find the +Add Stat Group <div>");
  125.  
  126. async function addNewStatGroup(statGroupType) {
  127.  
  128. var divAddStatGroup = getAddStatGroupDiv();
  129.  
  130. var options = Array.from(divAddStatGroup.querySelectorAll("li>span") || []);
  131.  
  132. if(!options)
  133. throw "Couldn't find options in Add Stat Group div";
  134.  
  135. var groupMatch = new RegExp(`^\s*${statGroupType}\s*$`, "i");
  136.  
  137. var option = first(options.filter(function(opt) {
  138. if(groupMatch.test(opt.textContent))
  139. return true;
  140. return false;
  141. }))
  142.  
  143. if(!option)
  144. throw "Couldn't find the stat group type " + statGroupType;
  145.  
  146. option.click();
  147.  
  148. await setTimeout(null, 3);
  149.  
  150. return last(document.querySelectorAll("div.filter-group"));
  151. };
  152.  
  153. const addWeightedResistances = asyncErrorHandled(async function() {
  154.  
  155. // Add the new panel
  156. const statGroupDiv = await addNewStatGroup("weighted sum v2");
  157.  
  158. // type "resistance" into the input so we get all the elements we need)
  159. const statFilterDiv = last(document.querySelectorAll("div.filter-select-mutate"));
  160. const statInput = statFilterDiv.querySelector("input");
  161.  
  162. statInput.focus();
  163. await simulateTyping(statInput, "resistance", 0);
  164.  
  165. // click all the resistances
  166. statFilterDiv.querySelectorAll("span.multiselect__option").forEach((optionSpan) => {
  167. if(!((optionSpan.textContent || "").match(/^(explicit|implicit) #% to (Cold|Fire|Lightning|Chaos|All Elemental) resistances?$/i)))
  168. return;
  169. console.debug(optionSpan.textContent);
  170. optionSpan.click();
  171. });
  172.  
  173. await setTimeout(null, 6);
  174.  
  175. // Set the weight for all/chaos
  176. statGroupDiv.querySelectorAll("span.filter-body:has(.weight)").forEach(async (optionSpan) => {
  177. if((optionSpan.innerText || "").match(/All Elemental/i)) {
  178. const weightInput = optionSpan.querySelector("input.weight");
  179. weightInput.focus();
  180. await simulateTyping(weightInput, "3", 0);
  181. await setTimeout(null, 6);
  182. weightInput.blur();
  183. }
  184. });
  185. });
  186.  
  187. const addWeightedAttributes = asyncErrorHandled(async function() {
  188.  
  189. // Add the new panel
  190. const statGroupDiv = await addNewStatGroup("weighted sum v2");
  191.  
  192. // type "resistance" into the input so we get all the elements we need)
  193. const statFilterDiv = last(document.querySelectorAll("div.filter-select-mutate"));
  194. const statInput = statFilterDiv.querySelector("input");
  195.  
  196. // statInput.focus();
  197. // await simulateTyping(statInput, "resistance", 0);
  198.  
  199. // click all the resistances
  200. statFilterDiv.querySelectorAll("span.multiselect__option").forEach((optionSpan) => {
  201. if(!((optionSpan.textContent || "").match(/^(explicit|implicit) #% to (Cold|Fire|Lightning|Chaos|All Elemental) resistances?$/i)))
  202. return;
  203. console.debug(optionSpan.textContent);
  204. optionSpan.click();
  205. });
  206.  
  207. await setTimeout(null, 6);
  208.  
  209. // Set the weight for all/chaos
  210. statGroupDiv.querySelectorAll("span.filter-body:has(.weight)").forEach(async (optionSpan) => {
  211. if((optionSpan.innerText || "").match(/All Elemental/i)) {
  212. const weightInput = optionSpan.querySelector("input.weight");
  213. weightInput.focus();
  214. await simulateTyping(weightInput, "3", 0);
  215. await setTimeout(null, 6);
  216. weightInput.blur();
  217. }
  218. });
  219.  
  220. });
  221. ;
  222. function addResistancesClick() {
  223. addWeightedResistances();
  224. // Clean up button
  225. }
  226.  
  227. function addAttributesClick() {
  228. addWeightedAttributes();
  229. // Clean up button
  230. }
  231.  
  232. function waitFor(waitForFunction, successCallback) {
  233. (new MutationObserver(function (changes, observer) {
  234. if (!waitForFunction())
  235. return;
  236. observer.disconnect();
  237. successCallback();
  238. })).observe(document, { childList: true, subtree: true });
  239. }
  240.  
  241. (function () {
  242. 'use strict';
  243. (new MutationObserver(function (changes, observer) {
  244. if (!document.querySelector('.filter-group-select'))
  245. return;
  246. observer.disconnect();
  247. onLoad();
  248. })).observe(document, { childList: true, subtree: true });
  249.  
  250. function onLoad() {
  251. const searchDiv = document.querySelector("div.search-advanced-pane");
  252. const newElement = document.createElement('div');
  253. newElement.className = "filter-group expanded";
  254. newElement.innerHTML = `
  255. <div class="filter-group-body">
  256. <button id='add-resistance-button' type="button" class='btn search-btn' onclick='addResistancesClick()'>
  257. <span>Add Resistances</span>
  258. </button>
  259. <button id='add-attributes-button' type="button" class='btn search-btn' onclick='addAttributesClick()'>
  260. <span>Add Attributes</span>
  261. </button>
  262. </div>
  263. `;
  264. searchDiv.prepend(newElement);
  265. }
  266. })();
  267.  
  268. window.addResistancesClick = addResistancesClick;
  269. window.addAttributesClick = addAttributesClick;
  270.  
  271. window.ky = {
  272. addResistancesClick,
  273. addWeightedResistances,
  274. addWeightedAttributes,
  275. addNewStatGroup,
  276. simulateTyping,
  277. last,
  278. first,
  279. }