Torn Faction Summaries

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.

当前为 2025-03-16 提交的版本,查看 最新版本

// ==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);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址