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