Predicts foreign stock depletion/restock before landing using YATA travel data.
当前为
// ==UserScript==
// @name Unified Stock (Smart Predictive)
// @namespace https://yata.alwaysdata.net/
// @version 3.0
// @description Predicts foreign stock depletion/restock before landing using YATA travel data.
// @author You
// @match https://www.torn.com/*
// @grant GM_xmlhttpRequest
// @connect yata.alwaysdata.net
// ==/UserScript==
(function () {
'use strict';
const API_BASE = 'https://yata.alwaysdata.net/api/v1';
const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
let unifiedStock = {};
let travelTimes = {};
// ----------- HELPERS -----------
const sleep = ms => new Promise(res => setTimeout(res, ms));
async function fetchJSON(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: res => {
try {
resolve(JSON.parse(res.responseText));
} catch (e) {
reject(e);
}
},
onerror: err => reject(err)
});
});
}
function timeNow() {
return new Date().toLocaleTimeString();
}
function getFlightTime(countryCode) {
return travelTimes[countryCode.toLowerCase()]?.duration || 45 * 60 * 1000; // fallback 45 min
}
function predictColor(item, countryCode) {
if (!item || !countryCode) return 'grey';
const now = Date.now();
const flightMs = getFlightTime(countryCode);
const arrival = now + flightMs;
const stock = item.stock;
const deplete = new Date(item.depletion).getTime();
const restock = new Date(item.restock).getTime();
// 🟢 Still available when arriving
if (stock > 0 && (isNaN(deplete) || deplete > arrival)) return 'green';
// 🔴 Will deplete before you land
if (stock > 0 && deplete <= arrival) return 'red';
// 🟠 Restocking before arrival
if (stock === 0 && restock <= arrival) return 'orange';
// 🔴 Otherwise empty
return 'red';
}
// ----------- MAIN UI -----------
function createPanel() {
const panel = document.createElement('div');
panel.id = 'unifiedStockPanel';
panel.style = `
position: fixed;
top: 70px;
left: 10px;
background: rgba(20,20,20,0.95);
color: #fff;
font-size: 12px;
padding: 8px 10px;
border-radius: 10px;
z-index: 9999;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 0 8px rgba(0,0,0,0.5);
`;
panel.innerHTML = `
<div style="font-weight:bold; margin-bottom:5px;">🌍 Unified Stock</div>
<div id="usBody">Loading...</div>
<button id="usRefresh" style="margin-top:5px;width:100%;">Refresh</button>
<div style="margin-top:5px;font-size:10px;color:#ccc;" id="usTime"></div>
`;
document.body.appendChild(panel);
document.getElementById('usRefresh').addEventListener('click', updateStock);
}
async function updateStock() {
const body = document.getElementById('usBody');
const time = document.getElementById('usTime');
if (!body) return;
body.textContent = 'Updating...';
try {
unifiedStock = await fetchJSON(`${API_BASE}/foreignstock/`);
travelTimes = await fetchJSON(`${API_BASE}/travel/`);
} catch (e) {
body.textContent = '⚠️ Failed to fetch YATA data.';
return;
}
let html = '';
Object.entries(unifiedStock).forEach(([country, items]) => {
html += `<div style="margin:3px 0;font-weight:bold;">${country.toUpperCase()}</div>`;
items.forEach(item => {
const color = predictColor(item, country);
html += `
<div style="display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid #333;">
<span style="color:${color};">●</span>
<span>${item.name}</span>
<span style="color:#999;">${item.stock}</span>
</div>`;
});
});
body.innerHTML = html;
time.textContent = `Updated: ${timeNow()}`;
}
// ----------- INIT -----------
async function init() {
createPanel();
await updateStock();
setInterval(updateStock, REFRESH_INTERVAL);
}
// Wait for Torn DOM
const checkInterval = setInterval(() => {
if (document.readyState === 'complete') {
clearInterval(checkInterval);
init();
}
}, 1000);
})();