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.

当前为 2024-02-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Torn Bazaar Filler
  3. // @namespace https://github.com/SOLiNARY
  4. // @version 0.9
  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. });
  146. observer.observe(observerTarget, observerConfig);
  147.  
  148. function insertFillAndWaveBtn(element, buttonLabels, pageType){
  149. const waveDiv = document.createElement('div');
  150. waveDiv.className = 'wave';
  151.  
  152. const outerSpanFill = document.createElement('span');
  153. outerSpanFill.className = 'btn-wrap torn-bazaar-fill-qty-price';
  154. const outerSpanClear = document.createElement('span');
  155. outerSpanClear.className = 'btn-wrap torn-bazaar-clear-qty-price';
  156.  
  157. const innerSpanFill = document.createElement('span');
  158. innerSpanFill.className = 'btn';
  159. const innerSpanClear = document.createElement('span');
  160. innerSpanClear.className = 'btn';
  161. innerSpanClear.style.display = 'none';
  162.  
  163. const inputElementFill = document.createElement('input');
  164. inputElementFill.type = 'button';
  165. inputElementFill.value = buttonLabels[0];
  166. inputElementFill.className = 'torn-btn';
  167. const inputElementClear = document.createElement('input');
  168. inputElementClear.type = 'button';
  169. inputElementClear.value = buttonLabels[1];
  170. inputElementClear.className = 'torn-btn';
  171.  
  172. innerSpanFill.appendChild(inputElementFill);
  173. innerSpanClear.appendChild(inputElementClear);
  174. outerSpanFill.appendChild(innerSpanFill);
  175. outerSpanClear.appendChild(innerSpanClear);
  176.  
  177. element.append(outerSpanFill, outerSpanClear, waveDiv);
  178.  
  179. switch(pageType) {
  180. case pages.AddItems:
  181. $(outerSpanFill).on("click", "input", function(event) {
  182. checkApiKey();
  183. this.parentNode.style.display = "none";
  184. fillQuantityAndPrice(this, pageType);
  185. event.stopPropagation();
  186. });
  187.  
  188. $(outerSpanClear).on("click", "input", function(event) {
  189. this.parentNode.style.display = "none";
  190. clearQuantityAndPrice(this);
  191. event.stopPropagation();
  192. });
  193. break;
  194. case pages.ManageItems:
  195. $(outerSpanFill).on("click", "input", function(event) {
  196. checkApiKey();
  197. // this.parentNode.style.display = "none";
  198. updatePrice(this);
  199. event.stopPropagation();
  200. });
  201.  
  202. // $(outerSpanClear).on("click", "input", function(event) {
  203. // this.parentNode.style.display = "none";
  204. // clearQuantity(this, pageType);
  205. // event.stopPropagation();
  206. // });
  207. break;
  208. }
  209.  
  210. }
  211.  
  212. function insertPercentageSpan(element){
  213. let moneyGroupDiv = element.querySelector("div.price div.input-money-group");
  214.  
  215. if (moneyGroupDiv.querySelector("span.overlay-percentage") === null) {
  216. const percentageSpan = document.createElement('span');
  217. percentageSpan.className = 'overlay-percentage overlay-percentage-add';
  218. moneyGroupDiv.appendChild(percentageSpan);
  219. }
  220.  
  221. return moneyGroupDiv.querySelector("span.overlay-percentage");
  222. }
  223.  
  224. function insertPercentageManageSpan(element){
  225. let moneyGroupDiv = element.querySelector("div.input-money-group");
  226.  
  227. if (moneyGroupDiv.querySelector("span.overlay-percentage") === null) {
  228. const percentageSpan = document.createElement('span');
  229. percentageSpan.className = 'overlay-percentage overlay-percentage-manage';
  230. moneyGroupDiv.appendChild(percentageSpan);
  231. }
  232.  
  233. return moneyGroupDiv.querySelector("span.overlay-percentage");
  234. }
  235.  
  236. function fillQuantityAndPrice(element, pageType){
  237. let amountDiv = element.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector("div.amount-main-wrap");
  238. let priceInputs = amountDiv.querySelectorAll("div.price div input");
  239. let keyupEvent = new Event("keyup", {bubbles: true});
  240. let inputEvent = new Event("input", {bubbles: true});
  241.  
  242. let image = element.parentElement.parentElement.parentElement.parentElement.querySelector("div.image-wrap img");
  243. let numberPattern = /\/(\d+)\//;
  244. let match = image.src.match(numberPattern);
  245. let extractedItemId = 0;
  246. if (match) {
  247. extractedItemId = parseInt(match[1], 10);
  248. } else {
  249. console.error("[TornBazaarFiller] ItemId not found!");
  250. }
  251.  
  252. let requestUrl = bazaarUrl
  253. .replace("{itemId}", extractedItemId)
  254. .replace("{apiKey}", apiKey);
  255.  
  256. let wave = element.parentElement.parentElement.parentElement.querySelector("div.wave");
  257. fetch(requestUrl)
  258. .then(response => response.json())
  259. .then(data => {
  260. if (data.error != null && data.error.code === 2){
  261. apiKey = null;
  262. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
  263. wave.style.backgroundColor = "red";
  264. wave.style.animationDuration = "5s";
  265. console.error("[TornBazaarFiller] Incorrect Api Key:", data);
  266. return;
  267. }
  268. let bazaarSlotOffset = priceDeltaRaw.indexOf('[') == -1 ? 0 : parseInt(priceDeltaRaw.substring(priceDeltaRaw.indexOf('[') + 1, priceDeltaRaw.indexOf(']')));
  269. let priceDeltaWithoutBazaarOffset = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['))
  270. let lowBallPrice = Math.round(performOperation(data.bazaar[Math.min(bazaarSlotOffset, data.bazaar.length - 1)].cost, priceDeltaWithoutBazaarOffset));
  271. let price3rd = data.bazaar[Math.min(2, data.bazaar.length - 1)].cost;
  272. let priceCoefficient = ((lowBallPrice / price3rd) * 100).toFixed(0);
  273.  
  274. let percentageOverlaySpan = insertPercentageSpan(amountDiv);
  275. if (priceCoefficient <= 95){
  276. percentageOverlaySpan.style.display = "block";
  277. if (priceCoefficient <= 50){
  278. percentageOverlaySpan.style.color = "red";
  279. wave.style.backgroundColor = "red";
  280. wave.style.animationDuration = "5s";
  281. } else if (priceCoefficient <= 75){
  282. percentageOverlaySpan.style.color = "yellow";
  283. wave.style.backgroundColor = "yellow";
  284. wave.style.animationDuration = "3s";
  285. } else {
  286. percentageOverlaySpan.style.color = "green";
  287. wave.style.backgroundColor = "green";
  288. }
  289. percentageOverlaySpan.innerText = priceCoefficient + "%";
  290. } else {
  291. percentageOverlaySpan.style.display = "none";
  292. wave.style.backgroundColor = "green";
  293. }
  294.  
  295. priceInputs[0].value = lowBallPrice;
  296. priceInputs[1].value = lowBallPrice;
  297. priceInputs[0].dispatchEvent(inputEvent);
  298. })
  299. .catch(error => {
  300. wave.style.backgroundColor = "red";
  301. wave.style.animationDuration = "5s";
  302. console.error("[TornBazaarFiller] Error fetching data:", error);
  303. })
  304. .finally(() => {
  305. element.parentNode.parentNode.parentNode.querySelector("span.btn-wrap.torn-bazaar-clear-qty-price span.btn").style.display = "inline-block";
  306. });
  307. wave.style.animation = 'none';
  308. wave.offsetHeight;
  309. wave.style.animation = null;
  310. wave.style.backgroundColor = "transparent";
  311. wave.style.animationDuration = "1s";
  312.  
  313. let isQuantityCheckbox = amountDiv.querySelector("div.amount.choice-container") !== null;
  314. if (isQuantityCheckbox){
  315. amountDiv.querySelector("div.amount.choice-container input").click();
  316. } else {
  317. let quantityInput = amountDiv.querySelector("div.amount input");
  318. quantityInput.value = getQuantity(element, pageType);
  319. quantityInput.dispatchEvent(keyupEvent);
  320. }
  321. }
  322.  
  323. function updatePrice(element){
  324. let moneyGroupDiv;
  325. let parentNode4 = element.parentNode.parentNode.parentNode.parentNode;
  326. if (isMobileView){
  327. if (parentNode4.querySelector(".menuActivators___STzEc button.iconContainer___H3dWv[aria-label=Manage] span.active___OTFsm") == null) {
  328. parentNode4.querySelector(".menuActivators___STzEc button.iconContainer___H3dWv[aria-label=Manage]").click();
  329. }
  330. moneyGroupDiv = parentNode4.parentNode.querySelector(".bottomMobileMenu___CCSjc .priceMobile___cpt8p");
  331. } else {
  332. moneyGroupDiv = element.parentNode.parentNode.parentNode.parentNode.querySelector("div.price___DoKP7");
  333. }
  334. let priceInputs = moneyGroupDiv.querySelectorAll("div.input-money-group input");
  335. let inputEvent = new Event("input", {bubbles: true});
  336.  
  337. let image = element.parentElement.parentElement.parentElement.parentElement.querySelector("div.imgContainer___tEZeE img");
  338. let extractedItemId = getItemIdFromImage(image);
  339.  
  340. let requestUrl = bazaarUrl
  341. .replace("{itemId}", extractedItemId)
  342. .replace("{apiKey}", apiKey);
  343.  
  344. let wave = element.parentElement.parentElement.parentElement.querySelector("div.wave");
  345. fetch(requestUrl)
  346. .then(response => response.json())
  347. .then(data => {
  348. if (data.error != null && data.error.code === 2){
  349. apiKey = null;
  350. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
  351. wave.style.backgroundColor = "red";
  352. wave.style.animationDuration = "5s";
  353. console.error("[TornBazaarFiller] Incorrect Api Key:", data);
  354. return;
  355. }
  356. let bazaarSlotOffset = priceDeltaRaw.indexOf('[') == -1 ? 0 : parseInt(priceDeltaRaw.substring(priceDeltaRaw.indexOf('[') + 1, priceDeltaRaw.indexOf(']')));
  357. let priceDeltaWithoutBazaarOffset = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['))
  358. let lowBallPrice = Math.round(performOperation(data.bazaar[Math.min(bazaarSlotOffset, data.bazaar.length - 1)].cost, priceDeltaWithoutBazaarOffset));;
  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):', priceDeltaRaw);
  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:", apiKey ?? '');
  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. })();