Torn Attack Log API Data

Shows API data for attacks on attack log pages

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Attack Log API Data
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Shows API data for attacks on attack log pages
// @author       Your Name
// @match        https://www.torn.com/loader.php?sid=attackLog*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      api.torn.com
// ==/UserScript==

(function() {
    'use strict';

    let API_KEY = GM_getValue('tornApiKey', '');

    if (!API_KEY) {
        const key = prompt('Enter your Torn API key for Attack Log Data script:');
        if (key && key.length === 16) {
            GM_setValue('tornApiKey', key);
            API_KEY = key;
        } else {
            alert('Invalid API key. Please refresh and try again.');
            return;
        }
    }

    const urlParams = new URLSearchParams(window.location.search);
    const attackCode = urlParams.get('ID');

    if (!attackCode) return;

    const observer = new MutationObserver((mutations, obs) => {
        const logInfoWrap = document.querySelector('.log-info-wrap');
        const actionLog = document.querySelector('.action-log');
        if (logInfoWrap && actionLog) {
            obs.disconnect();
            init();
        }
    });

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

    setTimeout(() => {
        const logInfoWrap = document.querySelector('.log-info-wrap');
        if (logInfoWrap) {
            observer.disconnect();
            init();
        }
    }, 500);

    function init() {
        // Get participant IDs from the page
        const participantLinks = document.querySelectorAll('.players-in-attack .participants-list a[href*="profiles.php"]');
        const participantIds = new Set();
        participantLinks.forEach(link => {
            const match = link.href.match(/XID=(\d+)/);
            if (match) participantIds.add(parseInt(match[1]));
        });

        // Get timestamp
        const logOptionsList = document.querySelector('.log-options .participants-list');
        if (!logOptionsList) return;

        const items = logOptionsList.querySelectorAll('li');
        if (items.length < 2) return;

        const timestampText = items[1].textContent.trim();
        const timestamp = parseTimestamp(timestampText);

        if (!timestamp) return;

        const fromTime = timestamp - 60;
        const toTime = timestamp + 60;

        fetchAttackData(fromTime, toTime, attackCode, 'faction', participantIds);
    }

    function parseTimestamp(text) {
        const match = text.match(/(\d{1,2}):(\d{2}):(\d{2})\s+(\d{1,2})\/(\d{1,2})\/(\d{2,4})/);
        if (!match) return null;

        let [, hours, minutes, seconds, day, month, year] = match.map(Number);
        if (year < 100) year = 2000 + year;

        const date = Date.UTC(year, month - 1, day, hours, minutes, seconds);
        return Math.floor(date / 1000);
    }

    function fetchAttackData(from, to, code, source, participantIds) {
        const baseUrl = source === 'faction'
            ? `https://api.torn.com/faction/?key=${API_KEY}&from=${from}&to=${to}&selections=attacks`
            : `https://api.torn.com/user/?key=${API_KEY}&from=${from}&to=${to}&selections=attacks`;

        showLoading();

        GM_xmlhttpRequest({
            method: 'GET',
            url: baseUrl,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);

                    if (data.error) {
                        if (source === 'faction' && (data.error.code === 7 || data.error.code === 15)) {
                            fetchAttackData(from, to, code, 'user', participantIds);
                            return;
                        }
                        displayError(data.error.error);
                        if (data.error.code === 2) {
                            GM_setValue('tornApiKey', '');
                        }
                        return;
                    }

                    const attacks = data.attacks || {};

                    // Find the main attack by code
                    let mainAttack = null;
                    for (const [id, attack] of Object.entries(attacks)) {
                        if (attack.code === code) {
                            mainAttack = { id, ...attack };
                            break;
                        }
                    }

                    if (!mainAttack && source === 'faction') {
                        fetchAttackData(from, to, code, 'user', participantIds);
                        return;
                    }

                    // Find all attacks that are part of this fight
                    // Based on: participant IDs AND similar end timestamp (within 5 seconds of main attack)
                    const fightAttacks = [];
                    const mainEndTime = mainAttack ? mainAttack.timestamp_ended : null;

                    for (const [id, attack] of Object.entries(attacks)) {
                        const isParticipant = participantIds.has(attack.attacker_id) || participantIds.has(attack.defender_id);
                        const isSameTimeframe = mainEndTime
                            ? Math.abs(attack.timestamp_ended - mainEndTime) <= 5
                            : true;

                        if (isParticipant && isSameTimeframe) {
                            fightAttacks.push({ id, ...attack, isMain: attack.code === code });
                        }
                    }

                    // Sort by timestamp
                    fightAttacks.sort((a, b) => a.timestamp_started - b.timestamp_started);

                    displayData(fightAttacks);
                } catch (e) {
                    if (source === 'faction') {
                        fetchAttackData(from, to, code, 'user', participantIds);
                    } else {
                        displayError('Parse error');
                    }
                }
            },
            onerror: function() {
                if (source === 'faction') {
                    fetchAttackData(from, to, code, 'user', participantIds);
                } else {
                    displayError('Request failed');
                }
            }
        });
    }

    function showLoading() {
        const container = createContainer();
        container.innerHTML = `
            <div class="viewport">
                <ul class="participants-list overview">
                    <li style="color: #666; text-align: center;">Loading...</li>
                </ul>
            </div>
        `;
        insertContainer(container);
    }

    function displayError(message) {
        const container = createContainer();
        container.innerHTML = `
            <div class="viewport">
                <ul class="participants-list overview">
                    <li style="color: #c44; font-size: 11px;">${message}</li>
                    <li><a href="#" id="reset-api-key-link" style="color: #69c; font-size: 10px;">Reset API Key</a></li>
                </ul>
            </div>
        `;
        insertContainer(container);

        document.getElementById('reset-api-key-link')?.addEventListener('click', (e) => {
            e.preventDefault();
            GM_setValue('tornApiKey', '');
            location.reload();
        });
    }

    function displayData(attacks) {
        const container = createContainer();

        if (!attacks || attacks.length === 0) {
            container.innerHTML = `
                <div class="viewport">
                    <ul class="participants-list overview">
                        <li style="color: #666; font-size: 11px;">No API data</li>
                    </ul>
                </div>
            `;
            insertContainer(container);
            return;
        }

        let html = `
            <style>
                #attack-api-box .attack-entry {
                    padding: 6px 8px;
                    border-bottom: 1px solid #333;
                    font-size: 11px;
                }
                #attack-api-box .attack-entry:last-child {
                    border-bottom: none;
                }
                #attack-api-box .attack-entry.main {
                    background: rgba(100, 200, 100, 0.1);
                }
                #attack-api-box .attack-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 4px;
                }
                #attack-api-box .attack-names {
                    color: #aaa;
                }
                #attack-api-box .attack-names a {
                    color: #69c;
                    text-decoration: none;
                }
                #attack-api-box .attack-arrow {
                    color: #555;
                    margin: 0 4px;
                }
                #attack-api-box .attack-result {
                    font-weight: bold;
                    font-size: 10px;
                }
                #attack-api-box .attack-stats {
                    display: flex;
                    gap: 8px;
                    color: #777;
                    font-size: 10px;
                }
                #attack-api-box .attack-tags {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 3px;
                    margin-top: 4px;
                }
                #attack-api-box .tag {
                    font-size: 9px;
                    padding: 1px 4px;
                    border-radius: 2px;
                    background: #333;
                }
                #attack-api-box .footer {
                    padding: 6px 8px;
                    font-size: 10px;
                    color: #555;
                    display: flex;
                    justify-content: space-between;
                    border-top: 1px solid #333;
                }
            </style>
            <div class="viewport">
        `;

        for (const attack of attacks) {
            const m = attack.modifiers || {};

            // Result color
            const resultColors = {
                'Hospitalized': '#ef4444',
                'Attacked': '#22c55e',
                'Mugged': '#f97316',
                'Assist': '#3b82f6',
                'Lost': '#ec4899',
                'Stalemate': '#6b7280',
                'Escaped': '#a855f7'
            };
            const resultColor = resultColors[attack.result] || '#999';

            // Build tags
            const tags = [];
            if (attack.stealthed) tags.push({ label: 'Stealth', color: '#a855f7' });
            if (attack.raid) tags.push({ label: 'Raid', color: '#ef4444' });
            if (attack.ranked_war) tags.push({ label: 'RW', color: '#f97316' });
            if (m.retaliation && m.retaliation !== 1) tags.push({ label: 'Retal', color: '#eab308' });
            if (m.group_attack && m.group_attack !== 1) tags.push({ label: 'Group', color: '#3b82f6' });
            if (m.overseas && m.overseas !== 1) tags.push({ label: 'Overseas', color: '#22c55e' });
            if (m.chain_bonus && m.chain_bonus !== 1) tags.push({ label: `Chain ${m.chain_bonus.toFixed(1)}x`, color: '#ec4899' });

            // Fair fight
            const ff = m.fair_fight || 1;
            const ffColor = ff >= 2.5 ? '#22c55e' : ff >= 1.5 ? '#eab308' : '#ef4444';

            // Respect
            let respectHtml = '';
            if (attack.respect_gain > 0) {
                respectHtml = `<span style="color: #22c55e;">+${attack.respect_gain.toFixed(2)}</span>`;
            } else if (attack.respect_loss > 0) {
                respectHtml = `<span style="color: #ef4444;">-${attack.respect_loss.toFixed(2)}</span>`;
            }

            // Chain - clickable if > 10 and stealthed
            const chainClickable = attack.chain > 10 && attack.stealthed;
            let chainHtml = '';
            if (attack.chain) {
                chainHtml = chainClickable
                    ? `<a href="https://www.torn.com/page.php?sid=factionWarfare#/chains" style="color: #69c;">#${attack.chain}</a>`
                    : `#${attack.chain}`;
            }

            html += `
                <div class="attack-entry${attack.isMain ? ' main' : ''}">
                    <div class="attack-header">
                        <span class="attack-names">
                            <a href="profiles.php?XID=${attack.attacker_id}">${attack.attacker_name || '?'}</a>
                            <span class="attack-arrow">→</span>
                            <a href="profiles.php?XID=${attack.defender_id}">${attack.defender_name || '?'}</a>
                        </span>
                        <span class="attack-result" style="color: ${resultColor};">${attack.result}</span>
                    </div>
                    <div class="attack-stats">
                        <span style="color: ${ffColor};">FF ${ff.toFixed(2)}</span>
                        ${respectHtml ? `<span>${respectHtml}</span>` : ''}
                        ${chainHtml ? `<span>${chainHtml}</span>` : ''}
                    </div>
                    ${tags.length > 0 ? `
                        <div class="attack-tags">
                            ${tags.map(t => `<span class="tag" style="color: ${t.color};">${t.label}</span>`).join('')}
                        </div>
                    ` : ''}
                </div>
            `;
        }

        html += `
                <div class="footer">
                    <span>${attacks.length} attack${attacks.length > 1 ? 's' : ''}</span>
                    <a href="#" id="reset-api-key-link" style="color: #555;">⚙</a>
                </div>
            </div>
        `;

        container.innerHTML = html;
        insertContainer(container);

        document.getElementById('reset-api-key-link')?.addEventListener('click', (e) => {
            e.preventDefault();
            if (confirm('Reset API key?')) {
                GM_setValue('tornApiKey', '');
                location.reload();
            }
        });
    }

    function createContainer() {
        const container = document.createElement('div');
        container.id = 'attack-api-box';
        container.className = 'log-options cont-black m-top10';
        return container;
    }

    function insertContainer(container) {
        const existing = document.getElementById('attack-api-box');
        if (existing) existing.remove();

        const logInfoWrap = document.querySelector('.log-info-wrap');
        if (logInfoWrap) {
            const lastLogOptions = logInfoWrap.querySelector('.log-options:last-of-type');
            if (lastLogOptions) {
                lastLogOptions.after(container);
            } else {
                logInfoWrap.appendChild(container);
            }
        }
    }

})();