Torn Loadout Switcher

Adds customisable quick loadout change buttons on Items page.

安装此脚本
作者推荐脚本

您可能也喜欢Torn Bazaar Filler

安装此脚本
  1. // ==UserScript==
  2. // @name Torn Loadout Switcher
  3. // @namespace https://github.com/SOLiNARY
  4. // @version 0.6.1
  5. // @description Adds customisable quick loadout change buttons on Items page.
  6. // @author Ramin Quluzade, Silmaril [2665762]
  7. // @license MIT
  8. // @match https://www.torn.com/item.php*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
  10. // @grant unsafeWindow
  11. // @grant GM_addStyle
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15. (async function() {
  16. 'use strict';
  17.  
  18. // Change to 'false' to see only numbers, 'true' to see titles
  19. const showTitles = true;
  20.  
  21. const includeLogo = false;
  22. const rfcvArg = "rfcv=";
  23. const isTampermonkeyEnabled = typeof unsafeWindow !== 'undefined';
  24. const getEquippedItemsUrl = "/page.php?sid=itemsLoadouts&step=getEquippedItems";
  25. let rfcv = localStorage.getItem("silmaril-loadout-switcher-rfcv") ?? null;
  26. let rfcvUpdatedThisSession = false;
  27. let mutationFound = false;
  28. let panelAdded = false;
  29. let loadoutTitles = {};
  30.  
  31. const { fetch: originalFetch } = isTampermonkeyEnabled ? unsafeWindow : window;
  32.  
  33. const customFetch = async (...args) => {
  34. let [resource, config] = args;
  35. let response = await originalFetch(resource, config);
  36.  
  37. if (rfcvUpdatedThisSession && Object.keys(loadoutTitles).length > 0) {
  38. return response;
  39. }
  40.  
  41. let fetchUrl = response.url;
  42. if (!rfcvUpdatedThisSession){
  43. let rfcvIdx = fetchUrl.indexOf(rfcvArg);
  44. if (rfcvIdx >= 0){
  45. rfcv = fetchUrl.substr(rfcvIdx + rfcvArg.length);
  46. localStorage.setItem("silmaril-loadout-switcher-rfcv", rfcv);
  47. document.querySelectorAll("div.silmaril-torn-loadout-switcher-container button").forEach((button) => button.classList.remove("disabled"));
  48. rfcvUpdatedThisSession = true;
  49. }
  50. }
  51. if (Object.keys(loadoutTitles).length == 0){
  52. if (fetchUrl.indexOf(getEquippedItemsUrl) >= 0){
  53. const json = () => response.clone().json()
  54. .then((data) => {
  55. if (data.currentLoadouts != null){
  56. for (let key in data.currentLoadouts) {
  57. if (data.currentLoadouts.hasOwnProperty(key)) {
  58. loadoutTitles[key] = data.currentLoadouts[key].title;
  59. }
  60. }
  61. }
  62. return data
  63. })
  64.  
  65. response.json = json;
  66. response.text = async () =>JSON.stringify(await json());
  67. }
  68. }
  69.  
  70. return response;
  71. };
  72.  
  73. if (isTampermonkeyEnabled){
  74. unsafeWindow.fetch = customFetch;
  75. } else {
  76. window.fetch = customFetch;
  77. }
  78.  
  79. const styles = `
  80. div#loadoutsRoot p[class^=title___] {
  81. overflow-y: hidden;
  82. overflow-x: auto;
  83. }
  84.  
  85. div.silmaril-torn-loadout-switcher-container {
  86. display: inline-flex;
  87. align-items: center;
  88. margin-left: 5px;
  89. }
  90.  
  91. div.silmaril-torn-loadout-switcher-container a img {
  92. display: flex;
  93. height: 50px;
  94. flex-direction: row;
  95. align-content: stretch;
  96. justify-content: space-around;
  97. align-items: flex-start;
  98. }
  99.  
  100. .wave-animation {
  101. position: relative;
  102. overflow: hidden;
  103. }
  104.  
  105. .wave {
  106. pointer-events: none;
  107. position: absolute;
  108. width: 100%;
  109. height: 33px;
  110. background-color: transparent;
  111. opacity: 0;
  112. transform: translateX(-100%);
  113. animation: waveAnimation 3s cubic-bezier(0, 0, 0, 1);
  114. }
  115.  
  116. @media (max-width: 768px) {
  117. div[class^=main___] > div[class^=content___] {
  118. margin-top: 10px;
  119. }
  120. }
  121.  
  122. @keyframes waveAnimation {
  123. 0% {
  124. opacity: 1;
  125. transform: translateX(-100%);
  126. }
  127. 100% {
  128. opacity: 0;
  129. transform: translateX(100%);
  130. }
  131. }
  132. `;
  133.  
  134. if (isTampermonkeyEnabled){
  135. GM_addStyle(styles);
  136. } else {
  137. let style = document.createElement("style");
  138. style.type = "text/css";
  139. style.innerHTML = styles;
  140. while (document.head == null){
  141. await sleep(50);
  142. }
  143. document.head.appendChild(style);
  144. }
  145.  
  146. const setLoadoutUrl = "/page.php?sid=itemsLoadouts&step=changeLoadout&setID={loadoutId}&rfcv={rfcv}";
  147. let selectedLoadouts = localStorage.getItem("silmaril-loadout-switcher-selected-loadouts") ?? "1,2,3";
  148. let selectedLoadoutsArray = selectedLoadouts.split(',');
  149.  
  150. const observerTarget = document.querySelector("html");
  151. const observerConfig = { attributes: false, childList: true, characterData: false, subtree: true };
  152.  
  153. const observer = new MutationObserver(function(mutations) {
  154. mutations.forEach(function(mutationItem) {
  155. if (mutationFound || panelAdded){
  156. observer.disconnect();
  157. return;
  158. }
  159. let mutation = mutationItem.target;
  160. if (mutation.classList.contains("title___nIMRx")) {
  161. mutationFound = true;
  162. observer.disconnect();
  163. const buttonContainer = document.createElement('div');
  164. buttonContainer.className = 'silmaril-torn-loadout-switcher-container';
  165.  
  166. const waveDiv = document.createElement('div');
  167. waveDiv.className = 'wave';
  168.  
  169. buttonContainer.appendChild(waveDiv);
  170. addLoadoutAndSettingButtons(buttonContainer);
  171. addLogo(buttonContainer);
  172.  
  173. if (!panelAdded){
  174. mutation.appendChild(buttonContainer);
  175. panelAdded = true;
  176. }
  177. }
  178. });
  179. });
  180. observer.observe(observerTarget, observerConfig);
  181.  
  182. function addLogo(root){
  183. if (!includeLogo){ return; }
  184.  
  185. const logoLink = document.createElement('a');
  186. logoLink.href = '/factions.php?step=profile&ID=6731';
  187. logoLink.target = '_blank';
  188.  
  189. const logoImg = document.createElement('img');
  190. logoImg.src = 'data:image/octet-stream;base64,UklGRpAFAABXRUJQVlA4WAoAAAAYAAAAfwAAfwAAQUxQSKcAAAABDzD/ERHCbW3tbSIRS4/gUTwaabEMa7ijxCVRj/jpl8ldRP8nwP0nx5AZVNBUcoOt0n/gUxSauzmCS9EzCFK400tCxBEcFGm8toWBRa6N0lQwgyzNSithbdHVNmnG3DbrERDjfO3c2/PPcXd8p5mgNOpqm6i1VlmsINNm2tYo10ZRA+1o4aHQnBaMehFBgl4NjiAVWt/Ma1tlDHkNKmg+KCHTxlF/RQBWUDggrAQAADAcAJ0BKoAAgAAAAAAlAGusoL8Y/ADVCuD/gr+rX9E5wzUrsB+pv8axQL4P+Ff8r4AD9CP55+O3AO/Tb+8/47hAP0A/iHCAfwf+O+jr/u/9F8C36q/2b/K/AT/Jf5R8/+3LeJf6B9AHjP/Sa6n+kU4BhD3961gj+Yfp160fzJ50PnT/He4J/G/5v/kvzY7pvoAfqqmnzJ4zUG87Va0zYvL6xWb+7JdFy9GX+yIyVf2p1gR6w+kkZGwvG/OF94fetMEdGx53ednF7J1scS7sPxiI6FjzydvQ4SbMdJNDC25UR6pUmPfvYcGGAAD+/97c8Ag//yLWLqou+QyhvH5zTSyXLSY2RoaUp2Wjt8xqZm4N5FaR+PoyTh8YUJM7QpH93+r4XaBryGMi0aFNVS8/7GOU+dSycbi/sz5Tz/hZhLoDMNx/CX7Gl1xcBT/L7AYCgsYxT9XAoH2pL3/9LpgQdOa6W/+JgLgV0/RBVejhSRsTkCyXl2UJH1kf9oU8GNvWkuD+mL+eZJVBocEr/9xsahGwJHFih+U67i9H5P/36l/Abt2sESXCK5WPOQn2zCQ+eAsAi6pLg5h+Sp+Vi6Kv0zH6AVXsPzb4N163PmwCeixL8S+oaebL0JSSbaOc3dOEcFV/7/yQN7cJmkJctgKW7TpzG1fBZjf3Vi3pVBgjt3DoFZ9H88FJsbZk4UQiSAdtgTBGGV0Qbako3/EaCDDhv6o3UXIy9VuFJpks3LnVLff/0tEUn/3H2mYOHRxXSCQhYbMo/6XcT8kbHRS/6WcF7Kq+JZpLoSWYlNcCRTgaomPehL6YDVV+P1MOwoKgPwzco3hVXqPoYRPjuqk9X6OWxjGQ/V4klAsdjwBL0nwpioJIYFxr5+e7qKBrLoO04+2st6nrmTVd/VSqEd/zP00iULw2nyDaeBgxC3T3J0fRf+s8oMIlseJhnGdpJRCUiaAEfGmACQG4zR+fsIkLKph96SQGxccfrou8foWYYSsrv1T5yJnbNXFgiNlMluCNU6JKxG7/ZZQJ/6vT/jD9Q5lNeaeXt//Ex/0JZFyx393d7XxhcoRvVUFWv/Mcj/ev2VLCojBVMPAn6f8SVZjwO54toOLjZfFY/oZc6jJnIH6bolYAD3Gt/wm55ZRHg2AlRSuMGsfBp1oTL0XxAMT/3Uxv56jhPLlFjPHZPRsN07Mp8ahvKqGaRjqc1NPylgQ0anN36Tkazqhznoj8OoNi1e/6Giff5NGL4f+Mbre0tl/9pdTw+4koHvkch/gnreoLlBf5Nb1OCy4J6L+hRhD0XZA/Z0za7xczd72XRx0cNO8l6mGOZuH98/4Lo3RIM2FLUV//u4oIuiJKtPt9/MNLMhcygG1EfRV6RICfU6QY4SxI/z3WEe439xw5wcpR2Vvzyy9srHuubX2rB0LJbtd0TvVlEiAdF1BC1he3fyzfhkY33hY4IJJ/c7na5AxArRy9TcD7D8hQc4UhVSsX4hlsH73UQSrPwn6v5YJHsQ30huCmqGJ/jbGu8jb18VcEFGB7wu/XqQ3juNRxhXEGqI3FH5GBeWSCinT3+epuVSD1dsxb9TvZq/IoGv6niKAAAAAARVhJRg4AAABNTQAqAAAACAAAAAAAAA==';
  191. logoImg.alt = 'Next Level logo';
  192.  
  193. logoLink.appendChild(logoImg);
  194. root.appendChild(logoLink);
  195. }
  196.  
  197. function addLoadoutAndSettingButtons(root){
  198. addLoadoutButtons(root);
  199.  
  200. const settings = document.createElement('button');
  201. settings.type = 'button';
  202. settings.title = 'Settings';
  203. settings.className = 'torn-btn';
  204. settings.textContent = '⚙';
  205. settings.addEventListener('click', () => {
  206. let userInput = prompt("Please, enter which loadouts from 1 to 9 you want to see, comma-separated (default: 1,2,3):", selectedLoadouts);
  207. let wave = root.querySelector("div.wave");
  208. if (userInput !== null && userInput.length > 0) {
  209. localStorage.setItem("silmaril-loadout-switcher-selected-loadouts", userInput);
  210. selectedLoadouts = userInput;
  211. selectedLoadoutsArray = selectedLoadouts.split(',');
  212. root.querySelectorAll("button, a").forEach((item) => item.remove());
  213. addLoadoutAndSettingButtons(root);
  214. addLogo(root);
  215. wave.style.backgroundColor = "green";
  216. } else {
  217. wave.style.animationDuration = "3s";
  218. wave.style.backgroundColor = "yellow";
  219. console.error("[TornLoadoutSwitcher] User cancelled input of selected loadouts.");
  220. }
  221. wave.style.animation = 'none';
  222. wave.offsetHeight;
  223. wave.style.animation = null;
  224. });
  225.  
  226. root.appendChild(settings);
  227. }
  228.  
  229. async function addLoadoutButtons(root){
  230. selectedLoadoutsArray.forEach((loadout) => {
  231. const button = document.createElement('button');
  232. button.type = 'button';
  233. button.title = showTitles ? loadout : loadoutTitles[loadout] ?? '';
  234. button.className = rfcv === null ? 'torn-btn disabled' : 'torn-btn';
  235. button.textContent = showTitles ? loadoutTitles[loadout] : loadout;
  236. button.setAttribute('data-loadout-number', loadout);
  237. button.addEventListener('click', () => {handleLoadoutClick(root)});
  238.  
  239. root.appendChild(button);
  240. })
  241. }
  242.  
  243. async function handleLoadoutClick(root){
  244. let loadout = event.target.getAttribute('data-loadout-number');
  245. if (event.target.classList.contains('disabled')){
  246. return;
  247. }
  248. let url = setLoadoutUrl.replace("{loadoutId}", loadout).replace("{rfcv}", rfcv);
  249. await sendSetLoadoutRequest(url, root);
  250. }
  251.  
  252. async function sendSetLoadoutRequest(url, root){
  253. let wave = root.querySelector("div.wave");
  254. await fetch(url, {
  255. method: 'GET',
  256. })
  257. .then(response => {
  258. if (response.ok) {
  259. wave.style.backgroundColor = "green";
  260. } else {
  261. console.error("[TornLoadoutSwitcher] Set Loadout request failed:", response);
  262. wave.style.backgroundColor = "red";
  263. wave.style.animationDuration = "5s";
  264. }
  265. })
  266. .catch(error => {
  267. console.error("[TornLoadoutSwitcher] Error setting loadout:", error);
  268. wave.style.backgroundColor = "red";
  269. wave.style.animationDuration = "5s";
  270. });
  271. wave.style.animation = 'none';
  272. wave.offsetHeight;
  273. wave.style.animation = null;
  274. }
  275.  
  276. function sleep(ms) {
  277. return new Promise(resolve => setTimeout(resolve, ms));
  278. }
  279. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址