// ==UserScript==
// @name Torn Faction Summaries
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Floating summaries of faction online and status info on Torn.com, with API key input, persistent positions, settings, clickable statuses showing member names, and optional second faction tracking.
// @author Roofis
// @match *://www.torn.com/*
// @match *://torn.com/*
// @icon https://www.torn.com/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Config
let API_KEY = localStorage.getItem('tornApiKey') || '';
const USER_FACTION_ID = ''; // User's faction ID (leave blank if dynamic)
const settings = JSON.parse(localStorage.getItem('factionSummarySettings')) || { lockPosition: true, trackSecondFaction: false, secondFactionId: '' };
const UPDATE_INTERVAL = 30000;
const DEFAULT_POSITIONS = {
userOnline: { top: '10px', left: '10px' },
userStatus: { top: '50px', left: '10px' },
secondOnline: { top: '10px', left: '250px' },
secondStatus: { top: '50px', left: '250px' }
};
const STATUS_COLORS = {
'Online': '#00FF00',
'Idle': '#FFD700',
'Offline': '#FF0000',
'Okay': '#00FF00',
'Traveling': '#00CCFF',
'Abroad': '#00CCFF',
'In Hospital': '#FF0000',
'In Jail': '#FFFFFF',
'Federal': '#808080'
};
// Load saved positions or set defaults
const savedUserOnlinePos = settings.lockPosition ? (JSON.parse(localStorage.getItem('userOnlineBoxPos')) || DEFAULT_POSITIONS.userOnline) : DEFAULT_POSITIONS.userOnline;
const savedUserStatusPos = settings.lockPosition ? (JSON.parse(localStorage.getItem('userStatusBoxPos')) || DEFAULT_POSITIONS.userStatus) : DEFAULT_POSITIONS.userStatus;
const savedSecondOnlinePos = settings.lockPosition ? (JSON.parse(localStorage.getItem('secondOnlineBoxPos')) || DEFAULT_POSITIONS.secondOnline) : DEFAULT_POSITIONS.secondOnline;
const savedSecondStatusPos = settings.lockPosition ? (JSON.parse(localStorage.getItem('secondStatusBoxPos')) || DEFAULT_POSITIONS.secondStatus) : DEFAULT_POSITIONS.secondStatus;
// Create User Faction Boxes
const userOnlineBox = document.createElement('div');
userOnlineBox.id = 'user-online-summary-box';
userOnlineBox.style.top = savedUserOnlinePos.top;
userOnlineBox.style.left = savedUserOnlinePos.left;
document.body.appendChild(userOnlineBox);
const userStatusBox = document.createElement('div');
userStatusBox.id = 'user-status-summary-box';
userStatusBox.style.top = savedUserStatusPos.top;
userStatusBox.style.left = savedUserStatusPos.left;
document.body.appendChild(userStatusBox);
// Create Second Faction Boxes (hidden by default)
const secondOnlineBox = document.createElement('div');
secondOnlineBox.id = 'second-online-summary-box';
secondOnlineBox.style.top = savedSecondOnlinePos.top;
secondOnlineBox.style.left = savedSecondOnlinePos.left;
secondOnlineBox.style.display = settings.trackSecondFaction ? 'block' : 'none';
document.body.appendChild(secondOnlineBox);
const secondStatusBox = document.createElement('div');
secondStatusBox.id = 'second-status-summary-box';
secondStatusBox.style.top = savedSecondStatusPos.top;
secondStatusBox.style.left = savedSecondStatusPos.left;
secondStatusBox.style.display = settings.trackSecondFaction ? 'block' : 'none';
document.body.appendChild(secondStatusBox);
// Create API Key Input Box
const apiInputBox = document.createElement('div');
apiInputBox.id = 'api-input-box';
apiInputBox.innerHTML = `
<div style="background: rgba(26, 26, 26, 0.9); padding: 15px; border: 1px solid #ff00ff; box-shadow: 0 0 10px #ff00ff;">
<label style="color: #e0e0e0; font-family: 'Courier New', monospace;">Enter your Torn API Key:</label><br>
<input type="text" id="api-key-input" style="width: 200px; padding: 5px; margin: 10px 0; background: #3a3a3a; border: 1px solid #00ffcc; color: #e0e0e0; font-family: 'Courier New', monospace;">
<br>
<button id="api-save-btn" style="padding: 5px 10px; background: #ff00ff; border: none; color: #e0e0e0; cursor: pointer;">Save</button>
</div>
`;
apiInputBox.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10002;
display: none;
`;
document.body.appendChild(apiInputBox);
// Create Settings Button
const settingsBtn = document.createElement('button');
settingsBtn.id = 'settings-btn';
settingsBtn.textContent = '⚙️';
settingsBtn.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: #ff00ff;
color: #e0e0e0;
border: none;
padding: 5px 10px;
cursor: pointer;
z-index: 10001;
font-size: 16px;
`;
document.body.appendChild(settingsBtn);
// Create Settings Panel
const settingsPanel = document.createElement('div');
settingsPanel.id = 'settings-panel';
settingsPanel.innerHTML = `
<div style="background: rgba(26, 26, 26, 0.9); padding: 15px; border: 1px solid #ff00ff; box-shadow: 0 0 10px #ff00ff;">
<h3 style="color: #e0e0e0; font-family: 'Courier New', monospace; margin: 0 0 10px;">Settings</h3>
<label style="color: #e0e0e0; font-family: 'Courier New', monospace;">
<input type="checkbox" id="lock-position" ${settings.lockPosition ? 'checked' : ''}> Lock Position Across Pages
</label><br>
<label style="color: #e0e0e0; font-family: 'Courier New', monospace;">
<input type="checkbox" id="track-second-faction" ${settings.trackSecondFaction ? 'checked' : ''}> Track Another Faction
</label><br>
<div id="second-faction-id-input" style="display: ${settings.trackSecondFaction ? 'block' : 'none'}; margin-top: 10px;">
<label style="color: #e0e0e0; font-family: 'Courier New', monospace;">Second Faction ID:</label><br>
<input type="text" id="second-faction-id" value="${settings.secondFactionId || ''}" style="width: 100px; padding: 5px; background: #3a3a3a; border: 1px solid #00ffcc; color: #e0e0e0; font-family: 'Courier New', monospace;">
</div>
<button id="settings-save-btn" style="padding: 5px 10px; background: #ff00ff; border: none; color: #e0e0e0; cursor: pointer; margin-top: 10px;">Save</button>
<button id="settings-close-btn" style="padding: 5px 10px; background: #666; border: none; color: #e0e0e0; cursor: pointer; margin-top: 10px; margin-left: 10px;">Close</button>
</div>
`;
settingsPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10002;
display: none;
`;
document.body.appendChild(settingsPanel);
// Create Names Popup Box
const namesPopup = document.createElement('div');
namesPopup.id = 'names-popup';
namesPopup.style.cssText = `
position: fixed;
top: 100px;
left: 200px;
background: rgba(26, 26, 26, 0.9);
color: #e0e0e0;
font-family: 'Courier New', monospace;
padding: 10px;
border: 1px solid #ff00ff;
box-shadow: 0 0 10px #ff00ff;
z-index: 10003;
display: none;
cursor: move;
max-height: 300px;
overflow-y: auto;
font-size: 16px;
`;
document.body.appendChild(namesPopup);
// Add CSS for floating boxes
const style = document.createElement('style');
style.textContent = `
#user-online-summary-box, #user-status-summary-box, #second-online-summary-box, #second-status-summary-box {
position: fixed;
background: rgba(26, 26, 26, 0.9);
color: #e0e0e0;
font-family: 'Courier New', monospace;
padding: 10px;
border: 1px solid #ff00ff;
box-shadow: 0 0 10px #ff00ff;
z-index: 10000;
font-size: 14px;
cursor: move;
user-select: none;
line-height: 1.5;
width: auto;
}
.status-span {
cursor: pointer;
}
.status-span:hover {
text-decoration: underline;
}
`;
document.head.appendChild(style);
// Make boxes draggable and save position
function makeDraggable(element, storageKey) {
let posX = 0, posY = 0, mouseX = 0, mouseY = 0;
element.onmousedown = dragMouseDown;
function dragMouseDown(e) {
if (e.target.className !== 'status-span' && e.target.id !== 'close-names-btn') {
e.preventDefault();
mouseX = e.clientX;
mouseY = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
}
function elementDrag(e) {
e.preventDefault();
posX = mouseX - e.clientX;
posY = mouseY - e.clientY;
mouseX = e.clientX;
mouseY = e.clientY;
element.style.top = (element.offsetTop - posY) + "px";
element.style.left = (element.offsetLeft - posX) + "px";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
if (settings.lockPosition) {
localStorage.setItem(storageKey, JSON.stringify({
top: element.style.top,
left: element.style.left
}));
}
}
}
makeDraggable(userOnlineBox, 'userOnlineBoxPos');
makeDraggable(userStatusBox, 'userStatusBoxPos');
makeDraggable(secondOnlineBox, 'secondOnlineBoxPos');
makeDraggable(secondStatusBox, 'secondStatusBoxPos');
makeDraggable(namesPopup, 'namesPopupPos');
// Settings Panel Logic
settingsBtn.onclick = () => {
settingsPanel.style.display = 'block';
};
document.getElementById('settings-close-btn').onclick = () => {
settingsPanel.style.display = 'none';
};
document.getElementById('track-second-faction').onchange = (e) => {
document.getElementById('second-faction-id-input').style.display = e.target.checked ? 'block' : 'none';
};
document.getElementById('settings-save-btn').onclick = () => {
const lockPosition = document.getElementById('lock-position').checked;
const trackSecondFaction = document.getElementById('track-second-faction').checked;
const secondFactionId = document.getElementById('second-faction-id').value.trim();
settings.lockPosition = lockPosition;
settings.trackSecondFaction = trackSecondFaction;
settings.secondFactionId = trackSecondFaction ? secondFactionId : '';
localStorage.setItem('factionSummarySettings', JSON.stringify(settings));
if (!lockPosition) {
localStorage.removeItem('userOnlineBoxPos');
localStorage.removeItem('userStatusBoxPos');
localStorage.removeItem('secondOnlineBoxPos');
localStorage.removeItem('secondStatusBoxPos');
userOnlineBox.style.top = DEFAULT_POSITIONS.userOnline.top;
userOnlineBox.style.left = DEFAULT_POSITIONS.userOnline.left;
userStatusBox.style.top = DEFAULT_POSITIONS.userStatus.top;
userStatusBox.style.left = DEFAULT_POSITIONS.userStatus.left;
secondOnlineBox.style.top = DEFAULT_POSITIONS.secondOnline.top;
secondOnlineBox.style.left = DEFAULT_POSITIONS.secondOnline.left;
secondStatusBox.style.top = DEFAULT_POSITIONS.secondStatus.top;
secondStatusBox.style.left = DEFAULT_POSITIONS.secondStatus.left;
}
secondOnlineBox.style.display = trackSecondFaction ? 'block' : 'none';
secondStatusBox.style.display = trackSecondFaction ? 'block' : 'none';
settingsPanel.style.display = 'none';
fetchFactionData(); // Refresh data immediately
};
// Online Summary Logic
function getOnlineSummary(members, factionName) {
const onlineCounts = { Online: 0, Idle: 0, Offline: 0 };
const onlineNames = { Online: [], Idle: [], Offline: [] };
for (const [id, member] of Object.entries(members || {})) {
const status = member.last_action?.status || "Offline";
if (status in onlineCounts) {
onlineCounts[status]++;
onlineNames[status].push(member.name);
}
}
return `${factionName} Online Status: <span class="status-span" style="color: ${STATUS_COLORS.Online}" data-status="Online" data-names="${onlineNames.Online.join(',')}">Online (${onlineCounts.Online})</span>, <span class="status-span" style="color: ${STATUS_COLORS.Idle}" data-status="Idle" data-names="${onlineNames.Idle.join(',')}">Idle (${onlineCounts.Idle})</span>, <span class="status-span" style="color: ${STATUS_COLORS.Offline}" data-status="Offline" data-names="${onlineNames.Offline.join(',')}">Offline (${onlineCounts.Offline})</span>`;
}
// Status Summary Logic
function getStatusSummary(members, factionName) {
const statusCounts = {
Okay: 0,
Traveling: 0,
Abroad: 0,
"In hospital": 0,
"In jail": 0,
Federal: 0
};
const statusNames = {
Okay: [],
Traveling: [],
Abroad: [],
"In hospital": [],
"In jail": [],
Federal: []
};
for (const [id, member] of Object.entries(members || {})) {
const state = (member.status?.state || "Okay").toLowerCase();
if (state.includes("hospital")) {
statusCounts["In hospital"]++;
statusNames["In hospital"].push(member.name);
} else if (state.includes("jail")) {
statusCounts["In jail"]++;
statusNames["In jail"].push(member.name);
} else if (state === "okay") {
statusCounts.Okay++;
statusNames.Okay.push(member.name);
} else if (state === "traveling") {
statusCounts.Traveling++;
statusNames.Traveling.push(member.name);
} else if (state === "abroad") {
statusCounts.Abroad++;
statusNames.Abroad.push(member.name);
} else if (state === "federal") {
statusCounts.Federal++;
statusNames.Federal.push(member.name);
}
}
return `${factionName} Status:<br>` +
`<span class="status-span" style="color: ${STATUS_COLORS.Okay}" data-status="Okay" data-names="${statusNames.Okay.join(',')}">Okay (${statusCounts.Okay})</span><br>` +
`<span class="status-span" style="color: ${STATUS_COLORS.Traveling}" data-status="Traveling" data-names="${statusNames.Traveling.join(',')}">Traveling (${statusCounts.Traveling})</span><br>` +
`<span class="status-span" style="color: ${STATUS_COLORS.Abroad}" data-status="Abroad" data-names="${statusNames.Abroad.join(',')}">Abroad (${statusCounts.Abroad})</span><br>` +
`<span class="status-span" style="color: ${STATUS_COLORS['In Hospital']}" data-status="In Hospital" data-names="${statusNames["In hospital"].join(',')}">In Hospital (${statusCounts["In hospital"]})</span><br>` +
`<span class="status-span" style="color: ${STATUS_COLORS['In Jail']}" data-status="In Jail" data-names="${statusNames["In jail"].join(',')}">In Jail (${statusCounts["In jail"]})</span><br>` +
`<span class="status-span" style="color: ${STATUS_COLORS.Federal}" data-status="Federal" data-names="${statusNames.Federal.join(',')}">Federal (${statusCounts.Federal})</span>`;
}
// Show Names Popup
function showNamesPopup(status, names) {
const color = STATUS_COLORS[status] || '#e0e0e0';
if (!names) {
namesPopup.innerHTML = `<span style="color: ${color}">${status}</span>: No members`;
} else {
const nameList = names.split(',').map(name => `<div style="color: ${color}">${name.trim()}</div>`).join('');
namesPopup.innerHTML = `<span style="color: ${color}">${status}</span>:<br>${nameList}<button id="close-names-btn" style="margin-top: 10px; padding: 5px; background: #ff00ff; border: none; color: #e0e0e0; cursor: pointer;">Close</button>`;
}
namesPopup.style.display = 'block';
document.getElementById('close-names-btn').onclick = () => {
namesPopup.style.display = 'none';
};
}
// Fetch Faction Data
async function fetchFactionData() {
try {
// User Faction
let userFactionUrl = `https://api.torn.com/faction/${USER_FACTION_ID}?selections=basic&key=${API_KEY}`;
console.log('Fetching user faction data from:', userFactionUrl);
const userResponse = await fetch(userFactionUrl);
if (!userResponse.ok) {
const text = await userResponse.text();
throw new Error(`User Faction HTTP error ${userResponse.status}: ${text}`);
}
const userFactionData = await userResponse.json();
if (userFactionData.error) throw new Error(`User Faction API Error: ${userFactionData.error.error}`);
console.log('User faction data received:', userFactionData);
const userFactionName = userFactionData.name || 'Your Faction';
userOnlineBox.innerHTML = getOnlineSummary(userFactionData.members, userFactionName);
userStatusBox.innerHTML = getStatusSummary(userFactionData.members, userFactionName);
// Second Faction (if enabled)
if (settings.trackSecondFaction && settings.secondFactionId) {
let secondFactionUrl = `https://api.torn.com/faction/${settings.secondFactionId}?selections=basic&key=${API_KEY}`;
console.log('Fetching second faction data from:', secondFactionUrl);
const secondResponse = await fetch(secondFactionUrl);
if (!secondResponse.ok) {
const text = await secondResponse.text();
throw new Error(`Second Faction HTTP error ${secondResponse.status}: ${text}`);
}
const secondFactionData = await secondResponse.json();
if (secondFactionData.error) throw new Error(`Second Faction API Error: ${secondFactionData.error.error}`);
console.log('Second faction data received:', secondFactionData);
const secondFactionName = secondFactionData.name || 'Second Faction';
secondOnlineBox.innerHTML = getOnlineSummary(secondFactionData.members, secondFactionName);
secondStatusBox.innerHTML = getStatusSummary(secondFactionData.members, secondFactionName);
}
// Add click listeners to status spans
document.querySelectorAll('.status-span').forEach(span => {
span.onclick = () => {
const status = span.getAttribute('data-status');
const names = span.getAttribute('data-names');
showNamesPopup(status, names);
};
});
} catch (error) {
console.error('Faction Fetch Error:', error.message);
userOnlineBox.innerHTML = 'Online Status: <span style="color: red">Error</span>';
userStatusBox.innerHTML = 'Status: <span style="color: red">Error</span>';
if (settings.trackSecondFaction) {
secondOnlineBox.innerHTML = 'Online Status: <span style="color: red">Error</span>';
secondStatusBox.innerHTML = 'Status: <span style="color: red">Error</span>';
}
showApiInput();
}
}
// Show API Key Input
function showApiInput() {
apiInputBox.style.display = 'block';
userOnlineBox.style.display = 'none';
userStatusBox.style.display = 'none';
secondOnlineBox.style.display = 'none';
secondStatusBox.style.display = 'none';
settingsBtn.style.display = 'none';
}
// Hide API Key Input
function hideApiInput() {
apiInputBox.style.display = 'none';
userOnlineBox.style.display = 'block';
userStatusBox.style.display = 'block';
secondOnlineBox.style.display = settings.trackSecondFaction ? 'block' : 'none';
secondStatusBox.style.display = settings.trackSecondFaction ? 'block' : 'none';
settingsBtn.style.display = 'block';
}
// Save API Key and Test
document.getElementById('api-save-btn').addEventListener('click', async () => {
const newApiKey = document.getElementById('api-key-input').value.trim();
if (!newApiKey) {
alert('Please enter a valid API key.');
return;
}
API_KEY = newApiKey;
localStorage.setItem('tornApiKey', API_KEY);
console.log('API Key saved:', API_KEY);
try {
let testUrl = `https://api.torn.com/faction/${USER_FACTION_ID}?selections=basic&key=${API_KEY}`;
const response = await fetch(testUrl);
const data = await response.json();
if (data.error) throw new Error(`API Error: ${data.error.error}`);
hideApiInput();
fetchFactionData();
setInterval(fetchFactionData, UPDATE_INTERVAL);
} catch (error) {
console.error('API Key Test Failed:', error.message);
alert('Invalid API key: ' + error.message);
}
});
// Initial Load Logic
if (!API_KEY) {
showApiInput();
} else {
fetchFactionData();
setInterval(fetchFactionData, UPDATE_INTERVAL);
}
})();