Floating tracker with Nerve planner and Native UI coloring (Shades of Green & Blue Leader mode).
// ==UserScript==
// @name Torn PDA - Graffiti Rep Tracker v4.2
// @namespace Torn.Crimes2.Graffiti.Tracker
// @version 4.2
// @description Floating tracker with Nerve planner and Native UI coloring (Shades of Green & Blue Leader mode).
// @author CDR.Shepard
// @license MIT
// @match https://www.torn.com/loader.php?sid=crimes*
// @match https://www.torn.com/page.php?sid=crimes*
// @grant none
// @run-at document-end
// ==/UserScript==
(() => {
"use strict";
if (document.getElementById("gt-gui")) return;
const STORAGE_KEY = "torn_graffiti_rep_data";
const MULTIPLIER_KEY = "torn_graffiti_multiplier";
const INTERVAL_KEY = "torn_graffiti_interval";
let isGuiClosed = false;
let isMinimized = false;
let savedMultiplier = localStorage.getItem(MULTIPLIER_KEY) || "1.0";
let savedInterval = localStorage.getItem(INTERVAL_KEY) || "0";
const style = document.createElement("style");
style.textContent = `
#gt-gui {
position: fixed; bottom: 70px; right: 10px; width: 240px;
border: 1px solid #333; border-radius: 8px; color: #ddd;
font-family: Arial, sans-serif; font-size: 11px; z-index: 999999;
box-shadow: 0px 4px 15px rgba(0,0,0,0.8); display: none;
will-change: transform; background: rgba(15, 15, 15, 0.95);
}
#gt-header {
background: #1a1a1a; padding: 8px 10px; border-bottom: 1px solid #333;
border-radius: 8px 8px 0 0; display: flex; justify-content: space-between;
align-items: center; cursor: grab; touch-action: none;
}
#gt-header:active { cursor: grabbing; }
.gt-title { font-weight: bold; color: #e0ac16; font-size: 12px; pointer-events: none; }
.gt-controls { display: flex; gap: 4px; }
.gt-controls button {
background: #333; color: #fff; border: none; width: 22px; height: 22px;
border-radius: 4px; cursor: pointer; font-weight: bold; line-height: 1;
}
.gt-controls button:active { background: #555; }
#gt-body { padding: 10px; border-radius: 0 0 8px 8px; }
.gt-settings-row {
display: flex; justify-content: space-between; align-items: center; gap: 5px;
background: #1a1a1a; padding: 4px 8px; border-radius: 4px; border: 1px solid #2a2a2a;
margin-bottom: 8px;
}
.gt-settings-row input {
width: 40px; background: #000; color: #44ff44; border: 1px solid #444;
border-radius: 3px; text-align: center; font-size: 11px; padding: 2px;
}
.gt-timestamp { font-size: 10px; color: #aaa; text-align: center; margin-bottom: 8px; }
.gt-status { font-size: 11px; text-align: center; margin-bottom: 8px; font-weight: bold; height: 12px; }
.gt-grid-header {
display: flex; justify-content: space-between; padding-bottom: 4px;
border-bottom: 1px solid #444; font-weight: bold; color: #fff; margin-bottom: 4px;
}
.gt-col-1 { flex: 2; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.gt-col-2 { flex: 1.5; text-align: right; display: flex; justify-content: flex-end; align-items: center; }
.gt-col-3 { flex: 1; text-align: right; }
.gt-data-container {
max-height: 150px; overflow-y: auto; margin-bottom: 10px;
scrollbar-width: thin; scrollbar-color: #555 #222;
}
.gt-area-row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #2a2a2a; }
.gt-locked { color: #ff4444; font-style: italic; }
.gt-rep { color: #44ff44; font-weight: bold; }
.gt-delta-pos { color: #44ff44; font-size: 9px; margin-left: 4px; font-weight: normal; }
.gt-delta-neg { color: #ff4444; font-size: 9px; margin-left: 4px; font-weight: normal; }
.gt-delta-zero { color: #ffff44; font-size: 9px; margin-left: 4px; font-weight: normal; }
.gt-nerve-est { color: #ff9900; }
.gt-nerve-done { color: #aaa; }
.gt-btn-fetch {
background: #1e4d1e; color: #fff; border: 1px solid #2e7d2e; padding: 8px;
border-radius: 4px; cursor: pointer; width: 100%; font-weight: bold; font-size: 11px;
}
.gt-btn-fetch:active { background: #2e7d2e; }
`;
document.head.appendChild(style);
const gui = document.createElement("div");
gui.id = "gt-gui";
gui.innerHTML = `
<div id="gt-header">
<span class="gt-title">Graffiti Rep v4.2</span>
<div class="gt-controls">
<button id="gt-min-btn">-</button>
<button id="gt-close-btn">X</button>
</div>
</div>
<div id="gt-body">
<div class="gt-settings-row">
<div><span style="color: #aaa;">Avg Rep: </span><input type="number" id="gt-multiplier" value="${savedMultiplier}" step="0.1" min="0.1"></div>
<div><span style="color: #aaa;">Interval: </span><input type="number" id="gt-interval" value="${savedInterval}" step="1" min="0"></div>
</div>
<div class="gt-timestamp" id="gt-time">No data fetched yet</div>
<div class="gt-status" id="gt-status"></div>
<div class="gt-grid-header">
<span class="gt-col-1">Area</span>
<span class="gt-col-2">Rep (Δ)</span>
<span class="gt-col-3">Nerve</span>
</div>
<div class="gt-data-container" id="gt-list"></div>
<button class="gt-btn-fetch" id="gt-fetch-btn">FETCH DATA</button>
</div>
`;
document.body.appendChild(gui);
const timeDisplay = document.getElementById("gt-time");
const listDisplay = document.getElementById("gt-list");
const statusDisplay = document.getElementById("gt-status");
const multInput = document.getElementById("gt-multiplier");
const intInput = document.getElementById("gt-interval");
const header = document.getElementById("gt-header");
const body = document.getElementById("gt-body");
multInput.addEventListener("input", () => {
let val = parseFloat(multInput.value);
if (!isNaN(val) && val > 0) {
localStorage.setItem(MULTIPLIER_KEY, val.toString());
loadSavedData();
}
});
intInput.addEventListener("input", () => {
let val = parseInt(intInput.value, 10);
if (!isNaN(val) && val >= 0) {
localStorage.setItem(INTERVAL_KEY, val.toString());
applyNativeStyling();
}
});
function flashStatus(msg, isError) {
statusDisplay.innerText = msg;
statusDisplay.style.color = isError ? "#ff4444" : "#44ff44";
setTimeout(() => { statusDisplay.innerText = ""; }, 3000);
}
function calculateNerve(repNum, isLocked, mult) {
if (isLocked) return `<span class="gt-nerve-est">~${Math.ceil((500 / mult) * 3)}</span>`;
if (repNum >= 500) return `<span class="gt-nerve-done">Done</span>`;
return `<span class="gt-nerve-est">~${Math.ceil(((500 - repNum) / mult) * 3)}</span>`;
}
function loadSavedData() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
const data = JSON.parse(saved);
timeDisplay.innerText = `Last Fetch: ${data.timestamp}`;
let mult = parseFloat(multInput.value) || 1.0;
if (mult <= 0) mult = 0.1;
listDisplay.innerHTML = "";
data.areas.forEach(area => {
const row = document.createElement("div");
row.className = "gt-area-row";
const isLocked = area.rep.includes("Locked");
const col1 = document.createElement("span");
col1.className = "gt-col-1";
col1.innerText = area.name;
const col2 = document.createElement("span");
col2.className = "gt-col-2";
const repSpan = document.createElement("span");
repSpan.className = isLocked ? "gt-locked" : "gt-rep";
repSpan.innerText = area.rep;
col2.appendChild(repSpan);
if (area.hasOldData) {
const deltaSpan = document.createElement("span");
if (area.delta > 0) {
deltaSpan.className = "gt-delta-pos"; deltaSpan.innerText = `(+${area.delta})`;
} else if (area.delta < 0) {
deltaSpan.className = "gt-delta-neg"; deltaSpan.innerText = `(${area.delta})`;
} else {
deltaSpan.className = "gt-delta-zero"; deltaSpan.innerText = `(0)`;
}
col2.appendChild(deltaSpan);
}
const col3 = document.createElement("span");
col3.className = "gt-col-3";
col3.innerHTML = calculateNerve(area.rawRep, isLocked, mult);
row.appendChild(col1); row.appendChild(col2); row.appendChild(col3);
listDisplay.appendChild(row);
});
} catch (e) { console.error("Graffiti Tracker: Error parsing data."); }
}
}
function applyNativeStyling() {
const saved = localStorage.getItem(STORAGE_KEY);
if (!saved) return;
let data;
try { data = JSON.parse(saved); } catch (e) { return; }
if (!data || !Array.isArray(data.areas)) return;
const interval = parseInt(intInput.value, 10) || 0;
let highestActiveRep = 0;
data.areas.forEach(a => {
if (a.rawRep < 500 && a.rawRep > highestActiveRep) {
highestActiveRep = a.rawRep;
}
});
const cards = document.querySelectorAll('[class*="crimeWrapper"], [class*="crimeOption"], [class*="crimePanel"]');
cards.forEach(card => {
const lines = card.innerText.split('\n').map(line => line.trim()).filter(line => line !== "");
if (lines.length < 2) return;
const areaName = lines[0];
const matchArea = data.areas.find(a => a.name === areaName);
if (!matchArea) return;
const rep = matchArea.rawRep;
if (rep === 0 || matchArea.rep === "Locked") return;
const rowWrapper = card.querySelector('[class*="item"], [class*="row"]') || card.firstElementChild;
if (!rowWrapper) return;
const children = Array.from(rowWrapper.children);
let boxA = children.find(child => child.querySelector('svg') || (child.querySelector('img') && !child.querySelector('img').src.includes('nerve') && !child.querySelector('img').src.includes('money')));
if (!boxA && children.length >= 2) { boxA = children[1]; }
if (children[0] && children[0] !== boxA) { children[0].style.backgroundColor = ''; }
if (boxA) {
boxA.style.transition = 'background 0.3s';
boxA.style.borderRadius = '4px';
boxA.style.padding = '2px';
if (rep >= 500) {
boxA.style.backgroundColor = 'rgba(255, 255, 0, 0.4)';
} else if (interval === 0) {
boxA.style.backgroundColor = (rep === highestActiveRep) ? 'rgba(0, 0, 255, 0.4)' : 'rgba(0, 128, 0, 0.4)';
} else {
const target = highestActiveRep + interval;
const opacity = Math.min(0.8, Math.max(0.1, rep / target));
boxA.style.backgroundColor = `rgba(0, 128, 0, ${opacity})`;
}
}
});
}
function fetchReputation() {
const cards = document.querySelectorAll('[class*="crimeWrapper"], [class*="crimeOption"], [class*="crimePanel"]');
if (cards.length === 0) {
flashStatus("Wait for areas to load...", true);
return;
}
const oldSaved = localStorage.getItem(STORAGE_KEY);
let oldData = null;
try { oldData = oldSaved ? JSON.parse(oldSaved) : null; } catch(e) { oldData = null; }
const oldRepMap = {};
if (oldData && oldData.areas) {
oldData.areas.forEach(a => {
let oldRepNum = a.rawRep !== undefined ? a.rawRep : parseInt(a.rep, 10);
if (isNaN(oldRepNum)) oldRepNum = 0;
oldRepMap[a.name] = oldRepNum;
});
}
const areas = [];
const seenNames = new Set();
cards.forEach(card => {
const lines = card.innerText.split('\n').map(line => line.trim()).filter(line => line !== "");
if (lines.length >= 2) {
const areaName = lines[0];
const repValueStr = lines[1];
const startsWithNumber = /^\d/.test(areaName);
if (!startsWithNumber && !seenNames.has(areaName)) {
seenNames.add(areaName);
let newRepNum = parseInt(repValueStr, 10);
if (isNaN(newRepNum)) newRepNum = 0;
let finalStatus = newRepNum.toString();
if (repValueStr === "0" || repValueStr.includes("Locked") || isNaN(parseInt(repValueStr, 10))) {
finalStatus = "Locked";
newRepNum = 0;
}
let delta = 0;
let hasOldData = oldRepMap.hasOwnProperty(areaName);
if (hasOldData) { delta = newRepNum - oldRepMap[areaName]; }
areas.push({ name: areaName, rep: finalStatus, rawRep: newRepNum, delta: delta, hasOldData: hasOldData });
}
}
});
if (areas.length > 0) {
const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true };
const timestampLocal = new Date().toLocaleString(undefined, options);
localStorage.setItem(STORAGE_KEY, JSON.stringify({ timestamp: timestampLocal, areas: areas }));
loadSavedData();
applyNativeStyling();
flashStatus("Success!", false);
} else {
flashStatus("Failed to read text.", true);
}
}
document.getElementById("gt-fetch-btn").addEventListener("click", fetchReputation);
document.getElementById("gt-close-btn").addEventListener("click", () => {
isGuiClosed = true;
gui.style.display = "none";
});
document.getElementById("gt-min-btn").addEventListener("click", () => {
isMinimized = !isMinimized;
body.style.display = isMinimized ? "none" : "block";
document.getElementById("gt-min-btn").innerText = isMinimized ? "+" : "-";
if (isMinimized) header.style.borderRadius = "8px";
else header.style.borderRadius = "8px 8px 0 0";
});
let isDragging = false, currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
function dragStart(e) {
if (e.target.tagName === "BUTTON" || e.target.tagName === "INPUT") return;
if (e.type === "touchstart") {
initialX = e.touches[0].clientX - xOffset;
initialY = e.touches[0].clientY - yOffset;
} else {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
isDragging = true;
}
function dragEnd() { initialX = currentX; initialY = currentY; isDragging = false; }
function drag(e) {
if (!isDragging) return;
e.preventDefault();
if (e.type === "touchmove") {
currentX = e.touches[0].clientX - initialX;
currentY = e.touches[0].clientY - initialY;
} else {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
}
xOffset = currentX; yOffset = currentY;
gui.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
}
header.addEventListener("touchstart", dragStart, { passive: false });
document.addEventListener("touchend", dragEnd);
document.addEventListener("touchmove", drag, { passive: false });
header.addEventListener("mousedown", dragStart);
document.addEventListener("mouseup", dragEnd);
document.addEventListener("mousemove", drag);
setInterval(() => {
const url = window.location.href;
const isGraffitiPage = url.includes("crimes") && url.includes("graffiti");
if (isGraffitiPage) {
if (!isGuiClosed && gui.style.display !== "block") {
loadSavedData();
gui.style.display = "block";
}
applyNativeStyling();
} else {
gui.style.display = "none";
if (!url.includes("crimes")) isGuiClosed = false;
}
}, 1000);
})();