Torn Bazaar Filler

On "Fill" click autofills bazaar item price with lowest bazaar price currently minus $1 (can be customised), shows current price coefficient compared to 3rd lowest, fills max quantity for items, marks checkboxes for guns.

当前为 2023-09-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Torn Bazaar Filler
  3. // @namespace https://github.com/SOLiNARY
  4. // @version 0.8
  5. // @description On "Fill" click autofills bazaar item price with lowest bazaar price currently minus $1 (can be customised), shows current price coefficient compared to 3rd lowest, fills max quantity for items, marks checkboxes for guns.
  6. // @author Ramin Quluzade, Silmaril [2665762]
  7. // @license MIT License
  8. // @match https://www.torn.com/bazaar.php*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
  11. // @run-at document-idle
  12. // @grant GM_addStyle
  13. // @grant GM_registerMenuCommand
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. const bazaarUrl = "https://api.torn.com/market/{itemId}?selections=bazaar&key={apiKey}";
  20. let priceDeltaRaw = localStorage.getItem("silmaril-torn-bazaar-filler-price-delta") ?? '-1';
  21. let apiKey = localStorage.getItem("silmaril-torn-bazaar-filler-apikey");
  22.  
  23. try {
  24. GM_registerMenuCommand('Set Price Delta', setPriceDelta);
  25. GM_registerMenuCommand('Set Api Key', function() { checkApiKey(false); });
  26. } catch (error) {
  27. console.log('[TornBazaarFiller] Tampermonkey not detected!');
  28. }
  29.  
  30. // TornPDA support for GM_addStyle
  31. let GM_addStyle = function (s) {
  32. let style = document.createElement("style");
  33. style.type = "text/css";
  34. style.innerHTML = s;
  35. document.head.appendChild(style);
  36. };
  37.  
  38. GM_addStyle(`
  39. .btn-wrap.torn-bazaar-fill-qty-price {
  40. float: right;
  41. margin-left: auto;
  42. z-index: 99999;
  43. }
  44.  
  45. .btn-wrap.torn-bazaar-clear-qty-price {
  46. z-index: 99999;
  47. }
  48.  
  49. div.title-wrap div.name-wrap {
  50. display: flex;
  51. justify-content: flex-end;
  52. }
  53.  
  54. .wave-animation {
  55. position: relative;
  56. overflow: hidden;
  57. }
  58.  
  59. .wave {
  60. pointer-events: none;
  61. position: absolute;
  62. width: 100%;
  63. height: 33px;
  64. background-color: transparent;
  65. opacity: 0;
  66. transform: translateX(-100%);
  67. animation: waveAnimation 1s cubic-bezier(0, 0, 0, 1);
  68. }
  69.  
  70. @keyframes waveAnimation {
  71. 0% {
  72. opacity: 1;
  73. transform: translateX(-100%);
  74. }
  75. 100% {
  76. opacity: 0;
  77. transform: translateX(100%);
  78. }
  79. }
  80.  
  81. .overlay-percentage {
  82. position: absolute;
  83. top: 0px;
  84. background-color: rgba(0, 0, 0, 0.9);
  85. padding: 0px 5px;
  86. border-radius: 15px;
  87. font-size: 10px;
  88. }
  89.  
  90. .overlay-percentage-add {
  91. right: -30px;
  92. }
  93.  
  94. .overlay-percentage-manage {
  95. right: 0px;
  96. }
  97. `);
  98.  
  99. const pages = { "AddItems": 10, "ManageItems": 20};
  100. const addItemsLabels = ["Fill", "Clear"];
  101. const updateItemsLabels = ["Update", "Clear"];
  102.  
  103. const viewPortWidthPx = window.innerWidth;
  104. const isMobileView = viewPortWidthPx <= 784;
  105.  
  106. const observerTarget = $(".content-wrapper")[0];
  107. const observerConfig = { attributes: false, childList: true, characterData: false, subtree: true };
  108.  
  109. const observer = new MutationObserver(function(mutations) {
  110. let mutation = mutations[0].target;
  111. if (mutation.classList.contains("items-cont") || mutation.classList.contains("core-layout___uf3LW")) {
  112. $("ul.ui-tabs-nav").on("click", "li:not(.ui-state-active):not(.ui-state-disabled):not(.m-show)", function() {
  113. observer.observe(observerTarget, observerConfig);
  114. });
  115. $("div.topSection___U7sVi").on("click", "div.linksContainer___LiOTN a[aria-labelledby=add-items]", function(){
  116. observer.observe(observerTarget, observerConfig);
  117. });
  118. $("div.topSection___U7sVi").on("click", "div.listItem___Q3FFU a[aria-labelledby=add-items]", function(){
  119. observer.observe(observerTarget, observerConfig);
  120. });
  121. $("div.topSection___U7sVi").on("click", "div.linksContainer___LiOTN a[aria-labelledby=manage-items]", function(){
  122. observer.observe(observerTarget, observerConfig);
  123. });
  124. $("div.topSection___U7sVi").on("click", "div.listItem___Q3FFU a[aria-labelledby=manage-items]", function(){
  125. observer.observe(observerTarget, observerConfig);
  126. });
  127.  
  128. let containerItems = $("ul.items-cont li.clearfix");
  129. containerItems.find("div.title-wrap div.name-wrap").each(function(){
  130. let isParentRowDisabled = this.parentElement.parentElement.classList.contains("disabled");
  131. let alreadyHasFillBtn = this.querySelector(".btn-wrap.torn-bazaar-fill-qty-price") != null;
  132. if (!alreadyHasFillBtn && !isParentRowDisabled){
  133. insertFillAndWaveBtn(this, addItemsLabels, pages.AddItems);
  134. }
  135. });
  136.  
  137. let containerItemsManage = $("div.row___n2Uxh");
  138. containerItemsManage.find("div.item___jLJcf div.desc___VJSNQ").each(function(){
  139. let alreadyHasUpdateBtn = this.querySelector(".btn-wrap.torn-bazaar-fill-qty-price") != null;
  140. if (!alreadyHasUpdateBtn) {
  141. insertFillAndWaveBtn(this, updateItemsLabels, pages.ManageItems);
  142. }
  143. });
  144.  
  145. if($("div.amount").length || $("div.desc___VJSNQ").length) {
  146. observer.disconnect();
  147. }
  148. }
  149. });
  150. observer.observe(observerTarget, observerConfig);
  151.  
  152. function insertFillAndWaveBtn(element, buttonLabels, pageType){
  153. const waveDiv = document.createElement('div');
  154. waveDiv.className = 'wave';
  155.  
  156. const outerSpanFill = document.createElement('span');
  157. outerSpanFill.className = 'btn-wrap torn-bazaar-fill-qty-price';
  158. const outerSpanClear = document.createElement('span');
  159. outerSpanClear.className = 'btn-wrap torn-bazaar-clear-qty-price';
  160.  
  161. const innerSpanFill = document.createElement('span');
  162. innerSpanFill.className = 'btn';
  163. const innerSpanClear = document.createElement('span');
  164. innerSpanClear.className = 'btn';
  165. innerSpanClear.style.display = 'none';
  166.  
  167. const inputElementFill = document.createElement('input');
  168. inputElementFill.type = 'button';
  169. inputElementFill.value = buttonLabels[0];
  170. inputElementFill.className = 'torn-btn';
  171. const inputElementClear = document.createElement('input');
  172. inputElementClear.type = 'button';
  173. inputElementClear.value = buttonLabels[1];
  174. inputElementClear.className = 'torn-btn';
  175.  
  176. innerSpanFill.appendChild(inputElementFill);
  177. innerSpanClear.appendChild(inputElementClear);
  178. outerSpanFill.appendChild(innerSpanFill);
  179. outerSpanClear.appendChild(innerSpanClear);
  180.  
  181. element.append(outerSpanFill, outerSpanClear, waveDiv);
  182.  
  183. switch(pageType) {
  184. case pages.AddItems:
  185. $(outerSpanFill).on("click", "input", function(event) {
  186. checkApiKey();
  187. this.parentNode.style.display = "none";
  188. fillQuantityAndPrice(this, pageType);
  189. event.stopPropagation();
  190. });
  191.  
  192. $(outerSpanClear).on("click", "input", function(event) {
  193. this.parentNode.style.display = "none";
  194. clearQuantityAndPrice(this);
  195. event.stopPropagation();
  196. });
  197. break;
  198. case pages.ManageItems:
  199. $(outerSpanFill).on("click", "input", function(event) {
  200. checkApiKey();
  201. // this.parentNode.style.display = "none";
  202. updatePrice(this);
  203. event.stopPropagation();
  204. });
  205.  
  206. // $(outerSpanClear).on("click", "input", function(event) {
  207. // this.parentNode.style.display = "none";
  208. // clearQuantity(this, pageType);
  209. // event.stopPropagation();
  210. // });
  211. break;
  212. }
  213.  
  214. }
  215.  
  216. function insertPercentageSpan(element){
  217. let moneyGroupDiv = element.querySelector("div.price div.input-money-group");
  218.  
  219. if (moneyGroupDiv.querySelector("span.overlay-percentage") === null) {
  220. const percentageSpan = document.createElement('span');
  221. percentageSpan.className = 'overlay-percentage overlay-percentage-add';
  222. moneyGroupDiv.appendChild(percentageSpan);
  223. }
  224.  
  225. return moneyGroupDiv.querySelector("span.overlay-percentage");
  226. }
  227.  
  228. function insertPercentageManageSpan(element){
  229. let moneyGroupDiv = element.querySelector("div.input-money-group");
  230.  
  231. if (moneyGroupDiv.querySelector("span.overlay-percentage") === null) {
  232. const percentageSpan = document.createElement('span');
  233. percentageSpan.className = 'overlay-percentage overlay-percentage-manage';
  234. moneyGroupDiv.appendChild(percentageSpan);
  235. }
  236.  
  237. return moneyGroupDiv.querySelector("span.overlay-percentage");
  238. }
  239.  
  240. function fillQuantityAndPrice(element, pageType){
  241. let amountDiv = element.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector("div.amount-main-wrap");
  242. let priceInputs = amountDiv.querySelectorAll("div.price div input");
  243. let keyupEvent = new Event("keyup", {bubbles: true});
  244. let inputEvent = new Event("input", {bubbles: true});
  245.  
  246. let image = element.parentElement.parentElement.parentElement.parentElement.querySelector("div.image-wrap img");
  247. let numberPattern = /\/(\d+)\//;
  248. let match = image.src.match(numberPattern);
  249. let extractedItemId = 0;
  250. if (match) {
  251. extractedItemId = parseInt(match[1], 10);
  252. } else {
  253. console.error("[TornBazaarFiller] ItemId not found!");
  254. }
  255.  
  256. let requestUrl = bazaarUrl
  257. .replace("{itemId}", extractedItemId)
  258. .replace("{apiKey}", apiKey);
  259.  
  260. let wave = element.parentElement.parentElement.parentElement.querySelector("div.wave");
  261. fetch(requestUrl)
  262. .then(response => response.json())
  263. .then(data => {
  264. if (data.error != null && data.error.code === 2){
  265. apiKey = null;
  266. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
  267. wave.style.backgroundColor = "red";
  268. wave.style.animationDuration = "5s";
  269. console.error("[TornBazaarFiller] Incorrect Api Key:", data);
  270. return;
  271. }
  272. let lowBallPrice = Math.round(performOperation(data.bazaar[0].cost, priceDeltaRaw));
  273. let price3rd = data.bazaar[Math.min(2, data.bazaar.length - 1)].cost;
  274. let priceCoefficient = ((lowBallPrice / price3rd) * 100).toFixed(0);
  275.  
  276. let percentageOverlaySpan = insertPercentageSpan(amountDiv);
  277. if (priceCoefficient <= 95){
  278. percentageOverlaySpan.style.display = "block";
  279. if (priceCoefficient <= 50){
  280. percentageOverlaySpan.style.color = "red";
  281. wave.style.backgroundColor = "red";
  282. wave.style.animationDuration = "5s";
  283. } else if (priceCoefficient <= 75){
  284. percentageOverlaySpan.style.color = "yellow";
  285. wave.style.backgroundColor = "yellow";
  286. wave.style.animationDuration = "3s";
  287. } else {
  288. percentageOverlaySpan.style.color = "green";
  289. wave.style.backgroundColor = "green";
  290. }
  291. percentageOverlaySpan.innerText = priceCoefficient + "%";
  292. } else {
  293. percentageOverlaySpan.style.display = "none";
  294. wave.style.backgroundColor = "green";
  295. }
  296.  
  297. priceInputs[0].value = lowBallPrice;
  298. priceInputs[1].value = lowBallPrice;
  299. priceInputs[0].dispatchEvent(inputEvent);
  300. })
  301. .catch(error => {
  302. wave.style.backgroundColor = "red";
  303. wave.style.animationDuration = "5s";
  304. console.error("[TornBazaarFiller] Error fetching data:", error);
  305. })
  306. .finally(() => {
  307. element.parentNode.parentNode.parentNode.querySelector("span.btn-wrap.torn-bazaar-clear-qty-price span.btn").style.display = "inline-block";
  308. });
  309. wave.style.animation = 'none';
  310. wave.offsetHeight;
  311. wave.style.animation = null;
  312. wave.style.backgroundColor = "transparent";
  313. wave.style.animationDuration = "1s";
  314.  
  315. let isQuantityCheckbox = amountDiv.querySelector("div.amount.choice-container") !== null;
  316. if (isQuantityCheckbox){
  317. amountDiv.querySelector("div.amount.choice-container input").click();
  318. } else {
  319. let quantityInput = amountDiv.querySelector("div.amount input");
  320. quantityInput.value = getQuantity(element, pageType);
  321. quantityInput.dispatchEvent(keyupEvent);
  322. }
  323. }
  324.  
  325. function updatePrice(element){
  326. let moneyGroupDiv;
  327. let parentNode4 = element.parentNode.parentNode.parentNode.parentNode;
  328. if (isMobileView){
  329. if (parentNode4.querySelector(".menuActivators___STzEc button.iconContainer___H3dWv[aria-label=Manage] span.active___OTFsm") == null) {
  330. parentNode4.querySelector(".menuActivators___STzEc button.iconContainer___H3dWv[aria-label=Manage]").click();
  331. }
  332. moneyGroupDiv = parentNode4.parentNode.querySelector(".bottomMobileMenu___CCSjc .priceMobile___cpt8p");
  333. } else {
  334. moneyGroupDiv = element.parentNode.parentNode.parentNode.parentNode.querySelector("div.price___DoKP7");
  335. }
  336. let priceInputs = moneyGroupDiv.querySelectorAll("div.input-money-group input");
  337. let inputEvent = new Event("input", {bubbles: true});
  338.  
  339. let image = element.parentElement.parentElement.parentElement.parentElement.querySelector("div.imgContainer___tEZeE img");
  340. let extractedItemId = getItemIdFromImage(image);
  341.  
  342. let requestUrl = bazaarUrl
  343. .replace("{itemId}", extractedItemId)
  344. .replace("{apiKey}", apiKey);
  345.  
  346. let wave = element.parentElement.parentElement.parentElement.querySelector("div.wave");
  347. fetch(requestUrl)
  348. .then(response => response.json())
  349. .then(data => {
  350. if (data.error != null && data.error.code === 2){
  351. apiKey = null;
  352. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
  353. wave.style.backgroundColor = "red";
  354. wave.style.animationDuration = "5s";
  355. console.error("[TornBazaarFiller] Incorrect Api Key:", data);
  356. return;
  357. }
  358. let lowBallPrice = Math.round(performOperation(data.bazaar[0].cost, priceDeltaRaw));;
  359. let price3rd = data.bazaar[Math.min(2, data.bazaar.length - 1)].cost;
  360. let priceCoefficient = ((lowBallPrice / price3rd) * 100).toFixed(0);
  361.  
  362. let percentageOverlaySpan = insertPercentageManageSpan(moneyGroupDiv);
  363. if (priceCoefficient <= 95){
  364. percentageOverlaySpan.style.display = "block";
  365. if (priceCoefficient <= 50){
  366. percentageOverlaySpan.style.color = "red";
  367. wave.style.backgroundColor = "red";
  368. wave.style.animationDuration = "5s";
  369. } else if (priceCoefficient <= 75){
  370. percentageOverlaySpan.style.color = "yellow";
  371. wave.style.backgroundColor = "yellow";
  372. wave.style.animationDuration = "3s";
  373. } else {
  374. percentageOverlaySpan.style.color = "green";
  375. wave.style.backgroundColor = "green";
  376. }
  377. percentageOverlaySpan.innerText = priceCoefficient + "%";
  378. } else {
  379. percentageOverlaySpan.style.display = "none";
  380. wave.style.backgroundColor = "green";
  381. }
  382.  
  383. priceInputs[0].value = lowBallPrice;
  384. priceInputs[1].value = lowBallPrice;
  385. priceInputs[0].dispatchEvent(inputEvent);
  386. })
  387. .catch(error => {
  388. wave.style.backgroundColor = "red";
  389. wave.style.animationDuration = "5s";
  390. console.error("[TornBazaarFiller] Error fetching data:", error);
  391. })
  392. .finally(() => {
  393. // element.parentNode.parentNode.parentNode.querySelector("span.btn-wrap.torn-bazaar-clear-qty-price span.btn").style.display = "inline-block";
  394. });
  395. wave.style.animation = 'none';
  396. wave.offsetHeight;
  397. wave.style.animation = null;
  398. wave.style.backgroundColor = "transparent";
  399. wave.style.animationDuration = "1s";
  400. }
  401.  
  402. function clearQuantityAndPrice(element){
  403. let amountDiv = element.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector("div.amount-main-wrap");
  404. let priceInputs = amountDiv.querySelectorAll("div.price div input");
  405. let keyupEvent = new Event("keyup", {bubbles: true});
  406. let inputEvent = new Event("input", {bubbles: true});
  407.  
  408. let wave = element.parentElement.parentElement.parentElement.querySelector("div.wave");
  409. wave.style.backgroundColor = "white";
  410.  
  411. let isQuantityCheckbox = amountDiv.querySelector("div.amount.choice-container") !== null;
  412. if (isQuantityCheckbox){
  413. amountDiv.querySelector("div.amount.choice-container input").click();
  414. } else {
  415. let quantityInput = amountDiv.querySelector("div.amount input");
  416. quantityInput.value = "";
  417. quantityInput.dispatchEvent(keyupEvent);
  418. }
  419.  
  420. priceInputs[0].value = "";
  421. priceInputs[1].value = "";
  422. priceInputs[0].dispatchEvent(inputEvent);
  423.  
  424. wave.style.animation = 'none';
  425. wave.offsetHeight;
  426. wave.style.animation = null;
  427.  
  428. element.parentNode.parentNode.parentNode.querySelector("span.btn-wrap.torn-bazaar-fill-qty-price span.btn").style.display = "inline-block";
  429. }
  430.  
  431. // function clearQuantity(element, pageType){
  432. // let itemRow = element.parentNode.parentNode.parentNode.parentNode;
  433. // let moneyGroupDiv = itemRow.querySelector("div.price___DoKP7");
  434. // let keyupEvent = new Event("keyup", {bubbles: true});
  435.  
  436. // let wave = element.parentElement.parentElement.parentElement.querySelector("div.wave");
  437. // wave.style.backgroundColor = "white";
  438.  
  439. // let quantityInput = itemRow.querySelector("div.remove___R4eVW input");
  440. // quantityInput.value = getQuantity(element, pageType);
  441. // quantityInput.dispatchEvent(keyupEvent);
  442.  
  443. // wave.style.animation = 'none';
  444. // wave.offsetHeight;
  445. // wave.style.animation = null;
  446.  
  447. // element.parentNode.parentNode.parentNode.querySelector("span.btn-wrap.torn-bazaar-fill-qty-price span.btn").style.display = "inline-block";
  448. // }
  449.  
  450. function getQuantity(element, pageType){
  451. let rgx = /x(\d+)$/;
  452. let rgxMobile = /^x(\d+)/
  453. let quantityText = 0;
  454. switch(pageType){
  455. case pages.AddItems:
  456. quantityText = element.parentNode.parentNode.parentNode.innerText;
  457. break;
  458. case pages.ManageItems:
  459. quantityText = element.parentNode.parentNode.parentNode.querySelector("span").innerText;
  460. break;
  461. }
  462. let match = isMobileView ? rgxMobile.exec(quantityText) : rgx.exec(quantityText);
  463. let quantity = match === null ? 1 : match[1];
  464. return quantity;
  465. }
  466.  
  467. function getItemIdFromImage(image){
  468. let numberPattern = /\/(\d+)\//;
  469. let match = image.src.match(numberPattern);
  470. if (match) {
  471. return parseInt(match[1], 10);
  472. } else {
  473. console.error("[TornBazaarFiller] ItemId not found!");
  474. }
  475. }
  476.  
  477. function performOperation(number, operation) {
  478. // Parse the operation string to extract the operator and value
  479. const match = operation.match(/^([-+]?)(\d+(?:\.\d+)?)(%)?$/);
  480.  
  481. if (!match) {
  482. throw new Error('Invalid operation string');
  483. }
  484.  
  485. const [, operator, operand, isPercentage] = match;
  486. const operandValue = parseFloat(operand);
  487.  
  488. // Check for percentage and convert if necessary
  489. const adjustedOperand = isPercentage ? (number * operandValue) / 100 : operandValue;
  490.  
  491. // Perform the operation based on the operator
  492. switch (operator) {
  493. case '':
  494. case '+':
  495. return number + adjustedOperand;
  496. case '-':
  497. return number - adjustedOperand;
  498. default:
  499. throw new Error('Invalid operator');
  500. }
  501. }
  502.  
  503. function setPriceDelta() {
  504. let userInput = prompt('Enter price delta formula (default: -1):');
  505. if (userInput !== null) {
  506. priceDeltaRaw = userInput;
  507. localStorage.setItem("silmaril-torn-bazaar-filler-price-delta", userInput);
  508. } else {
  509. console.error("[TornBazaarFiller] User cancelled the Price Delta input.");
  510. }
  511. }
  512.  
  513. function checkApiKey(checkExisting = true) {
  514. if (!checkExisting || apiKey === null || apiKey.length != 16){
  515. let userInput = prompt("Please enter a PUBLIC Api Key, it will be used to get current bazaar prices:");
  516. if (userInput !== null && userInput.length == 16) {
  517. apiKey = userInput;
  518. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", userInput);
  519. } else {
  520. console.error("[TornBazaarFiller] User cancelled the Api Key input.");
  521. }
  522. }
  523. }
  524. })();