Single-jump scroll + export all caches including Armor; quick-jump button on main log page
// ==UserScript==
// @name Torn Cache Log Exporter
// @namespace torn.cache.export
// @version 9.4
// @description Single-jump scroll + export all caches including Armor; quick-jump button on main log page
// @match https://www.torn.com/page.php?sid=log*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const ANALYZER_URL = "https://torn-two.vercel.app/";
const params = new URLSearchParams(window.location.search);
// ── On any log page that is NOT the cache log, show a quick-jump button ──
function addNavButton() {
if (document.getElementById("cacheNavBtn")) return;
const btn = document.createElement("button");
btn.id = "cacheNavBtn";
btn.innerText = "\uD83D\uDCE6 Go to Cache Logs";
btn.style.position = "fixed";
btn.style.top = "100px";
btn.style.right = "20px";
btn.style.zIndex = "9999";
btn.style.padding = "10px 16px";
btn.style.background = "#7c3aed";
btn.style.color = "white";
btn.style.border = "none";
btn.style.borderRadius = "8px";
btn.style.cursor = "pointer";
btn.style.fontWeight = "bold";
btn.style.fontSize = "13px";
btn.style.boxShadow = "0 2px 8px rgba(0,0,0,0.35)";
btn.style.transition = "background 0.15s";
btn.onmouseenter = () => { btn.style.background = "#6d28d9"; };
btn.onmouseleave = () => { btn.style.background = "#7c3aed"; };
btn.onclick = () => { window.location.href = "https://www.torn.com/page.php?sid=log&log=2615"; };
document.body.appendChild(btn);
}
function safeInit(fn) {
if (document.readyState === "complete") {
setTimeout(fn, 1000);
} else {
window.addEventListener("load", () => setTimeout(fn, 1000));
}
}
if (params.get("log") !== "2615") {
safeInit(addNavButton);
return;
}
// ── Cache log page (log=2615) ─────────────────────────────────────────────
function showNotification(message) {
const existing = document.getElementById("tornExporterToast");
if (existing) existing.remove();
const toast = document.createElement("div");
toast.id = "tornExporterToast";
toast.innerText = message;
Object.assign(toast.style, {
position: "fixed",
bottom: "30px",
left: "50%",
transform: "translateX(-50%)",
background: "#1e293b",
color: "white",
padding: "12px 20px",
borderRadius: "8px",
zIndex: "99999",
fontWeight: "bold",
fontSize: "14px",
boxShadow: "0 2px 10px rgba(0,0,0,0.4)",
opacity: "1",
transition: "opacity 0.5s ease",
});
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = "0";
setTimeout(() => toast.remove(), 500);
}, 4000);
}
function addUI() {
if (document.getElementById("cacheExportContainer")) return;
const container = document.createElement("div");
container.id = "cacheExportContainer";
container.style.position = "fixed";
container.style.top = "100px";
container.style.right = "20px";
container.style.zIndex = "9999";
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "8px";
// ── One click = one jump. No loop, no automation. ─────────────────────
const jumpBtn = createButton("⬇️ Jump to Bottom", "#2563eb", jumpToBottom);
const exportBtn = createButton("💾 Export Logs", "#16a34a", exportLogs);
container.appendChild(jumpBtn);
container.appendChild(exportBtn);
document.body.appendChild(container);
}
function createButton(text, color, onClick) {
const btn = document.createElement("button");
btn.innerText = text;
btn.style.padding = "8px 12px";
btn.style.background = color;
btn.style.color = "white";
btn.style.border = "none";
btn.style.borderRadius = "6px";
btn.style.cursor = "pointer";
btn.style.fontWeight = "bold";
btn.onclick = onClick;
return btn;
}
// ── Single scroll to bottom — equivalent to pressing the End key ──────────
function jumpToBottom() {
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
showNotification("⬇️ Jumped to bottom. Scroll back up manually to load more entries, then export.");
}
function parseCacheType(text) {
if (text.includes("Armor Cache")) return "Armor";
if (text.includes("Melee Cache")) return "Melee";
if (text.includes("Small Arms Cache")) return "Small Arms";
if (text.includes("Medium Arms Cache")) return "Medium Arms";
if (text.includes("Heavy Arms Cache")) return "Heavy Arms";
return null;
}
function detectRarity(el) {
const cls = el.className.toLowerCase();
if (cls.includes("text-red")) return "Red";
if (cls.includes("text-orange")) return "Orange";
if (cls.includes("text-yellow")) return "Yellow";
return "Unknown";
}
function extractTimestamp(entry) {
const parent = entry.closest("li, tr, div");
if (!parent) return null;
const match = parent.innerText.match(/\d{2}:\d{2}:\d{2}\s-\s\d{2}\/\d{2}\/\d{2}/);
return match ? match[0] : null;
}
function exportLogs() {
const logEntries = document.querySelectorAll("span.log-text");
const results = [];
logEntries.forEach(entry => {
const text = entry.innerText;
if (!text.includes("Cache") || !text.includes("gained")) return;
const cacheType = parseCacheType(text);
if (!cacheType) return;
const colored = entry.querySelector(".text-yellow, .text-orange, .text-red");
if (!colored) return;
const rarity = detectRarity(colored);
const timestamp = extractTimestamp(entry);
const fullText = colored.innerText.trim();
const bonusMatch = fullText.match(/\((.*?)\)/);
const bonus = bonusMatch ? bonusMatch[1] : null;
const itemName = bonusMatch ? fullText.replace(/\(.*?\)/, "").trim() : fullText;
results.push({
timestamp,
cacheType,
weaponName: itemName,
bonus,
doubleBonus: cacheType === "Armor" ? false : (bonus && bonus.includes("&")) || false,
rarity
});
});
downloadJSON(results);
}
function downloadJSON(data) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "torn_cache_logs_full.json";
a.click();
URL.revokeObjectURL(url);
showNotification(`💾 Exported ${data.length} entries! Opening analyzer...`);
setTimeout(() => window.open(ANALYZER_URL, "_blank"), 1200);
}
safeInit(addUI);
})();