Unified faction dashboard: OC 2.0 management, war target intelligence, chain awareness, member activity — all in one draggable panel with TornPDA support and Discord integration.
// ==UserScript==
// @name Torn Faction Command Center
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description Unified faction dashboard: OC 2.0 management, war target intelligence, chain awareness, member activity — all in one draggable panel with TornPDA support and Discord integration.
// @author DownyJR
// @match https://www.torn.com/*
// @match https://torn.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect api.torn.com
// @connect discord.com
// @connect discordapp.com
// @run-at document-end
// @license MIT
// ==/UserScript==
/*
* =============================================================================
* TERMS OF SERVICE
* =============================================================================
*
* By using this script, you agree to the following terms:
*
* DATA STORAGE TABLE:
* ┌─────────────────────┬─────────────────────────────────────────────────────┐
* │ Data Storage │ Only locally (browser localStorage) │
* │ Data Sharing │ Nobody — all data stays on your device │
* │ Purpose of Use │ Competitive advantage — faction coordination & │
* │ │ organized crime planning │
* │ Key Storage │ Stored locally in your browser only, never shared │
* │ Key Access Level │ Limited — requires faction API access + faction info│
* └─────────────────────┴─────────────────────────────────────────────────────┘
*
* DISCORD INTEGRATION (Opt-In):
* If you choose to enable Discord webhook notifications, your faction data
* (member names, OC slots, chain timers) will be sent to the Discord webhook
* URL you provide. This is entirely optional and disabled by default.
* Discord Terms of Service: https://discord.com/terms
*
* API USAGE:
* This script only makes requests to api.torn.com using YOUR API key.
* No data is sent to any other external service except your configured
* Discord webhook (if enabled).
*
* TORN SCRIPT RULES COMPLIANCE:
* This script operates within Torn City's scripting rules:
* - Only uses data from api.torn.com or the page you are currently viewing
* - All page interactions require manual user action
* - No automated clicking, form submission, or page navigation
* - API calls may be automated (permitted by Torn rules)
*
* =============================================================================
*/
(function () {
"use strict";
// ============================ CONFIGURATION ============================
const CONFIG = {
version: "1.0.0",
apiBase: "https://api.torn.com",
refreshInterval: 60000, // API refresh every 60 seconds
defaultPosition: { x: 20, y: 100 },
panelWidth: 380,
collapsed: false,
};
// Settings keys
const SETTINGS = {
apiKey: "tfcc_apiKey",
discordWebhook: "tfcc_discordWebhook",
panelX: "tfcc_panelX",
panelY: "tfcc_panelY",
collapsed: "tfcc_collapsed",
activeTab: "tfcc_activeTab",
enableOC: "tfcc_enableOC",
enableWar: "tfcc_enableWar",
enableChain: "tfcc_enableChain",
enableMembers: "tfcc_enableMembers",
enableDiscord: "tfcc_enableDiscord",
};
// ============================ STATE ============================
let state = {
apiKey: "",
discordWebhook: "",
factionData: null,
ocData: null,
chainData: null,
warData: null,
membersData: null,
lastRefresh: null,
isLoading: false,
activeTab: "overview",
collapsed: false,
panelEl: null,
isDragging: false,
};
// ============================ TORN PDA COMPATIBILITY ============================
const isTornPDA = typeof PDA !== "undefined";
function pdaHttpGet(url, callback) {
if (isTornPDA && typeof PDA_httpGet === "function") {
PDA_httpGet(url, (response) => {
try {
const data = JSON.parse(response);
callback(data);
} catch (e) {
console.error("[TFCC] PDA parse error:", e);
callback(null);
}
});
} else {
// Fallback to fetch
fetch(url)
.then((r) => r.json())
.then((data) => callback(data))
.catch((err) => {
console.error("[TFCC] Fetch error:", err);
callback(null);
});
}
}
// ============================ SETTINGS MANAGEMENT ============================
function loadSettings() {
state.apiKey = GM_getValue(SETTINGS.apiKey, "");
state.discordWebhook = GM_getValue(SETTINGS.discordWebhook, "");
state.activeTab = GM_getValue(SETTINGS.activeTab, "overview");
state.collapsed = GM_getValue(SETTINGS.collapsed, false);
}
function saveSetting(key, value) {
GM_setValue(key, value);
}
function getApiKey() {
return state.apiKey;
}
// ============================ API HELPERS ============================
function buildApiUrl(selections = {}) {
const key = getApiKey();
if (!key) return null;
const defaultSelections = {
profile: "",
basic: "",
crimes: "",
attackers: "",
faction: "basic,members,crimes,contributors",
};
const merged = { ...defaultSelections, ...selections };
const parts = [];
for (const [k, v] of Object.entries(merged)) {
if (v) parts.push(`${k}=${v}`);
}
return `${CONFIG.apiBase}/v2/faction/? selections=${encodeURIComponent(merged.faction)}&key=${key}`;
}
function fetchFactionData(callback) {
if (!getApiKey()) {
callback(null);
return;
}
const url = `${CONFIG.apiBase}/v2/faction/?selections=basic,members,crimes,contributors&key=${getApiKey()}`;
pdaHttpGet(url, (data) => {
if (data && data.error) {
console.error("[TFCC] API Error:", data.error);
callback(null);
return;
}
state.factionData = data;
state.lastRefresh = Date.now();
callback(data);
});
}
function fetchPlayerCrimes(callback) {
if (!getApiKey()) {
callback(null);
return;
}
const url = `${CONFIG.apiBase}/v2/user/?selections=crimes&key=${getApiKey()}`;
pdaHttpGet(url, (data) => {
callback(data);
});
}
// ============================ PAGE DATA READERS ============================
// These read data from the CURRENTLY VIEWED page only — compliant with Torn rules
function readPageChainData() {
// Read chain data from faction chain page elements if present
const chainTimer = document.querySelector("[class*='chainTimer'], [class*='chain__timer'], .chain-bar, #chain-hold-bar");
const chainCount = document.querySelector("[class*='chainCount'], [class*='chain__count'], .chain-counter");
const chainMax = document.querySelector("[class*='chainMax'], [class*='chain__max']");
return {
timer: chainTimer ? chainTimer.textContent.trim() : null,
count: chainCount ? chainCount.textContent.trim() : null,
max: chainMax ? chainMax.textContent.trim() : null,
onPage: !!chainTimer || !!chainCount,
};
}
function readPageWarData() {
// Read war data from faction war page if present
const warElements = document.querySelectorAll("[class*='war__'], [class*='faction-war'], .war-list, [class*='enemy__']");
const hospitalTimers = document.querySelectorAll("[class*='hospital'], [class*='timer__']");
return {
onWarPage: warElements.length > 0,
enemyCount: warElements.length,
hospitalTimers: Array.from(hospitalTimers).map((el) => ({
text: el.textContent.trim(),
target: el.closest("[class*='enemy'], [class*='target'], [class*='row']")?.textContent?.slice(0, 30) || "Unknown",
})),
};
}
function detectCurrentPage() {
const path = window.location.pathname;
const hash = window.location.hash;
return {
isFaction: path.includes("/factions.php") || path.includes("/faction"),
isWar: path.includes("/war") || path.includes("?tab=war") || document.body.textContent.includes("Ranked War"),
isOC: path.includes("/crimes.php") || path.includes("?tab=crimes") || document.title.includes("Crimes"),
isChain: path.includes("/chain") || document.body.textContent.includes("Chain") && path.includes("faction"),
isBazaar: path.includes("/bazaar.php"),
isProfile: path.includes("/profiles.php") || path.includes("/user"),
path,
};
}
// ============================ DISCORD WEBHOOK ============================
function sendDiscordMessage(content, embeds = []) {
if (!state.discordWebhook || !state.discordWebhook.startsWith("http")) {
return;
}
const payload = {
username: "Faction Command Center",
avatar_url: "https://www.torn.com/images/v2/travel_agency/dest_1.jpg",
content: content,
embeds: embeds,
};
// Use GM_xmlhttpRequest for cross-origin support
if (typeof GM_xmlhttpRequest !== "undefined") {
GM_xmlhttpRequest({
method: "POST",
url: state.discordWebhook,
headers: { "Content-Type": "application/json" },
data: JSON.stringify(payload),
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
showNotification("Discord notification sent!", "success");
} else {
showNotification("Discord send failed", "error");
}
},
onerror: () => showNotification("Discord connection failed", "error"),
});
} else {
fetch(state.discordWebhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).catch(() => showNotification("Discord connection failed", "error"));
}
}
// ============================ NOTIFICATIONS ============================
function showNotification(message, type = "info") {
const colors = {
info: "#3498db",
success: "#2ecc71",
error: "#e74c3c",
warning: "#f39c12",
};
const notif = document.createElement("div");
notif.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${colors[type] || colors.info};
color: white;
padding: 12px 20px;
border-radius: 8px;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 13px;
font-weight: 600;
z-index: 999999;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
animation: tfcc-slide-in 0.3s ease;
max-width: 300px;
word-wrap: break-word;
`;
notif.textContent = message;
const style = document.createElement("style");
style.textContent = `
@keyframes tfcc-slide-in {
from { transform: translateX(400px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes tfcc-slide-out {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(400px); opacity: 0; }
}
`;
document.head.appendChild(style);
document.body.appendChild(notif);
setTimeout(() => {
notif.style.animation = "tfcc-slide-out 0.3s ease forwards";
setTimeout(() => notif.remove(), 300);
}, 3000);
}
// ============================ UI: CSS ============================
function injectStyles() {
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
#tfcc-panel {
font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
position: fixed;
top: 100px;
left: 20px;
width: 400px;
max-height: 80vh;
background: linear-gradient(145deg, #1a1d29 0%, #12141d 100%);
border: 1px solid #2d3142;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);
z-index: 99999;
overflow: hidden;
transition: height 0.3s ease, opacity 0.2s ease;
color: #c9cdd4;
font-size: 12px;
line-height: 1.5;
user-select: none;
}
#tfcc-panel.collapsed {
height: 48px !important;
max-height: 48px !important;
overflow: hidden;
}
#tfcc-panel.collapsed .tfcc-body,
#tfcc-panel.collapsed .tfcc-tabs {
display: none !important;
}
.tfcc-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: linear-gradient(90deg, #1e3a5f 0%, #2d5a87 100%);
border-bottom: 1px solid #3d6fa0;
cursor: grab;
}
.tfcc-header:active {
cursor: grabbing;
}
.tfcc-header-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 14px;
color: #ffffff;
}
.tfcc-header-icon {
width: 24px;
height: 24px;
background: linear-gradient(135deg, #4fc3f7 0%, #29b6f6 100%);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.tfcc-header-controls {
display: flex;
gap: 6px;
}
.tfcc-btn-icon {
width: 28px;
height: 28px;
border-radius: 6px;
border: none;
background: rgba(255,255,255,0.1);
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.15s ease;
}
.tfcc-btn-icon:hover {
background: rgba(255,255,255,0.2);
}
.tfcc-tabs {
display: flex;
background: #161822;
border-bottom: 1px solid #2d3142;
overflow-x: auto;
scrollbar-width: none;
}
.tfcc-tabs::-webkit-scrollbar {
display: none;
}
.tfcc-tab {
flex: 1;
min-width: 70px;
padding: 10px 8px;
border: none;
background: transparent;
color: #7a8199;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
border-bottom: 2px solid transparent;
white-space: nowrap;
text-align: center;
}
.tfcc-tab:hover {
color: #b0b8d4;
background: rgba(255,255,255,0.03);
}
.tfcc-tab.active {
color: #4fc3f7;
border-bottom-color: #4fc3f7;
background: rgba(79, 195, 247, 0.05);
}
.tfcc-body {
max-height: calc(80vh - 100px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #2d3142 transparent;
}
.tfcc-body::-webkit-scrollbar {
width: 6px;
}
.tfcc-body::-webkit-scrollbar-track {
background: transparent;
}
.tfcc-body::-webkit-scrollbar-thumb {
background: #2d3142;
border-radius: 3px;
}
.tfcc-section {
padding: 14px 16px;
border-bottom: 1px solid #1e2030;
}
.tfcc-section:last-child {
border-bottom: none;
}
.tfcc-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #5c7aea;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.tfcc-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #1a1c28;
}
.tfcc-row:last-child {
border-bottom: none;
}
.tfcc-label {
color: #7a8199;
font-size: 12px;
}
.tfcc-value {
color: #e2e5ec;
font-weight: 600;
font-size: 12px;
}
.tfcc-value.good { color: #2ecc71; }
.tfcc-value.warn { color: #f39c12; }
.tfcc-value.bad { color: #e74c3c; }
.tfcc-value.info { color: #4fc3f7; }
.tfcc-btn {
padding: 8px 14px;
border-radius: 6px;
border: none;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
font-family: inherit;
}
.tfcc-btn-primary {
background: linear-gradient(135deg, #4fc3f7 0%, #29b6f6 100%);
color: #0a1628;
}
.tfcc-btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.3);
}
.tfcc-btn-secondary {
background: #2d3142;
color: #c9cdd4;
}
.tfcc-btn-secondary:hover {
background: #3d4156;
}
.tfcc-btn-danger {
background: #e74c3c22;
color: #e74c3c;
border: 1px solid #e74c3c44;
}
.tfcc-btn-danger:hover {
background: #e74c3c33;
}
.tfcc-btn-success {
background: #2ecc7122;
color: #2ecc71;
border: 1px solid #2ecc7144;
}
.tfcc-btn-success:hover {
background: #2ecc7133;
}
.tfcc-input {
width: 100%;
padding: 8px 12px;
background: #0f111a;
border: 1px solid #2d3142;
border-radius: 6px;
color: #e2e5ec;
font-size: 12px;
font-family: inherit;
box-sizing: border-box;
transition: border-color 0.15s ease;
}
.tfcc-input:focus {
outline: none;
border-color: #4fc3f7;
}
.tfcc-input::placeholder {
color: #4a4f66;
}
.tfcc-toggle {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 0;
}
.tfcc-toggle-switch {
width: 36px;
height: 20px;
background: #2d3142;
border-radius: 10px;
position: relative;
cursor: pointer;
transition: background 0.2s ease;
flex-shrink: 0;
}
.tfcc-toggle-switch.active {
background: #4fc3f7;
}
.tfcc-toggle-switch::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: transform 0.2s ease;
}
.tfcc-toggle-switch.active::after {
transform: translateX(16px);
}
.tfcc-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
}
.tfcc-badge-ready { background: #2ecc7122; color: #2ecc71; }
.tfcc-badge-pending { background: #f39c1222; color: #f39c12; }
.tfcc-badge-empty { background: #e74c3c22; color: #e74c3c; }
.tfcc-badge-completed { background: #7f8c8d22; color: #7f8c8d; }
.tfcc-member-list {
max-height: 200px;
overflow-y: auto;
}
.tfcc-member-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
margin-bottom: 2px;
transition: background 0.1s ease;
}
.tfcc-member-item:hover {
background: rgba(255,255,255,0.03);
}
.tfcc-member-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.tfcc-status-online { background: #2ecc71; box-shadow: 0 0 4px #2ecc7188; }
.tfcc-status-idle { background: #f39c12; }
.tfcc-status-offline { background: #7f8c8d; }
.tfcc-progress-bar {
width: 100%;
height: 6px;
background: #1e2030;
border-radius: 3px;
overflow: hidden;
margin-top: 4px;
}
.tfcc-progress-fill {
height: 100%;
background: linear-gradient(90deg, #4fc3f7, #29b6f6);
border-radius: 3px;
transition: width 0.3s ease;
}
.tfcc-empty-state {
text-align: center;
padding: 30px 20px;
color: #4a4f66;
}
.tfcc-empty-state-icon {
font-size: 32px;
margin-bottom: 10px;
opacity: 0.5;
}
.tfcc-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
z-index: 100000;
display: flex;
align-items: center;
justify-content: center;
}
.tfcc-modal {
background: linear-gradient(145deg, #1a1d29 0%, #12141d 100%);
border: 1px solid #2d3142;
border-radius: 12px;
width: 420px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 16px 48px rgba(0,0,0,0.5);
}
.tfcc-modal-header {
padding: 16px 20px;
border-bottom: 1px solid #2d3142;
font-size: 16px;
font-weight: 700;
color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
}
.tfcc-modal-body {
padding: 20px;
}
.tfcc-modal-footer {
padding: 16px 20px;
border-top: 1px solid #2d3142;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.tfcc-form-group {
margin-bottom: 16px;
}
.tfcc-form-label {
display: block;
font-size: 12px;
font-weight: 600;
color: #9aa0b8;
margin-bottom: 6px;
}
.tfcc-form-hint {
font-size: 11px;
color: #4a4f66;
margin-top: 4px;
}
.tfcc-oc-card {
background: #161822;
border: 1px solid #232636;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
}
.tfcc-oc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.tfcc-oc-title {
font-weight: 700;
color: #e2e5ec;
font-size: 13px;
}
.tfcc-oc-slots {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 6px;
margin-top: 8px;
}
.tfcc-oc-slot {
padding: 6px;
border-radius: 4px;
text-align: center;
font-size: 10px;
font-weight: 600;
}
.tfcc-slot-filled { background: #2ecc7111; color: #2ecc71; border: 1px solid #2ecc7133; }
.tfcc-slot-empty { background: #e74c3c11; color: #e74c3c; border: 1px solid #e74c3c33; }
.tfcc-slot-player { background: #4fc3f711; color: #4fc3f7; border: 1px solid #4fc3f733; }
.tfcc-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30px;
gap: 8px;
color: #4a4f66;
}
.tfcc-spinner {
width: 20px;
height: 20px;
border: 2px solid #2d3142;
border-top-color: #4fc3f7;
border-radius: 50%;
animation: tfcc-spin 0.8s linear infinite;
}
@keyframes tfcc-spin {
to { transform: rotate(360deg); }
}
.tfcc-footer {
padding: 10px 16px;
background: #0f111a;
border-top: 1px solid #1e2030;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
color: #3a3f55;
}
.tfcc-footer a {
color: #4a4f66;
text-decoration: none;
}
.tfcc-footer a:hover {
color: #4fc3f7;
}
.tfcc-chain-timer {
font-size: 24px;
font-weight: 700;
font-family: 'Courier New', monospace;
color: #4fc3f7;
text-align: center;
padding: 10px;
background: #0f111a;
border-radius: 8px;
letter-spacing: 2px;
}
.tfcc-chain-timer.warning {
color: #f39c12;
animation: tfcc-pulse 1s ease infinite;
}
.tfcc-chain-timer.danger {
color: #e74c3c;
animation: tfcc-pulse 0.5s ease infinite;
}
@keyframes tfcc-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.tfcc-refresh-btn {
animation: none;
}
.tfcc-refresh-btn.spinning .tfcc-spinner-icon {
animation: tfcc-spin 1s linear infinite;
display: inline-block;
}
`);
}
// ============================ UI: PANEL ============================
function createPanel() {
const panel = document.createElement("div");
panel.id = "tfcc-panel";
if (state.collapsed) panel.classList.add("collapsed");
const savedX = GM_getValue(SETTINGS.panelX, CONFIG.defaultPosition.x);
const savedY = GM_getValue(SETTINGS.panelY, CONFIG.defaultPosition.y);
panel.style.left = savedX + "px";
panel.style.top = savedY + "px";
// Header
const header = document.createElement("div");
header.className = "tfcc-header";
header.innerHTML = `
<div class="tfcc-header-title">
<div class="tfcc-header-icon">⚔</div>
<span>Faction Command Center</span>
</div>
<div class="tfcc-header-controls">
<button class="tfcc-btn-icon tfcc-refresh-btn" id="tfcc-refresh" title="Refresh Data">↻</button>
<button class="tfcc-btn-icon" id="tfcc-settings" title="Settings">⚙</button>
<button class="tfcc-btn-icon" id="tfcc-toggle" title="Collapse/Expand">−</button>
</div>
`;
// Tabs
const tabs = document.createElement("div");
tabs.className = "tfcc-tabs";
tabs.innerHTML = `
<button class="tfcc-tab ${state.activeTab === "overview" ? "active" : ""}" data-tab="overview">Overview</button>
<button class="tfcc-tab ${state.activeTab === "oc" ? "active" : ""}" data-tab="oc">OC 2.0</button>
<button class="tfcc-tab ${state.activeTab === "war" ? "active" : ""}" data-tab="war">War</button>
<button class="tfcc-tab ${state.activeTab === "chain" ? "active" : ""}" data-tab="chain">Chain</button>
<button class="tfcc-tab ${state.activeTab === "members" ? "active" : ""}" data-tab="members">Members</button>
`;
// Body
const body = document.createElement("div");
body.className = "tfcc-body";
body.id = "tfcc-body";
// Footer
const footer = document.createElement("div");
footer.className = "tfcc-footer";
footer.innerHTML = `
<span>v${CONFIG.version}</span>
<span id="tfcc-last-refresh">Not loaded</span>
`;
panel.appendChild(header);
panel.appendChild(tabs);
panel.appendChild(body);
panel.appendChild(footer);
document.body.appendChild(panel);
state.panelEl = panel;
// Event listeners
setupDrag(header, panel);
setupTabs(tabs);
document.getElementById("tfcc-toggle").addEventListener("click", togglePanel);
document.getElementById("tfcc-settings").addEventListener("click", openSettings);
document.getElementById("tfcc-refresh").addEventListener("click", manualRefresh);
renderTab(state.activeTab);
}
function setupDrag(handle, panel) {
let isDragging = false;
let startX, startY, startLeft, startTop;
handle.addEventListener("mousedown", (e) => {
if (e.target.closest(".tfcc-btn-icon")) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = panel.offsetLeft;
startTop = panel.offsetTop;
panel.style.transition = "none";
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
panel.style.left = Math.max(0, startLeft + dx) + "px";
panel.style.top = Math.max(0, startTop + dy) + "px";
});
document.addEventListener("mouseup", () => {
if (!isDragging) return;
isDragging = false;
panel.style.transition = "";
GM_setValue(SETTINGS.panelX, panel.offsetLeft);
GM_setValue(SETTINGS.panelY, panel.offsetTop);
});
}
function togglePanel() {
state.collapsed = !state.collapsed;
state.panelEl.classList.toggle("collapsed", state.collapsed);
GM_setValue(SETTINGS.collapsed, state.collapsed);
document.querySelector("#tfcc-toggle").textContent = state.collapsed ? "+" : "−";
}
function setupTabs(container) {
container.addEventListener("click", (e) => {
if (!e.target.classList.contains("tfcc-tab")) return;
const tab = e.target.dataset.tab;
setActiveTab(tab);
});
}
function setActiveTab(tab) {
state.activeTab = tab;
GM_setValue(SETTINGS.activeTab, tab);
document.querySelectorAll(".tfcc-tab").forEach((t) => t.classList.toggle("active", t.dataset.tab === tab));
renderTab(tab);
}
// ============================ RENDER: TABS ============================
function renderTab(tab) {
const body = document.getElementById("tfcc-body");
if (!body) return;
switch (tab) {
case "overview":
renderOverview(body);
break;
case "oc":
renderOC(body);
break;
case "war":
renderWar(body);
break;
case "chain":
renderChain(body);
break;
case "members":
renderMembers(body);
break;
default:
renderOverview(body);
}
}
function renderOverview(container) {
const faction = state.factionData;
const pageInfo = detectCurrentPage();
const chainPage = readPageChainData();
let html = `
<div class="tfcc-section">
<div class="tfcc-section-title">📊 Faction Status</div>
`;
if (!faction || !faction.ID) {
html += `
<div class="tfcc-empty-state">
<div class="tfcc-empty-state-icon">🔑</div>
<div>No API key configured</div>
<div style="font-size: 11px; margin-top: 8px;">Click ⚙ to add your API key</div>
</div>
`;
} else {
html += `
<div class="tfcc-row">
<span class="tfcc-label">Faction</span>
<span class="tfcc-value">${faction.name || "Unknown"}</span>
</div>
<div class="tfcc-row">
<span class="tfcc-label">ID</span>
<span class="tfcc-value">${faction.ID}</span>
</div>
<div class="tfcc-row">
<span class="tfcc-label">Members</span>
<span class="tfcc-value info">${faction.members ? Object.keys(faction.members).length : "?"}</span>
</div>
<div class="tfcc-row">
<span class="tfcc-label">Respect</span>
<span class="tfcc-value">${faction.respect?.toLocaleString() || "?"}</span>
</div>
`;
if (faction.rank) {
html += `
<div class="tfcc-row">
<span class="tfcc-label">Rank</span>
<span class="tfcc-value">${faction.rank.name || faction.rank} (${faction.rank.level || "?"})</span>
</div>
`;
}
}
html += `</div>`;
// Quick stats from page
html += `
<div class="tfcc-section">
<div class="tfcc-section-title">📍 Current Page</div>
<div class="tfcc-row">
<span class="tfcc-label">Page Type</span>
<span class="tfcc-value">${getPageTypeLabel(pageInfo)}</span>
</div>
`;
if (chainPage.onPage) {
html += `
<div class="tfcc-row">
<span class="tfcc-label">Chain Status</span>
<span class="tfcc-value ${getChainStatusClass(chainPage)}">${chainPage.count || "Active"}</span>
</div>
`;
}
html += `</div>`;
// Quick Actions
html += `
<div class="tfcc-section">
<div class="tfcc-section-title">⚡ Quick Actions</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button class="tfcc-btn tfcc-btn-primary" id="tfcc-goto-faction">Faction Page</button>
<button class="tfcc-btn tfcc-btn-secondary" id="tfcc-goto-oc">OC Crimes</button>
<button class="tfcc-btn tfcc-btn-secondary" id="tfcc-goto-war">War Room</button>
<button class="tfcc-btn tfcc-btn-secondary" id="tfcc-goto-chain">Chain</button>
</div>
</div>
`;
// Discord quick-send
if (state.discordWebhook) {
html += `
<div class="tfcc-section">
<div class="tfcc-section-title">🔔 Discord</div>
<button class="tfcc-btn tfcc-btn-success" id="tfcc-discord-ping" style="width: 100%;">Send Faction Status to Discord</button>
</div>
`;
}
container.innerHTML = html;
// Bind quick action buttons
document.getElementById("tfcc-goto-faction")?.addEventListener("click", () => {
window.location.href = "https://www.torn.com/factions.php?step=your";
});
document.getElementById("tfcc-goto-oc")?.addEventListener("click", () => {
window.location.href = "https://www.torn.com/factions.php?step=your#/tab=crimes";
});
document.getElementById("tfcc-goto-war")?.addEventListener("click", () => {
window.location.href = "https://www.torn.com/factions.php?step=your#/tab=war";
});
document.getElementById("tfcc-goto-chain")?.addEventListener("click", () => {
window.location.href = "https://www.torn.com/factions.php?step=your#/tab=chains";
});
document.getElementById("tfcc-discord-ping")?.addEventListener("click", sendFactionStatusToDiscord);
}
function renderOC(container) {
const faction = state.factionData;
let html = `
<div class="tfcc-section">
<div class="tfcc-section-title">🎯 Organized Crimes 2.0</div>
`;
if (!faction || !faction.crimes) {
html += `
<div class="tfcc-empty-state">
<div class="tfcc-empty-state-icon">📋</div>
<div>No OC data available</div>
<div style="font-size: 11px; margin-top: 8px;">Configure API key and refresh</div>
</div>
`;
} else {
const crimes = faction.crimes;
const crimeEntries = Object.entries(crimes).filter(([k]) => !isNaN(k));
if (crimeEntries.length === 0) {
html += `<div class="tfcc-empty-state">No active organized crimes found</div>`;
} else {
crimeEntries.forEach(([id, crime]) => {
const slots = crime.slots || [];
const filledSlots = slots.filter((s) => s.user_id).length;
const totalSlots = slots.length;
const isReady = filledSlots === totalSlots && crime.ready_at && crime.ready_at <= Math.floor(Date.now() / 1000);
const isCompleted = crime.initiated_at && crime.completed_at;
const isPlanning = crime.ready_at && crime.ready_at > Math.floor(Date.now() / 1000);
let badgeClass = "tfcc-badge-pending";
let badgeText = "PLANNING";
if (isCompleted) {
badgeClass = "tfcc-badge-completed";
badgeText = "DONE";
} else if (isReady) {
badgeClass = "tfcc-badge-ready";
badgeText = "READY";
} else if (filledSlots < totalSlots) {
badgeClass = "tfcc-badge-empty";
badgeText = `${filledSlots}/${totalSlots}`;
}
html += `
<div class="tfcc-oc-card">
<div class="tfcc-oc-header">
<span class="tfcc-oc-title">${crime.name || `Crime #${id}`}</span>
<span class="tfcc-badge ${badgeClass}">${badgeText}</span>
</div>
<div style="font-size: 11px; color: #7a8199;">
${crime.difficulty ? `Difficulty: ${crime.difficulty}` : ""}
${crime.ready_at ? `| Ready: ${formatTimestamp(crime.ready_at)}` : ""}
</div>
<div class="tfcc-oc-slots">
`;
if (slots.length > 0) {
slots.forEach((slot, idx) => {
if (slot.user_id) {
const isPlayer = slot.user_id === faction.player_id;
html += `<div class="tfcc-oc-slot ${isPlayer ? "tfcc-slot-player" : "tfcc-slot-filled"}">${slot.user_name || "Member"}</div>`;
} else {
html += `<div class="tfcc-oc-slot tfcc-slot-empty">Slot ${idx + 1}</div>`;
}
});
}
html += `
</div>
</div>
`;
});
}
}
html += `</div>`;
// CPR Reference
html += `
<div class="tfcc-section">
<div class="tfcc-section-title">📚 CPR Quick Reference</div>
<div style="font-size: 11px; color: #7a8199; line-height: 1.8;">
<div><strong style="color: #e2e5ec;">Political Assassination:</strong> 100+ each stat</div>
<div><strong style="color: #e2e5ec;">Plane Hijacking:</strong> 80+ each stat</div>
<div><strong style="color: #e2e5ec;">Train Robbery:</strong> 60+ each stat</div>
<div><strong style="color: #e2e5ec;">Bank Robbery:</strong> 50+ each stat</div>
<div><strong style="color: #e2e5ec;">Armored Truck:</strong> 40+ each stat</div>
<div><strong style="color: #e2e5ec;">Kidnapping:</strong> 30+ each stat</div>
<div style="margin-top: 6px; color: #4a4f66; font-size: 10px;">
Note: CPR requirements may vary. Visit tornprobability.com for exact values.
</div>
</div>
</div>
`;
// Manual Discord notify
if (state.discordWebhook) {
html += `
<div class="tfcc-section">
<button class="tfcc-btn tfcc-btn-success" id="tfcc-discord-oc" style="width: 100%;">Send OC Status to Discord</button>
</div>
`;
}
container.innerHTML = html;
document.getElementById("tfcc-discord-oc")?.addEventListener("click", sendOCStatusToDiscord);
}
function renderWar(container) {
const pageData = readPageWarData();
const faction = state.factionData;
let html = `
<div class="tfcc-section">
<div class="tfcc-section-title">⚔ War Intelligence</div>
`;
html += `
<div class="tfcc-row">
<span class="tfcc-label">On War Page</span>
<span class="tfcc-value ${pageData.onWarPage ? "good" : ""}">${pageData.onWarPage ? "YES" : "NO"}</span>
</div>
`;
if (pageData.onWarPage) {
html += `
<div class="tfcc-row">
<span class="tfcc-label">Visible Enemies</span>
<span class="tfcc-value warn">${pageData.enemyCount}</span>
</div>
`;
if (pageData.hospitalTimers.length > 0) {
html += `<div class="tfcc-section-title" style="margin-top: 12px;">🏥 Hospital Timers</div>`;
html += `<div style="max-height: 150px; overflow-y: auto;">`;
pageData.hospitalTimers.forEach((timer) => {
html += `
<div class="tfcc-row">
<span class="tfcc-label" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${timer.target}</span>
<span class="tfcc-value info">${timer.text}</span>
</div>
`;
});
html += `</div>`;
}
} else {
html += `
<div class="tfcc-empty-state">
<div class="tfcc-empty-state-icon">🛡</div>
<div>Navigate to the war page to see live intelligence</div>
</div>
`;
}
html += `</div>`;
// Ranked War Info
if (faction && faction.ranked_wars) {
html += `
<div class="tfcc-section">
<div class="tfcc-section-title">🏆 Ranked Wars</div>
`;
const rwEntries = Object.entries(faction.ranked_wars);
if (rwEntries.length === 0) {
html += `<div style="color: #4a4f66; font-size: 11px;">No active ranked wars</div>`;
} else {
rwEntries.forEach(([id, rw]) => {
html += `
<div class="tfcc-oc-card">
<div class="tfcc-oc-title">War #${id}</div>
<div style="font-size: 11px; color: #7a8199; margin-top: 4px;">
Start: ${formatTimestamp(rw.started)} | End: ${formatTimestamp(rw.ends)}
</div>
<div style="margin-top: 6px;">
<div class="tfcc-progress-bar">
<div class="tfcc-progress-fill" style="width: ${getWarProgress(rw)}%"></div>
</div>
</div>
</div>
`;
});
}
html += `</div>`;
}
// Manual Discord notify
if (state.discordWebhook && pageData.onWarPage) {
html += `
<div class="tfcc-section">
<button class="tfcc-btn tfcc-btn-success" id="tfcc-discord-war" style="width: 100%;">Send War Intel to Discord</button>
</div>
`;
}
container.innerHTML = html;
document.getElementById("tfcc-discord-war")?.addEventListener("click", sendWarStatusToDiscord);
}
function renderChain(container) {
const chainPage = readPageChainData();
const faction = state.factionData;
let html = `
<div class="tfcc-section">
<div class="tfcc-section-title">⛓ Chain Monitor</div>
`;
if (chainPage.onPage) {
const isWarning = chainPage.timer && parseTime(chainPage.timer) < 30 && parseTime(chainPage.timer) > 0;
const isDanger = chainPage.timer && parseTime(chainPage.timer) < 10;
html += `
<div class="tfcc-chain-timer ${isDanger ? "danger" : isWarning ? "warning" : ""}">
${chainPage.timer || "--:--"}
</div>
<div style="text-align: center; margin-top: 8px; font-size: 13px;">
<span class="tfcc-value ${isWarning ? "warn" : "good"}">Chain: ${chainPage.count || "?"}${chainPage.max ? ` / ${chainPage.max}` : ""}</span>
</div>
`;
if (isWarning) {
html += `
<div style="background: #f39c1222; border: 1px solid #f39c1244; border-radius: 6px; padding: 8px; margin-top: 10px; text-align: center; font-size: 11px; color: #f39c12; font-weight: 600;">
⚠ Chain timer running low!
</div>
`;
}
if (isDanger) {
html += `
<div style="background: #e74c3c22; border: 1px solid #e74c3c44; border-radius: 6px; padding: 8px; margin-top: 8px; text-align: center; font-size: 11px; color: #e74c3c; font-weight: 700;">
🚨 CRITICAL — Chain about to break!
</div>
`;
}
} else {
html += `
<div class="tfcc-empty-state">
<div class="tfcc-empty-state-icon">⛓</div>
<div>Navigate to the chain page to see live data</div>
</div>
`;
}
html += `</div>`;
// Chain history from API
if (faction && faction.chain) {
html += `
<div class="tfcc-section">
<div class="tfcc-section-title">📈 Chain History</div>
<div class="tfcc-row">
<span class="tfcc-label">Current</span>
<span class="tfcc-value">${faction.chain.current || 0}</span>
</div>
<div class="tfcc-row">
<span class="tfcc-label">Maximum</span>
<span class="tfcc-value">${faction.chain.max || 0}</span>
</div>
<div class="tfcc-row">
<span class="tfcc-label">Cooldown</span>
<span class="tfcc-value">${faction.chain.cooldown || 0}s</span>
</div>
</div>
`;
}
// Discord notify
if (state.discordWebhook && chainPage.onPage) {
html += `
<div class="tfcc-section">
<button class="tfcc-btn tfcc-btn-success" id="tfcc-discord-chain" style="width: 100%;">Send Chain Alert to Discord</button>
</div>
`;
}
container.innerHTML = html;
document.getElementById("tfcc-discord-chain")?.addEventListener("click", sendChainAlertToDiscord);
}
function renderMembers(container) {
const faction = state.factionData;
let html = `
<div class="tfcc-section">
<div class="tfcc-section-title">👥 Faction Members</div>
`;
if (!faction || !faction.members) {
html += `
<div class="tfcc-empty-state">
<div class="tfcc-empty-state-icon">👤</div>
<div>No member data available</div>
</div>
`;
} else {
const members = Object.entries(faction.members);
html += `<div style="font-size: 11px; color: #7a8199; margin-bottom: 10px;">${members.length} members</div>`;
html += `<div class="tfcc-member-list">`;
// Sort by level descending
members.sort((a, b) => (b[1].level || 0) - (a[1].level || 0));
members.forEach(([id, member]) => {
let statusClass = "tfcc-status-offline";
if (member.last_action) {
if (member.last_action.status === "Online") statusClass = "tfcc-status-online";
else if (member.last_action.status === "Idle") statusClass = "tfcc-status-idle";
}
html += `
<div class="tfcc-member-item">
<div class="tfcc-member-status ${statusClass}"></div>
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 600; color: #e2e5ec; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${member.name || "Unknown"}
</div>
<div style="font-size: 10px; color: #4a4f66;">
Lvl ${member.level || "?"} | ${member.last_action?.relative || "Unknown status"}
</div>
</div>
<div style="text-align: right; flex-shrink: 0;">
<div style="font-size: 10px; color: #7a8199;">${formatNumber(member.stats?.strength)} STR</div>
<div style="font-size: 10px; color: #7a8199;">${formatNumber(member.stats?.speed)} SPD</div>
</div>
</div>
`;
});
html += `</div>`;
}
html += `</div>`;
container.innerHTML = html;
}
// ============================ SETTINGS MODAL ============================
function openSettings() {
const existing = document.getElementById("tfcc-settings-modal");
if (existing) existing.remove();
const overlay = document.createElement("div");
overlay.className = "tfcc-modal-overlay";
overlay.id = "tfcc-settings-modal";
overlay.innerHTML = `
<div class="tfcc-modal">
<div class="tfcc-modal-header">
<span>⚙ Settings</span>
<button class="tfcc-btn-icon" id="tfcc-close-settings" style="color: #7a8199;">✕</button>
</div>
<div class="tfcc-modal-body">
<div class="tfcc-form-group">
<label class="tfcc-form-label">Torn API Key</label>
<input type="password" class="tfcc-input" id="tfcc-api-key" placeholder="Paste your Limited Access API key" value="${state.apiKey}">
<div class="tfcc-form-hint">Requires Limited access with 'faction' permissions. Key is stored only in your browser.</div>
</div>
<div class="tfcc-form-group">
<label class="tfcc-form-label">Discord Webhook URL (Optional)</label>
<input type="password" class="tfcc-input" id="tfcc-discord-url" placeholder="https://discord.com/api/webhooks/..." value="${state.discordWebhook}">
<div class="tfcc-form-hint">For sending faction alerts to Discord. Leave empty to disable. <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" target="_blank" style="color: #4fc3f7;">How to create a webhook</a></div>
</div>
<div style="border-top: 1px solid #2d3142; margin: 16px 0; padding-top: 16px;">
<div class="tfcc-form-label" style="margin-bottom: 12px;">Feature Toggles</div>
<div class="tfcc-toggle">
<div class="tfcc-toggle-switch ${GM_getValue(SETTINGS.enableOC, true) ? "active" : ""}" data-setting="${SETTINGS.enableOC}"></div>
<span>OC 2.0 Management</span>
</div>
<div class="tfcc-toggle">
<div class="tfcc-toggle-switch ${GM_getValue(SETTINGS.enableWar, true) ? "active" : ""}" data-setting="${SETTINGS.enableWar}"></div>
<span>War Intelligence</span>
</div>
<div class="tfcc-toggle">
<div class="tfcc-toggle-switch ${GM_getValue(SETTINGS.enableChain, true) ? "active" : ""}" data-setting="${SETTINGS.enableChain}"></div>
<span>Chain Monitor</span>
</div>
<div class="tfcc-toggle">
<div class="tfcc-toggle-switch ${GM_getValue(SETTINGS.enableMembers, true) ? "active" : ""}" data-setting="${SETTINGS.enableMembers}"></div>
<span>Member Activity</span>
</div>
<div class="tfcc-toggle">
<div class="tfcc-toggle-switch ${GM_getValue(SETTINGS.enableDiscord, true) ? "active" : ""}" data-setting="${SETTINGS.enableDiscord}"></div>
<span>Discord Integration</span>
</div>
</div>
<div style="border-top: 1px solid #2d3142; margin-top: 16px; padding-top: 16px;">
<div class="tfcc-form-label">Data & Privacy</div>
<div style="font-size: 11px; color: #7a8199; line-height: 1.8; margin-top: 8px;">
<div>📦 <strong style="color: #e2e5ec;">Data Storage:</strong> Only locally in your browser</div>
<div>🔒 <strong style="color: #e2e5ec;">Data Sharing:</strong> Nobody — stays on your device</div>
<div>🎯 <strong style="color: #e2e5ec;">Purpose:</strong> Faction coordination & OC planning</div>
<div>🔑 <strong style="color: #e2e5ec;">API Key:</strong> Stored locally, never shared</div>
<div>📡 <strong style="color: #e2e5ec;">Discord:</strong> Only sends when you click a button</div>
</div>
</div>
</div>
<div class="tfcc-modal-footer">
<button class="tfcc-btn tfcc-btn-secondary" id="tfcc-cancel-settings">Cancel</button>
<button class="tfcc-btn tfcc-btn-primary" id="tfcc-save-settings">Save Settings</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Toggle switches
overlay.querySelectorAll(".tfcc-toggle-switch").forEach((sw) => {
sw.addEventListener("click", () => {
sw.classList.toggle("active");
});
});
document.getElementById("tfcc-close-settings").addEventListener("click", () => overlay.remove());
document.getElementById("tfcc-cancel-settings").addEventListener("click", () => overlay.remove());
document.getElementById("tfcc-save-settings").addEventListener("click", () => {
const newKey = document.getElementById("tfcc-api-key").value.trim();
const newWebhook = document.getElementById("tfcc-discord-url").value.trim();
// Save API key
if (newKey) {
GM_setValue(SETTINGS.apiKey, newKey);
state.apiKey = newKey;
}
// Save webhook
GM_setValue(SETTINGS.discordWebhook, newWebhook);
state.discordWebhook = newWebhook;
// Save toggles
overlay.querySelectorAll(".tfcc-toggle-switch").forEach((sw) => {
const settingKey = sw.dataset.setting;
GM_setValue(settingKey, sw.classList.contains("active"));
});
overlay.remove();
showNotification("Settings saved! Refreshing data...", "success");
refreshAllData();
});
}
// ============================ DISCORD SENDERS ============================
function sendFactionStatusToDiscord() {
const faction = state.factionData;
if (!faction) {
showNotification("No faction data to send", "error");
return;
}
const memberCount = faction.members ? Object.keys(faction.members).length : "?";
const embed = {
title: `⚔️ ${faction.name || "Faction"} Status`,
color: 0x4fc3f7,
fields: [
{ name: "Members", value: String(memberCount), inline: true },
{ name: "Respect", value: faction.respect?.toLocaleString() || "?", inline: true },
{ name: "Rank", value: faction.rank?.name || "?", inline: true },
],
timestamp: new Date().toISOString(),
};
sendDiscordMessage("📊 Faction status update:", [embed]);
}
function sendOCStatusToDiscord() {
const faction = state.factionData;
if (!faction || !faction.crimes) {
showNotification("No OC data to send", "error");
return;
}
const crimes = Object.entries(faction.crimes).filter(([k]) => !isNaN(k));
const readyCrimes = crimes.filter(([_, c]) => {
const slots = c.slots || [];
const filled = slots.filter((s) => s.user_id).length;
return filled === slots.length && c.ready_at && c.ready_at <= Math.floor(Date.now() / 1000);
});
const embed = {
title: "🎯 OC 2.0 Status",
color: readyCrimes.length > 0 ? 0x2ecc71 : 0xf39c12,
fields: [
{ name: "Active OCs", value: String(crimes.length), inline: true },
{ name: "Ready to Start", value: String(readyCrimes.length), inline: true },
],
timestamp: new Date().toISOString(),
};
sendDiscordMessage("🎯 OC status update:", [embed]);
}
function sendWarStatusToDiscord() {
const pageData = readPageWarData();
const embed = {
title: "⚔️ War Intelligence",
color: 0xe74c3c,
fields: [
{ name: "Enemies Visible", value: String(pageData.enemyCount), inline: true },
{ name: "Page", value: window.location.pathname, inline: true },
],
timestamp: new Date().toISOString(),
};
sendDiscordMessage("⚔️ War intel update:", [embed]);
}
function sendChainAlertToDiscord() {
const chainPage = readPageChainData();
const isWarning = chainPage.timer && parseTime(chainPage.timer) < 30;
const embed = {
title: isWarning ? "🚨 CHAIN ALERT" : "⛓ Chain Status",
color: isWarning ? 0xe74c3c : 0x4fc3f7,
fields: [
{ name: "Timer", value: chainPage.timer || "?", inline: true },
{ name: "Count", value: chainPage.count || "?", inline: true },
],
timestamp: new Date().toISOString(),
};
sendDiscordMessage(isWarning ? "🚨 **CHAIN TIMER LOW!**" : "⛓ Chain update:", [embed]);
}
// ============================ DATA REFRESH ============================
function refreshAllData() {
if (state.isLoading) return;
state.isLoading = true;
const refreshBtn = document.getElementById("tfcc-refresh");
if (refreshBtn) refreshBtn.classList.add("spinning");
fetchFactionData(() => {
state.isLoading = false;
if (refreshBtn) refreshBtn.classList.remove("spinning");
const now = new Date();
const timeStr = now.toLocaleTimeString();
const lastRefreshEl = document.getElementById("tfcc-last-refresh");
if (lastRefreshEl) lastRefreshEl.textContent = "Updated: " + timeStr;
renderTab(state.activeTab);
});
}
function manualRefresh() {
refreshAllData();
showNotification("Refreshing faction data...", "info");
}
function startAutoRefresh() {
refreshAllData();
setInterval(() => {
if (!state.collapsed && getApiKey()) {
refreshAllData();
}
}, CONFIG.refreshInterval);
}
// ============================ HELPERS ============================
function formatTimestamp(ts) {
if (!ts) return "?";
const date = new Date(ts * 1000);
const now = Date.now();
const diff = date - now;
if (diff < 0) return "Ready now";
const hours = Math.floor(diff / 3600000);
const mins = Math.floor((diff % 3600000) / 60000);
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
}
function parseTime(timeStr) {
if (!timeStr) return Infinity;
const parts = timeStr.split(":").map(Number);
if (parts.length === 2) return parts[0] * 60 + parts[1];
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
return Infinity;
}
function getPageTypeLabel(info) {
if (info.isFaction) return "Faction";
if (info.isWar) return "War";
if (info.isOC) return "Crimes";
if (info.isChain) return "Chain";
if (info.isBazaar) return "Bazaar";
if (info.isProfile) return "Profile";
return "Other";
}
function getChainStatusClass(chainPage) {
if (!chainPage.count) return "";
const count = parseInt(chainPage.count);
if (count >= 100) return "good";
if (count >= 10) return "warn";
return "bad";
}
function getWarProgress(rw) {
if (!rw.started || !rw.ends) return 0;
const total = rw.ends - rw.started;
const elapsed = Math.floor(Date.now() / 1000) - rw.started;
return Math.min(100, Math.max(0, (elapsed / total) * 100));
}
function formatNumber(num) {
if (!num) return "?";
if (num >= 1000000000) return (num / 1000000000).toFixed(1) + "B";
if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
if (num >= 1000) return (num / 1000).toFixed(1) + "K";
return String(num);
}
// ============================ INITIALIZATION ============================
function init() {
loadSettings();
injectStyles();
createPanel();
if (getApiKey()) {
startAutoRefresh();
}
// Register Tampermonkey menu commands
if (typeof GM_registerMenuCommand !== "undefined") {
GM_registerMenuCommand("⚙ TFCC Settings", openSettings);
GM_registerMenuCommand("↻ Refresh Data", manualRefresh);
}
console.log(`[TFCC] Faction Command Center v${CONFIG.version} loaded${isTornPDA ? " (TornPDA mode)" : ""}`);
}
// Wait for page to be ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();