// ==UserScript== // @name Torn - Arrival Time Predictor // @namespace http://tampermonkey.net/ // @version 1.03 // @description Calculates and displays the exact local time you will land while traveling // @author You // @match https://www.torn.com/page.php?sid=travel // @grant GM_addStyle // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; // Setup CSS rule for the pulsing text. GM_addStyle(` @keyframes colorPulse { 0% { color: #ff3333; } /* Start Red */ 50% { color: #ffffff; } /* Midpoint Orange */ 100% { color: #ff3333; } /* End Red */ } .pulse-text { animation: colorPulse 1s infinite ease-in-out; font-size: 2rem; } `); // Setup Formatter const currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }); //const flight_image = document.querySelector("#travel-root > div.viewport___cdEK9 > div.airspaceScene___euCKY.outboundFlight___rIvIA"); //flight_image.remove(); // 1. Wait for Torn's travel container to load const checkFlight = setInterval(() => { // Torn uses countdown timers inside specific structural classes depending on the layout type const timerElement = document.querySelector("#travel-root > div.viewport___cdEK9 > div.flightProgressSection___OEJoR > div.progressText___BEQ0k > span > span:nth-child(2) > time"); if (timerElement) { clearInterval(checkFlight); initClockExtension(timerElement); } }, 500); function initClockExtension(timerElement) { // Create a custom UI box to display our text safely without breaking Torn's CSS const travelRoot = document.querySelector("#travel-root"); const parentContainer = travelRoot.querySelector( '[class^="progressText"]', ); // Remove travel fact travelRoot.querySelector('[class^="factWrapper"]').remove(); // Remove flight scene travelRoot.querySelector('[class^="airspaceScene"]').remove(); const ogTimer = document.querySelector( "#travel-root > div.viewport___cdEK9 > div.flightProgressSection___OEJoR > div.progressText___BEQ0k > div > span", ); var timerContainer = null; const timeDisplay = document.createElement("div"); timeDisplay.style.cssText = "padding: 10px; margin-top: 10px; color: #fff; text-align: center; font-weight: bold; transition: color 0.5s ease; font-weight: bold; font-size: 1.5rem;"; //timerElement.parentNode.insertBefore(timeDisplay, timerElement.nextSibling); parentContainer.insertAdjacentElement("beforebegin", timeDisplay); timeDisplay.innerText = "Calculating landing time..."; timerContainer = timeDisplay; // Enumerate Inventory items var travelInventory = travelRoot.querySelectorAll('[class^="itemButton"]'); var iteminfo = []; console.log(`${travelInventory.length} items found.`); // TODO: DEBUG: Remove. if (travelInventory.length > 0) { travelInventory.forEach((item) => { let itemImg = item.querySelector("img"); // Use the image URL to get the item ID if (itemImg) { let splitUrl = itemImg.src.split("/"); // Split the URL by the forward slashes const id = splitUrl[5]; // The 6th element is the Item ID var itemIndex = iteminfo.findIndex(item => item.id === id); if (itemIndex === -1) { itemIndex = iteminfo.push( { id: id, qty: 0, item_name: "", market: 0, bazaar_min: 0, bazaar_average: 0}) - 1; } iteminfo[itemIndex].qty++; } }); iteminfo.forEach(item => { getPriceInfo(item.id); }); } // Function to update the page with price info function updatePriceInfo() { var totalValue = 0; var values = { market: 0, bazaar_average: 0, bazaar_min: 0 }; const invPanel = travelRoot.querySelector('[class^="inventoryPanel"]'); const appraisePanel = document.createElement("div"); appraisePanel.style.cssText = "color: #0c0; font-weight: bold; font-size: 1.2rem;" invPanel.parentNode.insertBefore(appraisePanel, invPanel.nextSibling); appraisePanel.innerText = "Appraising items..."; iteminfo.forEach(item => { console.log(`Market Value of ${item.qty} ${item.item_name}: ${item.market * item.qty}. Bazaar Avg: ${item.bazaar_average * item.qty}. Bazaar Min: ${item.bazaar_min * item.qty}`); values.bazaar_min = currencyFormatter.format(item.bazaar_min * item.qty); values.bazaar_average = currencyFormatter.format(item.bazaar_average * item.qty); values.market = currencyFormatter.format(item.market * item.qty); }); // TODO: Update UI Elements appraisePanel.innerText = `Quick Sell: ${values.bazaar_min} | Market: ${values.market} | Bazaar Avg: ${values.bazaar_average}`; } // Function to get price information function getPriceInfo(itemId) { const w3bPriceUrl = "https://weav3r.dev/api/marketplace/" + itemId; // API Endpoint for W3B Market Information var data = null; GM_xmlhttpRequest({ method: "GET", url: w3bPriceUrl, headers: { "Content-Type": "application/json" }, onload: function(response) { if (response.status === 200) { data = JSON.parse(response.responseText); // Start updating the item's information. let index = iteminfo.findIndex(item => item.id === itemId); iteminfo[index].item_name = data.item_name; iteminfo[index].market = data.market_price; iteminfo[index].bazaar_min = data.listings[0].price; iteminfo[index].bazaar_average = data.bazaar_average; updatePriceInfo(); // Update the UI } } }); } // Update the landing time prediction every single second setInterval(() => { const timeString = timerElement.innerText; // Grabs 'HH:MM:SS' from the page if (!timeString) return; const timeParts = timeString.split(":"); if (timeParts.length === 3) { const hours = parseInt(timeParts[0], 10); const minutes = parseInt(timeParts[1], 10); const seconds = parseInt(timeParts[2], 10); // Convert countdown duration entirely to total milliseconds const msRemaining = (hours * 60 * 60 + minutes * 60 + seconds) * 1000; // Blink the text if time left is < 2 minutes. if (msRemaining <= 120000) { // Add the blinking class to the text. timerContainer.classList.add("pulse-text"); } // Add the flight duration to the current system date/time const landingTime = new Date(Date.now() + msRemaining); // Format the output neatly (e.g., "16:24:05") timerContainer.innerText = `Estimated Landing: ${landingTime.toLocaleTimeString()}`; } }, 1000); } })();