Torn Attack Page - Clickable Player Names

Make player names clickable in attack logs and stats

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Torn Attack Page - Clickable Player Names
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Make player names clickable in attack logs and stats
// @author       ShAdOwCrEsT [3929345]
// @match        https://www.torn.com/loader.php?sid=attack&user2ID=*
// @match        https://www.torn.com/loader.php?sid=attack
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      shadowcrest96.workers.dev
// ==/UserScript==

(function() {
    'use strict';

    const API_URL = 'https://lookup.shadowcrest96.workers.dev';
    const playerIdCache = {};
    const failedLookups = {};
    const processedElements = new WeakSet();
    let userApiKey = null;
    let authCheckDone = false;

    function getApiKey() {
        if (userApiKey) return userApiKey;

        userApiKey = GM_getValue('torn_api_key', null);

        if (!userApiKey) {
            const key = prompt(
                '🔑 Torn Attack Page Script - API Key Required'
            );

            if (key && key.trim().length > 0) {
                userApiKey = key.trim();
                GM_setValue('torn_api_key', userApiKey);
            } else {
                alert('❌ API key is required to use this script. The script will not function without it.');
                return null;
            }
        }

        return userApiKey;
    }

    function resetApiKey() {
        GM_setValue('torn_api_key', null);
        userApiKey = null;
        authCheckDone = false;
        alert('✓ API key has been reset. Refresh the page to enter a new key.');
    }

    window.resetTornApiKey = resetApiKey;

    function getPlayerId(username) {
        if (!authCheckDone && playerIdCache[username]) {
            return Promise.resolve(playerIdCache[username]);
        }

        if (failedLookups[username] && Date.now() - failedLookups[username] < 30000) {
            return Promise.resolve(null);
        }

        const apiKey = getApiKey();
        if (!apiKey) {
            return Promise.resolve(null);
        }

        return new Promise((resolve) => {
            console.log(`🔍 Looking up: "${username}"`);
            GM_xmlhttpRequest({
                method: 'GET',
                url: `${API_URL}?name=${encodeURIComponent(username)}`,
                headers: {
                    'X-Torn-Key': apiKey
                },
                timeout: 5000,
                onload: function(response) {
                    console.log(`📥 Response for ${username}:`, {
                        status: response.status,
                        statusText: response.statusText,
                        contentType: response.responseHeaders.match(/content-type: ([^\r\n]+)/i)?.[1],
                        responseLength: response.responseText.length,
                        responsePreview: response.responseText.substring(0, 100)
                    });

                    try {
                        if (response.status === 401 || response.status === 403) {
                            if (!authCheckDone) {
                                authCheckDone = true;
                                Object.keys(playerIdCache).forEach(key => delete playerIdCache[key]);

                                if (response.status === 401) {
                                    alert('❌ Invalid API Key\n\nPlease check your API key and try again.');
                                } else {
                                    alert('❌ Unauthorized\n\nYou are not authorized to use this script.');
                                }
                            }
                            resolve(null);
                            return;
                        }

                        const contentType = response.responseHeaders.toLowerCase();
                        if (!contentType.includes('application/json') && response.responseText.trim().startsWith('<!DOCTYPE')) {
                            console.warn(`❌ API returned HTML for ${username}, likely an error page`);
                            failedLookups[username] = Date.now();
                            resolve(null);
                            return;
                        }

                        const data = JSON.parse(response.responseText);
                        if (data.id) {
                            playerIdCache[username] = data.id;
                            console.log(`✓ Found ID for ${username}: ${data.id}`);
                            resolve(data.id);
                        } else if (data.error) {
                            console.log(`✗ ${username}: ${data.error}`);
                            failedLookups[username] = Date.now();
                            resolve(null);
                        } else {
                            console.log(`✗ No ID found for ${username}:`, data);
                            failedLookups[username] = Date.now();
                            resolve(null);
                        }
                    } catch (error) {
                        console.error(`Error parsing response for ${username}:`, error);
                        console.log('Response text:', response.responseText.substring(0, 200));
                        failedLookups[username] = Date.now();
                        resolve(null);
                    }
                },
                onerror: function(error) {
                    console.error(`Error fetching ID for ${username}:`, error);
                    failedLookups[username] = Date.now();
                    resolve(null);
                },
                ontimeout: function() {
                    console.error(`Timeout fetching ID for ${username}`);
                    failedLookups[username] = Date.now();
                    resolve(null);
                }
            });
        });
    }

    function extractPlayerNames(text) {
        const patterns = [
            /^([\w-]+)\s+(?:hit|fired|critically|missed|initiated|injected|threw|used)/i,
            /hitting\s+([\w-]+)/i,
            /missing\s+([\w-]+)/i,
            /against\s+([\w-]+)/i,
            /around\s+([\w-]+)/i,
            /on\s+([\w-]+)/i,
            /to\s+([\w-]+)/i
        ];

        const names = [];
        for (const pattern of patterns) {
            const match = text.match(pattern);
            if (match && match[1]) {
                names.push(match[1]);
            }
        }
        return [...new Set(names)];
    }

    async function makeNamesClickableInText(spanElement) {
        if (processedElements.has(spanElement)) {
            return;
        }
        processedElements.add(spanElement);

        const text = spanElement.textContent;
        const names = extractPlayerNames(text);

        if (names.length === 0) return;

        const nameIdMap = {};
        for (const name of names) {
            const id = await getPlayerId(name);
            if (id) {
                nameIdMap[name] = id;
            }
        }

        if (Object.keys(nameIdMap).length === 0) return;

        let html = spanElement.innerHTML;
        for (const [name, id] of Object.entries(nameIdMap)) {
            const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const regex = new RegExp(`\\b${escapedName}\\b`, 'g');
            html = html.replace(regex, `<a href="https://www.torn.com/loader.php?sid=attack&user2ID=${id}" target="_blank" rel="noopener noreferrer" style="color: #00A2FF; text-decoration: underline; font-weight: bold;">${name}</a>`);
        }

        spanElement.innerHTML = html;
    }

    function processAttackLog() {
        const messages = document.querySelectorAll('.message___Z4JCk span');

        messages.forEach(span => {
            if (!processedElements.has(span) && !span.querySelector('a')) {
                makeNamesClickableInText(span);
            }
        });
    }

    async function processStatsParticipants() {
        const playerNameElements = document.querySelectorAll('.playername___oeaye');

        for (const element of playerNameElements) {
            if (processedElements.has(element) || element.querySelector('a')) continue;
            processedElements.add(element);

            const username = element.textContent.trim();
            if (username) {
                const playerId = await getPlayerId(username);
                if (playerId) {
                    const link = document.createElement('a');
                    link.href = `https://www.torn.com/loader.php?sid=attack&user2ID=${playerId}`;
                    link.target = '_blank';
                    link.rel = 'noopener noreferrer';
                    link.textContent = username;
                    link.style.color = '#00A2FF';
                    link.style.textDecoration = 'underline';
                    link.style.fontWeight = 'bold';

                    element.textContent = '';
                    element.appendChild(link);
                }
            }
        }
    }

    async function processPlayerHeaders() {
        const userNameElements = document.querySelectorAll('.userName___loAWK');

        for (const element of userNameElements) {
            if (processedElements.has(element) || element.querySelector('a')) continue;
            processedElements.add(element);

            const username = element.textContent.trim();
            if (username) {
                const playerId = await getPlayerId(username);
                if (playerId) {
                    const link = document.createElement('a');
                    link.href = `https://www.torn.com/loader.php?sid=attack&user2ID=${playerId}`;
                    link.target = '_blank';
                    link.rel = 'noopener noreferrer';
                    link.textContent = username;
                    link.style.color = '#00A2FF';
                    link.style.textDecoration = 'underline';
                    link.style.fontWeight = 'bold';

                    element.textContent = '';
                    element.appendChild(link);
                }
            }
        }
    }

    async function processAllPlayerNames() {
        await processAttackLog();
        await processStatsParticipants();
        await processPlayerHeaders();
    }

    setTimeout(() => {
        console.log('🚀 Torn Attack Page Script with Auth loaded!');
        console.log('💡 To reset your API key, type: resetTornApiKey()');

        if (getApiKey()) {
            console.log('✓ API key found, processing...');
            processAllPlayerNames();
        }
    }, 1500);

    const observer = new MutationObserver((mutations) => {
        const hasRelevantChanges = mutations.some(mutation => {
            return mutation.addedNodes.length > 0 ||
                   (mutation.type === 'characterData' && mutation.target.textContent);
        });

        if (hasRelevantChanges) {
            clearTimeout(observer.timeout);
            observer.timeout = setTimeout(() => {
                console.log('Processing changes...');
                processAllPlayerNames();
            }, 300);
        }
    });

    setTimeout(() => {
        const targetNode = document.querySelector('.coreWrap___LtSEy');
        if (targetNode) {
            observer.observe(targetNode, {
                childList: true,
                subtree: true,
                characterData: true
            });
            console.log('Torn Attack Page - Observing for changes!');
        } else {
            console.warn('Could not find .coreWrap___LtSEy element');
        }
    }, 1000);

})();