Torn API User Profile

Displays bars, cooldowns, and battle stats (with total) for specific users on their Torn profile. PDA friendly.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Torn API User Profile
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Displays bars, cooldowns, and battle stats (with total) for specific users on their Torn profile. PDA friendly.
// @author       Primordial
// @match        https://www.torn.com/profiles.php*
// @grant        GM_addStyle
// @license     MIT
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // CONFIGURATION: Add your special users here
    // ==========================================
    const specialUsers = [
        { user: 1, apiKey: "n/a" },
        { user: 1, apiKey: "n/a" }
    ];
    // ==========================================
    // LOGIC & EXECUTION
    // ==========================================

    const urlParams = new URLSearchParams(window.location.search);
    const targetId = parseInt(urlParams.get('XID'));

    if (!targetId) return;

    const userData = specialUsers.find(u => u.user === targetId);
    if (!userData) return;

    const observer = new MutationObserver((mutations, obs) => {
        const profileWrapper = document.querySelector('.profile-wrapper');
        if (profileWrapper && !document.getElementById('custom-stats-panel')) {
            obs.disconnect();
            fetchAndRenderData(userData.apiKey, profileWrapper);
        }
    });

    observer.observe(document, { childList: true, subtree: true });

    // ==========================================
    // FUNCTIONS
    // ==========================================

    async function fetchAndRenderData(apiKey, container) {
        try {
            const response = await fetch(`https://api.torn.com/user/?selections=bars,cooldowns,battlestats&key=${apiKey}`);
            const data = await response.json();

            if (data.error) {
                console.error("Torn API Error:", data.error);
                return;
            }

            injectUI(data, container);
        } catch (error) {
            console.error("Failed to fetch special user data:", error);
        }
    }

    function formatTime(seconds) {
        if (seconds <= 0) return "00:00:00";
        const h = Math.floor(seconds / 3600).toString().padStart(2, '0');
        const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
        const s = (seconds % 60).toString().padStart(2, '0');
        return `${h}:${m}:${s}`;
    }

    function injectUI(data, profileWrapper) {
        const statsPanel = document.createElement('div');
        statsPanel.id = 'custom-stats-panel';
        statsPanel.className = 'profile-container mt10';
        statsPanel.style.marginBottom = '10px';

        // Destructure API data
        const { energy, nerve, happy, life } = data;
        const cd = data.cooldowns;
        const bs = data;

        // Calculate Total Battle Stats
        const totalStats = (bs.strength || 0) + (bs.speed || 0) + (bs.dexterity || 0) + (bs.defense || 0);

        // Responsive grid handles Torn PDA's narrow mobile viewport automatically
        statsPanel.innerHTML = `
            <div class="title-black top-round" style="display: flex; justify-content: space-between; align-items: center; padding: 0 10px; cursor: pointer;" id="custom-stats-header">
                <div class="text">Live Status & Stats</div>
                <div class="options"></div>
                <svg id="custom-stats-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" width="1em" height="1em" class="icon" style="transition: transform 0.2s;">
                    <path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path>
                </svg>
            </div>
            <div id="custom-stats-content" class="bottom-round cont-gray" style="display: block; padding: 10px; background-color: var(--default-bg-panel-color); color: var(--default-color);">
                <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 15px;">
                    <!-- Bars -->
                    <div>
                        <h4 style="border-bottom: 1px solid var(--default-panel-divider-color, #444); margin-bottom: 5px; padding-bottom: 3px;">Bars</h4>
                        <div><strong>Energy:</strong> ${energy.current} / ${energy.maximum}</div>
                        <div><strong>Nerve:</strong> ${nerve.current} / ${nerve.maximum}</div>
                        <div><strong>Happy:</strong> ${happy.current} / ${happy.maximum}</div>
                        <div><strong>Life:</strong> ${life.current} / ${life.maximum}</div>
                    </div>

                    <!-- Cooldowns -->
                    <div>
                        <h4 style="border-bottom: 1px solid var(--default-panel-divider-color, #444); margin-bottom: 5px; padding-bottom: 3px;">Cooldowns</h4>
                        <div><strong>Medical:</strong> ${formatTime(cd.medical)}</div>
                        <div><strong>Drug:</strong> ${formatTime(cd.drug)}</div>
                        <div><strong>Booster:</strong> ${formatTime(cd.booster)}</div>
                    </div>

                    <!-- Battle Stats -->
                    <div>
                        <h4 style="border-bottom: 1px solid var(--default-panel-divider-color, #444); margin-bottom: 5px; padding-bottom: 3px;">Battle Stats</h4>
                        <div><strong>Strength:</strong> ${bs.strength ? bs.strength.toLocaleString() : 'N/A'}</div>
                        <div><strong>Speed:</strong> ${bs.speed ? bs.speed.toLocaleString() : 'N/A'}</div>
                        <div><strong>Dexterity:</strong> ${bs.dexterity ? bs.dexterity.toLocaleString() : 'N/A'}</div>
                        <div><strong>Defense:</strong> ${bs.defense ? bs.defense.toLocaleString() : 'N/A'}</div>
                        <div style="margin-top: 5px; padding-top: 5px; border-top: 1px dashed var(--default-panel-divider-color, #444);">
                            <strong>Total:</strong> ${totalStats > 0 ? totalStats.toLocaleString() : 'N/A'}
                        </div>
                    </div>
                </div>
            </div>
        `;

        profileWrapper.insertBefore(statsPanel, profileWrapper.firstChild);

        // Collapsible Logic
        const header = document.getElementById('custom-stats-header');
        const content = document.getElementById('custom-stats-content');
        const icon = document.getElementById('custom-stats-icon');

        header.addEventListener('click', () => {
            if (content.style.display === 'none') {
                content.style.display = 'block';
                icon.style.transform = 'rotate(0deg)';
            } else {
                content.style.display = 'none';
                icon.style.transform = 'rotate(180deg)';
            }
        });
    }

})();