Greasy Fork is available in English.
Parses Xanax and overdose data directly from the Addiction Log page HTML
// ==UserScript==
// @name Torn City OD Tracker (HTML Parser)
// @namespace http://tampermonkey.net/
// @version 10.0
// @description Parses Xanax and overdose data directly from the Addiction Log page HTML
// @author c0pyc4t
// @match https://www.torn.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect torn.com
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let xanaxEvents = [];
let overdoseEvents = [];
let rehabilitationEvents = [];
let currentStats = {
xanaxSinceLastOD: 0,
lastODTimestamp: null,
lastODDate: null,
totalXanaxCount: 0,
totalODCount: 0,
addictionLevel: 0
};
let panelPosition = { left: 20, top: 100 };
let isLoading = false;
// ================= STORAGE =================
function saveData() {
GM_setValue('xanax_events_v3', JSON.stringify(xanaxEvents));
GM_setValue('overdose_events_v3', JSON.stringify(overdoseEvents));
GM_setValue('rehab_events_v3', JSON.stringify(rehabilitationEvents));
GM_setValue('current_stats_v3', JSON.stringify(currentStats));
GM_setValue('panel_position', JSON.stringify(panelPosition));
}
function loadData() {
xanaxEvents = JSON.parse(GM_getValue('xanax_events_v3', '[]'));
overdoseEvents = JSON.parse(GM_getValue('overdose_events_v3', '[]'));
rehabilitationEvents = JSON.parse(GM_getValue('rehab_events_v3', '[]'));
currentStats = JSON.parse(GM_getValue('current_stats_v3', JSON.stringify(currentStats)));
panelPosition = JSON.parse(GM_getValue('panel_position', JSON.stringify({ left: 20, top: 100 })));
}
// ================= FETCH AND PARSE ADDICTION LOG =================
async function fetchAddictionLog() {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: "https://www.torn.com/page.php?sid=log&cat=126",
onload: (response) => {
if (response.status === 200) {
console.log("✅ Addiction log page loaded");
resolve(response.responseText);
} else {
console.error("Failed to load page:", response.status);
resolve(null);
}
},
onerror: () => resolve(null)
});
});
}
function parseAddictionLog(html) {
const newXanax = [];
const newOverdoses = [];
const newRehabs = [];
console.log("📝 Parsing HTML log entries...");
// Create a temporary DOM element to parse the HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Find all log-text spans
const logSpans = doc.querySelectorAll('span.log-text');
console.log(`📊 Found ${logSpans.length} log entries`);
// Also look for date headers
const dateHeaders = doc.querySelectorAll('.log-date, .date-header, [class*="date"]');
let currentDate = new Date();
// Process date headers first to establish timeline
dateHeaders.forEach(header => {
const headerText = header.textContent.trim().toUpperCase();
if (headerText.includes('TODAY')) {
currentDate = new Date();
} else if (headerText.includes('YESTERDAY')) {
currentDate = new Date();
currentDate.setDate(currentDate.getDate() - 1);
} else {
const daysMatch = headerText.match(/(\d+)\s+DAYS?\s+AGO/);
if (daysMatch) {
const daysAgo = parseInt(daysMatch[1]);
currentDate = new Date();
currentDate.setDate(currentDate.getDate() - daysAgo);
}
}
});
// Process each log entry
logSpans.forEach(span => {
const logText = span.textContent.trim();
// Look for timestamps in parent elements or nearby
let timestamp = currentDate.getTime();
let dateStr = currentDate.toLocaleString();
// Check for time in nearby elements
let parent = span.parentElement;
let timeElement = parent?.querySelector('.log-time, [class*="time"]');
if (timeElement) {
const timeText = timeElement.textContent.trim();
const timeMatch = timeText.match(/(\d{2}):(\d{2})/);
if (timeMatch) {
const hours = parseInt(timeMatch[1]);
const minutes = parseInt(timeMatch[2]);
const dateWithTime = new Date(currentDate);
dateWithTime.setHours(hours, minutes, 0);
timestamp = dateWithTime.getTime();
dateStr = dateWithTime.toLocaleString();
}
}
// Check for Xanax usage
if (logText.toLowerCase().includes('xanax')) {
// Check if it's an overdose
if (logText.toLowerCase().includes('overdose') || logText.toLowerCase().includes('od')) {
console.log(`💀 Overdose found: ${dateStr} - ${logText.substring(0, 50)}...`);
newOverdoses.push({
timestamp: timestamp,
date: dateStr,
text: logText,
type: 'overdose'
});
}
// Regular Xanax use
else if (logText.toLowerCase().includes('used') || logText.toLowerCase().includes('gain')) {
console.log(`💊 Xanax found: ${dateStr} - ${logText.substring(0, 50)}...`);
newXanax.push({
timestamp: timestamp,
date: dateStr,
text: logText,
type: 'xanax'
});
}
}
// Track rehabilitation (resets addiction)
if (logText.toLowerCase().includes('rehabilitated')) {
console.log(`💚 Rehab found: ${dateStr}`);
newRehabs.push({
timestamp: timestamp,
date: dateStr,
text: logText,
type: 'rehab'
});
}
});
// Also use regex as backup to catch any missed entries
const xanaxRegex = /You used some Xanax/g;
const odRegex = /You overdosed on some Xanax/g;
const rehabRegex = /You rehabilitated \d+ times/g;
let match;
while ((match = xanaxRegex.exec(html)) !== null) {
// Check if already captured
const contextStart = Math.max(0, match.index - 200);
const context = html.substring(contextStart, match.index + 200);
// Try to find timestamp in context
let timestamp = Date.now();
let dateStr = new Date().toLocaleString();
const timeMatch = context.match(/(\d{2}:\d{2}:\d{2}\s*-\s*\d{2}\/\d{2}\/\d{2})/);
if (timeMatch) {
const parsedDate = parseLogDate(timeMatch[1]);
timestamp = parsedDate.getTime();
dateStr = parsedDate.toLocaleString();
}
if (!newXanax.some(x => Math.abs(x.timestamp - timestamp) < 60000)) {
newXanax.push({
timestamp: timestamp,
date: dateStr,
text: "You used some Xanax",
type: 'xanax'
});
}
}
while ((match = odRegex.exec(html)) !== null) {
const contextStart = Math.max(0, match.index - 200);
const context = html.substring(contextStart, match.index + 200);
let timestamp = Date.now();
let dateStr = new Date().toLocaleString();
const timeMatch = context.match(/(\d{2}:\d{2}:\d{2}\s*-\s*\d{2}\/\d{2}\/\d{2})/);
if (timeMatch) {
const parsedDate = parseLogDate(timeMatch[1]);
timestamp = parsedDate.getTime();
dateStr = parsedDate.toLocaleString();
}
if (!newOverdoses.some(od => Math.abs(od.timestamp - timestamp) < 60000)) {
newOverdoses.push({
timestamp: timestamp,
date: dateStr,
text: "You overdosed on some Xanax",
type: 'overdose'
});
}
}
console.log(`📊 Parse results: ${newXanax.length} Xanax, ${newOverdoses.length} ODs, ${newRehabs.length} Rehabs`);
return { xanax: newXanax, overdoses: newOverdoses, rehabs: newRehabs };
}
function parseLogDate(dateStr) {
// Format: "19:47:39 - 01/04/26"
const timeMatch = dateStr.match(/(\d{2}):(\d{2}):(\d{2})\s*-\s*(\d{2})\/(\d{2})\/(\d{2})/);
if (timeMatch) {
const hours = parseInt(timeMatch[1]);
const minutes = parseInt(timeMatch[2]);
const seconds = parseInt(timeMatch[3]);
const day = parseInt(timeMatch[4]);
const month = parseInt(timeMatch[5]) - 1;
let year = parseInt(timeMatch[6]);
year = 2000 + year;
const date = new Date(year, month, day, hours, minutes, seconds);
if (!isNaN(date.getTime())) {
return date;
}
}
return new Date();
}
// ================= UPDATE STATS =================
function updateStats() {
// Sort by timestamp (newest first)
xanaxEvents.sort((a, b) => b.timestamp - a.timestamp);
overdoseEvents.sort((a, b) => b.timestamp - a.timestamp);
rehabilitationEvents.sort((a, b) => b.timestamp - a.timestamp);
// Remove duplicates
const uniqueXanax = [];
const seenXanax = new Set();
for (let x of xanaxEvents) {
const key = `${x.timestamp}`;
if (!seenXanax.has(key)) {
seenXanax.add(key);
uniqueXanax.push(x);
}
}
xanaxEvents = uniqueXanax;
const uniqueODs = [];
const seenODs = new Set();
for (let od of overdoseEvents) {
const key = `${od.timestamp}`;
if (!seenODs.has(key)) {
seenODs.add(key);
uniqueODs.push(od);
}
}
overdoseEvents = uniqueODs;
// Get last overdose
if (overdoseEvents.length > 0) {
const lastOD = overdoseEvents[0];
currentStats.lastODTimestamp = lastOD.timestamp;
currentStats.lastODDate = lastOD.date;
// Count Xanax since last OD
currentStats.xanaxSinceLastOD = xanaxEvents.filter(x => x.timestamp > lastOD.timestamp).length;
} else {
currentStats.xanaxSinceLastOD = xanaxEvents.length;
currentStats.lastODDate = null;
currentStats.lastODTimestamp = null;
}
currentStats.totalXanaxCount = xanaxEvents.length;
currentStats.totalODCount = overdoseEvents.length;
// Calculate addiction level (rough estimate)
// Each Xanax increases addiction, rehab reduces it
let addiction = 0;
const events = [...xanaxEvents, ...overdoseEvents, ...rehabilitationEvents];
events.sort((a, b) => a.timestamp - b.timestamp);
for (let event of events) {
if (event.type === 'xanax' || event.type === 'overdose') {
addiction = Math.min(100, addiction + 5);
} else if (event.type === 'rehab') {
addiction = Math.max(0, addiction - 100);
}
}
currentStats.addictionLevel = Math.round(addiction);
saveData();
updateUI();
console.log(`📈 Stats updated: ${currentStats.xanaxSinceLastOD} Xanax since last OD`);
console.log(`📊 Total: ${xanaxEvents.length} Xanax, ${overdoseEvents.length} ODs`);
console.log(`💊 Addiction level: ${currentStats.addictionLevel}%`);
}
// ================= PREDICTIONS =================
function calculateODProbability() {
if (overdoseEvents.length === 0) {
const risk = Math.min(95, Math.round((currentStats.xanaxSinceLastOD / 18) * 100));
return risk;
}
// Calculate average Xanax between ODs
const xanaxBetweenODs = [];
for (let i = 0; i < overdoseEvents.length - 1; i++) {
const currentOD = overdoseEvents[i];
const nextOD = overdoseEvents[i + 1];
const count = xanaxEvents.filter(x =>
x.timestamp > nextOD.timestamp && x.timestamp < currentOD.timestamp
).length;
if (count > 0) xanaxBetweenODs.push(count);
}
if (xanaxBetweenODs.length === 0) {
return Math.min(95, Math.round((currentStats.xanaxSinceLastOD / 18) * 100));
}
const avg = xanaxBetweenODs.reduce((a, b) => a + b, 0) / xanaxBetweenODs.length;
const current = currentStats.xanaxSinceLastOD;
let probability;
if (current >= avg) {
probability = 50 + Math.min(48, ((current - avg) / avg) * 50);
} else {
probability = (current / avg) * 50;
}
// Factor in addiction level
const addictionFactor = currentStats.addictionLevel / 100;
probability = probability * (1 + addictionFactor * 0.3);
return Math.min(98, Math.max(1, Math.round(probability)));
}
function getNextODPrediction() {
if (overdoseEvents.length < 2) return null;
const xanaxBetweenODs = [];
for (let i = 0; i < overdoseEvents.length - 1; i++) {
const currentOD = overdoseEvents[i];
const nextOD = overdoseEvents[i + 1];
const count = xanaxEvents.filter(x =>
x.timestamp > nextOD.timestamp && x.timestamp < currentOD.timestamp
).length;
if (count > 0) xanaxBetweenODs.push(count);
}
if (xanaxBetweenODs.length === 0) return null;
const avgXanaxToOD = xanaxBetweenODs.reduce((a, b) => a + b, 0) / xanaxBetweenODs.length;
const remainingXanax = Math.max(0, Math.round(avgXanaxToOD - currentStats.xanaxSinceLastOD));
// Calculate trend (increasing or decreasing risk)
let trend = "stable";
if (xanaxBetweenODs.length >= 3) {
const recent = xanaxBetweenODs.slice(0, 2);
const older = xanaxBetweenODs.slice(-2);
const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length;
const olderAvg = older.reduce((a, b) => a + b, 0) / older.length;
if (recentAvg < olderAvg) trend = "decreasing (safer)";
else if (recentAvg > olderAvg) trend = "increasing (riskier)";
}
return {
remainingXanax: remainingXanax,
avgXanaxToOD: Math.round(avgXanaxToOD),
confidence: Math.min(95, Math.round((overdoseEvents.length / 15) * 100)),
trend: trend
};
}
function getRiskLevel(probability) {
if (probability >= 80) return { text: "CRITICAL", color: "#ff0000", emoji: "🔴", action: "⚠️ STOP! Very high risk!" };
if (probability >= 60) return { text: "HIGH", color: "#ff6600", emoji: "🟠", action: "⚠️ Be extremely careful" };
if (probability >= 35) return { text: "MEDIUM", color: "#ffcc00", emoji: "🟡", action: "Monitor closely" };
if (probability >= 15) return { text: "LOW", color: "#88ff88", emoji: "🟢", action: "Normal risk level" };
return { text: "VERY LOW", color: "#00ff00", emoji: "✅", action: "Safe for now" };
}
// ================= SYNC LOGS =================
async function syncLogs() {
if (isLoading) {
showNotification("Already loading...", "info");
return;
}
isLoading = true;
showNotification("📊 Reading Addiction Log...", "info");
updateUI(true);
const html = await fetchAddictionLog();
if (!html) {
showNotification("❌ Could not load Addiction Log. Make sure you're on Torn.", "error");
isLoading = false;
updateUI(false);
return;
}
const parsed = parseAddictionLog(html);
let newXanaxCount = 0;
for (let x of parsed.xanax) {
if (!xanaxEvents.some(e => Math.abs(e.timestamp - x.timestamp) < 60000)) {
xanaxEvents.push(x);
newXanaxCount++;
}
}
let newODCount = 0;
for (let od of parsed.overdoses) {
if (!overdoseEvents.some(e => Math.abs(e.timestamp - od.timestamp) < 60000)) {
overdoseEvents.push(od);
newODCount++;
showNotification(`⚠️ Overdose found: ${od.date}`, "warning");
}
}
let newRehabCount = 0;
for (let r of parsed.rehabs) {
if (!rehabilitationEvents.some(e => Math.abs(e.timestamp - r.timestamp) < 60000)) {
rehabilitationEvents.push(r);
newRehabCount++;
}
}
updateStats();
showNotification(`✅ Found ${newXanaxCount} Xanax, ${newODCount} ODs, ${newRehabCount} Rehabs`, "success");
isLoading = false;
}
// ================= UI =================
function makeDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const header = element.querySelector('.od-header');
if (!header) return;
header.style.cursor = 'move';
header.onmousedown = dragMouseDown;
function dragMouseDown(e) {
if (e.target.tagName === 'BUTTON') return;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
let newLeft = element.offsetLeft - pos1;
let newTop = element.offsetTop - pos2;
newLeft = Math.min(Math.max(0, newLeft), window.innerWidth - element.offsetWidth);
newTop = Math.min(Math.max(0, newTop), window.innerHeight - element.offsetHeight);
element.style.left = newLeft + 'px';
element.style.top = newTop + 'px';
element.style.bottom = 'auto';
element.style.right = 'auto';
panelPosition = { left: newLeft, top: newTop };
saveData();
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
function createUI() {
if (document.getElementById("odbox")) return;
const panelHTML = `
<div id="odbox" style="
position: fixed;
left: ${panelPosition.left}px;
top: ${panelPosition.top}px;
background: linear-gradient(135deg, #1a1a2e 0%, #0d1117 100%);
border-radius: 12px;
z-index: 9999;
width: 360px;
font-family: 'Segoe UI', Arial, sans-serif;
border: 1px solid rgba(255,255,255,0.15);
box-shadow: 0 4px 15px rgba(0,0,0,0.4);
">
<div class="od-header" style="
padding: 12px 15px;
background: rgba(0,0,0,0.3);
border-radius: 12px 12px 0 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
user-select: none;
">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 14px; font-weight: bold;">💊 OD Tracker</span>
<div style="display: flex; gap: 5px;">
<button id="od-refresh-btn" style="
background: #44aaff;
border: none;
color: white;
padding: 4px 8px;
border-radius: 5px;
cursor: pointer;
font-size: 11px;
">🔄 Refresh</button>
<button id="od-toggle-btn" style="
background: #ff8844;
border: none;
color: white;
padding: 4px 8px;
border-radius: 5px;
cursor: pointer;
font-size: 11px;
">👁️ Hide</button>
<button id="od-minimize-btn" style="
background: none;
border: none;
color: #aaa;
cursor: pointer;
font-size: 18px;
">−</button>
</div>
</div>
</div>
<div id="od-tracker-content" style="padding: 15px;">
<div style="text-align:center; color:#888;">Click Refresh to load logs</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', panelHTML);
const panel = document.getElementById('odbox');
makeDraggable(panel);
document.getElementById('od-refresh-btn')?.addEventListener('click', () => syncLogs());
document.getElementById('od-toggle-btn')?.addEventListener('click', () => {
const content = document.getElementById('od-tracker-content');
if (content) {
panelVisible = !panelVisible;
content.style.display = panelVisible ? 'block' : 'none';
}
});
let minimized = false;
document.getElementById('od-minimize-btn')?.addEventListener('click', () => {
const content = document.getElementById('od-tracker-content');
if (content) {
minimized = !minimized;
content.style.display = minimized ? 'none' : 'block';
document.getElementById('od-minimize-btn').textContent = minimized ? '+' : '−';
}
});
// Auto-load on first run
if (xanaxEvents.length === 0 && overdoseEvents.length === 0) {
setTimeout(() => syncLogs(), 2000);
} else {
updateUI();
}
}
function updateUI(loading = false) {
const content = document.getElementById('od-tracker-content');
if (!content) return;
if (loading) {
content.innerHTML = `
<div style="text-align:center; padding: 20px;">
<div style="color: #44aaff;">📊 Loading Addiction Log...</div>
<div style="font-size: 11px; color: #888; margin-top: 8px;">Parsing Xanax and overdose events</div>
</div>
`;
return;
}
const probability = calculateODProbability();
const risk = getRiskLevel(probability);
const prediction = getNextODPrediction();
// Addiction level color
let addictionColor = "#88ff88";
if (currentStats.addictionLevel > 70) addictionColor = "#ff4444";
else if (currentStats.addictionLevel > 40) addictionColor = "#ffcc00";
content.innerHTML = `
<div style="text-align:center; margin-bottom: 12px;">
<div style="font-size: 48px; font-weight: bold; color: ${risk.color};">${currentStats.xanaxSinceLastOD}</div>
<div style="font-size: 11px; color: #888;">XANAX SINCE LAST OVERDOSE</div>
</div>
<div style="background: rgba(0,0,0,0.3); border-radius: 8px; padding: 10px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 6px;">
<span>${risk.emoji} OD Risk Level:</span>
<span style="color: ${risk.color}; font-weight: bold;">${risk.text} (${probability}%)</span>
</div>
<div style="background: rgba(255,255,255,0.1); height: 6px; border-radius: 3px; overflow: hidden;">
<div style="width: ${probability}%; background: ${risk.color}; height: 100%;"></div>
</div>
<div style="font-size: 10px; margin-top: 5px; color: #aaa;">${risk.action}</div>
</div>
<div style="font-size: 11px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span>📅 Last Overdose:</span>
<span style="color: #aaa;">${currentStats.lastODDate || 'Never'}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span>💊 Total Xanax:</span>
<span style="color: #88ff88;">${currentStats.totalXanaxCount}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span>💀 Total ODs:</span>
<span style="color: #ff8888;">${currentStats.totalODCount}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>💚 Addiction Level:</span>
<span style="color: ${addictionColor};">${currentStats.addictionLevel}%</span>
</div>
${prediction ? `
<div style="background: rgba(0,0,0,0.4); border-radius: 8px; padding: 8px; margin-top: 8px; border-left: 3px solid #ffcc00;">
<div style="font-size: 10px; color: #ffcc00; margin-bottom: 5px;">🎯 PREDICTION</div>
<div style="display: flex; justify-content: space-between; font-size: 11px;">
<span>Expected in:</span>
<span style="color: #88ff88; font-weight: bold;">~${prediction.remainingXanax} more Xanax</span>
</div>
<div style="display: flex; justify-content: space-between; font-size: 10px; margin-top: 3px;">
<span>Your average:</span>
<span>${prediction.avgXanaxToOD} Xanax between ODs</span>
</div>
<div style="display: flex; justify-content: space-between; font-size: 10px;">
<span>Trend:</span>
<span style="color: #ffcc00;">${prediction.trend}</span>
</div>
<div style="font-size: 9px; color: #666; margin-top: 4px;">Confidence: ${prediction.confidence}% (${overdoseEvents.length} ODs)</div>
</div>
` : '<div style="color:#666;text-align:center;margin-top:8px;">📈 Need 2+ overdoses for prediction</div>'}
</div>
<div style="font-size: 10px; color: #666; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; text-align: center;">
📊 ${overdoseEvents.length} ODs | 💊 ${xanaxEvents.length} Xanax | 💚 ${rehabilitationEvents.length} Rehabs
<br>🔄 Data from Addiction Log
</div>
`;
}
function showNotification(message, type) {
const notif = document.createElement('div');
notif.textContent = message;
notif.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'error' ? '#ff4444' : type === 'warning' ? '#ff8800' : '#44aaff'};
color: white;
padding: 10px 15px;
border-radius: 8px;
z-index: 10000;
font-weight: bold;
font-family: 'Segoe UI', Arial, sans-serif;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
`;
document.body.appendChild(notif);
setTimeout(() => notif.remove(), 4000);
}
// ================= INIT =================
function init() {
console.log("🚀 OD Tracker v10.0 - HTML Parser");
console.log("📝 Looking for span.log-text elements");
loadData();
createUI();
}
init();
})();