Torn Bazaar Filler

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

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

  1. // ==UserScript==
  2. // @name Torn Bazaar Filler
  3. // @namespace https://github.com/SOLiNARY
  4. // @version 0.5
  5. // @description On "Fill" click autofills bazaar item price with lowest bazaar price currently minus $1, 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. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const bazaarUrl = "https://api.torn.com/market/{itemId}?selections=bazaar&key={apiKey}";
  19. let apiKey = localStorage.getItem("silmaril-torn-bazaar-filler-apikey");
  20.  
  21. // TornPDA support for GM_addStyle
  22. let GM_addStyle = function (s) {
  23. let style = document.createElement("style");
  24. style.type = "text/css";
  25. style.innerHTML = s;
  26. document.head.appendChild(style);
  27. };
  28.  
  29. GM_addStyle(`
  30. .btn-wrap.torn-bazaar-fill-qty-price {
  31. float: right;
  32. margin-left: auto;
  33. }
  34.  
  35. div.title-wrap div.name-wrap {
  36. display: flex;
  37. justify-content: flex-end;
  38. }
  39.  
  40. .wave-animation {
  41. position: relative;
  42. overflow: hidden;
  43. }
  44.  
  45. .wave {
  46. position: absolute;
  47. width: 100%;
  48. height: 33px;
  49. background-color: transparent;
  50. opacity: 0;
  51. transform: translateX(-100%);
  52. animation: waveAnimation 1s cubic-bezier(0, 0, 0, 1);
  53. }
  54.  
  55. @keyframes waveAnimation {
  56. 0% {
  57. opacity: 1;
  58. transform: translateX(-100%);
  59. }
  60. 100% {
  61. opacity: 0;
  62. transform: translateX(100%);
  63. }
  64. }
  65.  
  66. .overlay-percentage {
  67. position: absolute;
  68. top: 0px;
  69. right: -30px;
  70. background-color: rgba(0, 0, 0, 0.9);
  71. padding: 0px 5px;
  72. border-radius: 15px;
  73. font-size: 10px;
  74. }
  75. `);
  76.  
  77. var observerTarget = $(".content-wrapper")[0];
  78. var observerConfig = { attributes: false, childList: true, characterData: false, subtree: true };
  79.  
  80. var observer = new MutationObserver(function(mutations) {
  81. $("ul.ui-tabs-nav").on("click", "li:not(.ui-state-active):not(.ui-state-disabled):not(.m-show)", function() {
  82. observer.observe(observerTarget, observerConfig);
  83. });
  84. $("div.topSection___U7sVi").on("click", "div.linksContainer___LiOTN a[aria-labelledby=add-items]", function(){
  85. observer.observe(observerTarget, observerConfig);
  86. });
  87. $("div.topSection___U7sVi").on("click", "div.listItem___Q3FFU a[aria-labelledby=add-items]", function(){
  88. observer.observe(observerTarget, observerConfig);
  89. });
  90.  
  91. let containerItems = $("ul.items-cont li.clearfix");
  92. containerItems.find("div.title-wrap div.name-wrap").each(function(){
  93. let isParentRowDisabled = this.parentElement.parentElement.classList.contains("disabled");
  94. let alreadyHasFillBtn = this.querySelector(".btn-wrap.torn-bazaar-fill-qty-price") != null;
  95. if (!alreadyHasFillBtn && !isParentRowDisabled){
  96. insertFillAndWaveBtn(this);
  97. }
  98. });
  99.  
  100. if($("div.amount").length) {
  101. observer.disconnect();
  102. }
  103. });
  104. observer.observe(observerTarget, observerConfig);
  105.  
  106. function insertFillAndWaveBtn(element){
  107. const waveDiv = document.createElement('div');
  108. waveDiv.className = 'wave';
  109.  
  110. const outerSpan = document.createElement('span');
  111. outerSpan.className = 'btn-wrap torn-bazaar-fill-qty-price';
  112.  
  113. const innerSpan = document.createElement('span');
  114. innerSpan.className = 'btn';
  115.  
  116. const inputElement = document.createElement('input');
  117. inputElement.type = 'submit';
  118. inputElement.value = 'Fill';
  119. inputElement.className = 'torn-btn';
  120.  
  121. innerSpan.appendChild(inputElement);
  122. outerSpan.appendChild(innerSpan);
  123.  
  124. element.append(outerSpan, waveDiv);
  125.  
  126. $(outerSpan).on("click", "input", function() {
  127. checkApiKey();
  128. fillQuantityAndPrice(this);
  129. event.stopPropagation();
  130. });
  131. }
  132.  
  133. function insertPercentageSpan(element){
  134. let moneyGroupDiv = element.parentElement.parentElement.parentElement.parentElement.parentElement
  135. .querySelector("div.actions-main-wrap div.amount-main-wrap div.price div.input-money-group");
  136.  
  137. if (moneyGroupDiv.querySelector("span.overlay-percentage") === null) {
  138. const percentageSpan = document.createElement('span');
  139. percentageSpan.className = 'overlay-percentage';
  140. moneyGroupDiv.appendChild(percentageSpan);
  141. }
  142.  
  143. return moneyGroupDiv.querySelector("span.overlay-percentage");
  144. }
  145.  
  146. function getQuantity(element){
  147. let rgx = /x(\d+)$/;
  148. let quantityText = $(element).parent().parent().prev().text();
  149. let match = rgx.exec(quantityText);
  150. let quantity = match === null ? 1 : match[1];
  151. return quantity;
  152. }
  153.  
  154. function fillQuantityAndPrice(element){
  155. let amountDiv = element.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector("div.amount-main-wrap");
  156. let priceInputs = amountDiv.querySelectorAll("div.price div input");
  157. let keyupEvent = new Event("keyup", {bubbles: true});
  158. let inputEvent = new Event("input", {bubbles: true});
  159.  
  160. let image = element.parentElement.parentElement.parentElement.parentElement.querySelector("div.image-wrap img");
  161. let numberPattern = /\/(\d+)\//;
  162. let match = image.src.match(numberPattern);
  163. let extractedItemId = 0;
  164. if (match) {
  165. extractedItemId = parseInt(match[1], 10);
  166. } else {
  167. console.error("[TornBazaarFiller] ItemId not found!");
  168. }
  169.  
  170. let requestUrl = bazaarUrl
  171. .replace("{itemId}", extractedItemId)
  172. .replace("{apiKey}", apiKey);
  173.  
  174. let wave = element.parentElement.parentElement.parentElement.parentElement.parentElement.querySelector("div.wave");
  175. fetch(requestUrl)
  176. .then(response => response.json())
  177. .then(data => {
  178. if (data.error != null && data.error.code === 2){
  179. apiKey = null;
  180. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
  181. wave.style.backgroundColor = "red";
  182. wave.style.animationDuration = "5s";
  183. console.error("[TornBazaarFiller] Incorrect Api Key:", data);
  184. return;
  185. }
  186. let lowBallPrice = data.bazaar[0].cost - 1;
  187. let price3rd = data.bazaar[Math.min(2, data.bazaar.length - 1)].cost;
  188. let priceCoefficient = ((data.bazaar[0].cost / price3rd) * 100).toFixed(0);
  189.  
  190. let percentageOverlaySpan = insertPercentageSpan(element);
  191. if (priceCoefficient <= 95){
  192. percentageOverlaySpan.style.display = "block";
  193. if (priceCoefficient <= 50){
  194. percentageOverlaySpan.style.color = "red";
  195. wave.style.backgroundColor = "red";
  196. wave.style.animationDuration = "5s";
  197. } else if (priceCoefficient <= 75){
  198. percentageOverlaySpan.style.color = "yellow";
  199. wave.style.backgroundColor = "yellow";
  200. wave.style.animationDuration = "3s";
  201. } else {
  202. percentageOverlaySpan.style.color = "green";
  203. wave.style.backgroundColor = "green";
  204. }
  205. percentageOverlaySpan.innerText = priceCoefficient + "%";
  206. } else {
  207. percentageOverlaySpan.style.display = "none";
  208. wave.style.backgroundColor = "green";
  209. }
  210.  
  211. priceInputs[0].value = lowBallPrice;
  212. priceInputs[1].value = lowBallPrice;
  213. priceInputs[0].dispatchEvent(inputEvent);
  214. })
  215. .catch(error => {
  216. wave.style.backgroundColor = "red";
  217. wave.style.animationDuration = "5s";
  218. console.error("[TornBazaarFiller] Error fetching data:", error);
  219. });
  220. wave.style.animation = 'none';
  221. wave.offsetHeight;
  222. wave.style.animation = null;
  223. wave.style.backgroundColor = "transparent";
  224. wave.style.animationDuration = "1s";
  225.  
  226. let isQuantityCheckbox = amountDiv.querySelector("div.amount.choice-container") !== null;
  227. if (isQuantityCheckbox){
  228. amountDiv.querySelector("div.amount.choice-container input").click();
  229. } else {
  230. let quantityInput = amountDiv.querySelector("div.amount input");
  231. quantityInput.value = getQuantity(element);
  232. quantityInput.dispatchEvent(keyupEvent);
  233. }
  234. }
  235.  
  236. function checkApiKey(){
  237. if (apiKey === null || apiKey.length != 16){
  238. let userInput = prompt("Please enter a PUBLIC Api Key, it will be used to get current bazaar prices:");
  239. if (userInput !== null && userInput.length == 16) {
  240. apiKey = userInput;
  241. localStorage.setItem("silmaril-torn-bazaar-filler-apikey", userInput);
  242. } else {
  243. console.error("[TornBazaarFiller] User cancelled the Api Key input.");
  244. }
  245. }
  246. }
  247. })();