Torn Attack Log API Data

Shows API data for attacks on attack log pages

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

})();