Official TriX Proxy Bot Script

A multi-tab bot script for territorial.io that simulates typing on proxy sites. | Supported Proxies: CroxyProxy

ของเมื่อวันที่ 17-09-2025 ดู เวอร์ชันล่าสุด

// ==UserScript==
// @name         Official TriX Proxy Bot Script
// @version      0.2.0
// @description  A multi-tab bot script for territorial.io that simulates typing on proxy sites. | Supported Proxies: CroxyProxy
// @author       painsel
// @license      MIT
// @homepageURL  https://greasyfork.org/en/scripts/549132-trix-executor-beta-for-territorial-io
// @match        *://*/*?__cpo=*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @namespace    http://tampermonkey.net/
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. Conditional Execution & Config ---
    const urlParams = new URLSearchParams(window.location.search);
    if (!urlParams.has('__cpo')) return;
    try {
        if (!atob(urlParams.get('__cpo')).includes('territorial.io')) {
            console.log('[TriX Bot] Not a territorial.io proxy. Halting execution.');
            return;
        }
    } catch (e) { return; }

    const CONFIG = {
        STORAGE_KEY: 'd122_bot_settings',
        CONTROLLER_ID_KEY: 'd122_controller_id',
        BROADCAST_CHANNEL: 'trix_proxy_bot_channel',
        HEARTBEAT_INTERVAL: 3000, // 3 seconds
        WORKER_TIMEOUT: 7000, // 7 seconds
        MAIN_SOCKET_URL_PART: '/s52/',
        AI_NAMES: [
            "Apex", "Vortex", "Shadow", "Nova", "Cipher", "Blaze", "Reaper", "Phantom", "Genesis", "Specter", "Warden", "Rogue", "Titan", "Fury", "Serpent", "Oracle", "Zenith", "Pulsar", "Jester", "Mirage", "Nomad", "Havoc", "Crux", "Wraith", "Glitch", "Vector", "Flux", "Byte", "Pixel", "Matrix", "Kernel", "Grid", "Node", "Syntax", "Griffin", "Wyvern", "Goliath", "Leviathan", "Hydra", "Phoenix", "Golem", "Paladin", "Warlock", "Rune", "Storm", "Quake", "Inferno", "Frost", "Cyclone", "Meteor", "Comet", "Abyss", "Ember", "Tempest", "Striker", "Raider", "Vanguard", "Sentinel", "Commando", "Juggernaut", "Marshal", "Legion", "Phalanx", "Blitz", "Saber", "Echo", "Impulse", "Catalyst", "Paradox", "Rift", "Karma", "Legacy", "Valor", "Creed", "Requiem", "Solstice", "Equinox", "Infinity", "Axiom", "Enigma", "Viper", "Cobra", "Hawk", "Falcon", "Tiger", "Wolf", "Panther", "Jackal", "Goliath", "Javelin", "Overload", "Torrent", "Cascade", "Helix", "Ion", "Ronin", "Monarch", "Zealot", "Herald", "Jinx", "Quasar"
        ]
    };

    let isController = false;
    const tabId = Date.now() + Math.random().toString(36).substring(2);
    const channel = new BroadcastChannel(CONFIG.BROADCAST_CHANNEL);

    // --- 2. Shared Functions ---
    function simulateTyping(element, text, onComplete) {
        let i = 0;
        element.value = '';
        element.focus();
        function typeCharacter() {
            if (i < text.length) {
                element.value += text.charAt(i);
                element.dispatchEvent(new Event('input', { bubbles: true }));
                i++;
                setTimeout(typeCharacter, Math.random() * 150 + 50);
            } else {
                element.blur();
                if (onComplete) onComplete();
            }
        }
        typeCharacter();
    }

    // --- 3. Controller Logic ---
    const controller = {
        workers: new Map(),
        uiInitialized: false,

        init() {
            isController = true;
            document.title = "[C] " + document.title;
            this.cleanupStaleWorkers();
            this.listenForWorkers();
            waitForGameUI(() => {
                this.initializeUI();
                setInterval(() => this.cleanupStaleWorkers(), CONFIG.WORKER_TIMEOUT);
            });
        },

        listenForWorkers() {
            channel.onmessage = (event) => {
                const { type, payload } = event.data;
                if (type === 'WORKER_HEARTBEAT') {
                    this.workers.set(payload.id, { ...payload, lastSeen: Date.now() });
                    this.updateWorkerList();
                }
            };
        },

        cleanupStaleWorkers() {
            const now = Date.now();
            let changed = false;
            for (const [id, worker] of this.workers.entries()) {
                if (now - worker.lastSeen > CONFIG.WORKER_TIMEOUT) {
                    this.workers.delete(id);
                    changed = true;
                }
            }
            if (changed) {
                this.updateWorkerList();
            }
        },

        updateWorkerList() {
            if (!this.uiInitialized) return;
            const list = document.getElementById('trix-tab-list');
            const counter = document.getElementById('trix-tab-counter');
            if (!list || !counter) return;

            list.innerHTML = '';
            if (this.workers.size === 0) {
                list.innerHTML = '<li class="trix-tab-item-empty">No other tabs found.</li>';
            } else {
                this.workers.forEach(worker => {
                    const li = document.createElement('li');
                    li.className = `trix-tab-item status-${worker.status.toLowerCase()}`;
                    li.innerHTML = `<span>Tab ${worker.id.slice(-6)}</span> <span class="tab-status">${worker.status}</span>`;
                    list.appendChild(li);
                });
            }
            counter.textContent = this.workers.size;
        },

        initializeUI() {
            if (this.uiInitialized) return;
            this.uiInitialized = true;
            const trixCSS = `
                /* ... (Same VS Code theme CSS as before) ... */
                :root{--trix-bg:#1e1e1e;--trix-bg-light:#252526;--trix-header-bg:#333333;--trix-border:#3c3c3c;--trix-text:#d4d4d4;--trix-text-secondary:#cccccc;--trix-blue-accent:#007acc;--trix-button-bg:#0e639c;--trix-button-hover-bg:#1177bb;--trix-input-bg:#3c3c3c}
                #trix-container{position:fixed;top:20px;left:20px;width:280px;background-color:var(--trix-bg-light);border:1px solid var(--trix-border);border-radius:6px;color:var(--trix-text);font-family:'Consolas','Menlo','Courier New',monospace;font-size:14px;z-index:99999;box-shadow:0 5px 20px rgba(0,0,0,0.5);user-select:none;overflow:hidden}
                #trix-header{background-color:var(--trix-header-bg);padding:8px 12px;cursor:move;font-weight:bold;border-bottom:1px solid var(--trix-border)}
                #trix-header a{color:var(--trix-blue-accent);text-decoration:none}
                #trix-header a:hover{text-decoration:underline}
                #trix-body{padding:15px;display:flex;flex-direction:column;gap:15px}
                .trix-input-group{display:flex;flex-direction:column;gap:5px}
                .trix-input-group label{font-size:13px;color:var(--trix-text-secondary)}
                .trix-input-group input[type="text"]{background-color:var(--trix-input-bg);border:1px solid var(--trix-border);color:var(--trix-text);padding:8px;border-radius:4px;font-family:inherit;outline:none;transition:border-color .2s,background-color .2s}
                .trix-input-group input[type="text"]:focus{border-color:var(--trix-blue-accent)}
                .trix-input-group input[type="text"]:disabled{background-color:#2a2a2a;color:#888;cursor:not-allowed}
                #trix-start-btn{background-color:var(--trix-button-bg);color:white;border:none;padding:10px;border-radius:4px;font-family:inherit;font-size:14px;font-weight:bold;cursor:pointer;transition:background-color .2s}
                #trix-start-btn:hover{background-color:var(--trix-button-hover-bg)}
                #trix-start-btn:disabled{background-color:#5a5a5a;cursor:not-allowed}
                .trix-radio-group{display:flex;gap:15px}
                .trix-radio-group label{display:flex;align-items:center;gap:5px;cursor:pointer}
                #trix-tab-manager{margin-top:15px;border-top:1px solid var(--trix-border);padding-top:10px}
                #trix-tab-header{display:flex;justify-content:space-between;align-items:center;font-weight:bold;margin-bottom:5px}
                #trix-tab-counter{background-color:var(--trix-blue-accent);color:white;padding:2px 6px;border-radius:10px;font-size:12px}
                #trix-tab-list{list-style:none;padding:0;margin:0;max-height:120px;overflow-y:auto;background-color:var(--trix-bg);border-radius:4px;border:1px solid var(--trix-border)}
                .trix-tab-item,.trix-tab-item-empty{padding:5px 8px;border-bottom:1px solid var(--trix-border);font-size:12px;display:flex;justify-content:space-between;align-items:center}
                .trix-tab-item:last-child{border-bottom:none}
                .trix-tab-item-empty{justify-content:center;color:var(--trix-text-secondary)}
                .tab-status{font-weight:bold}
                .status-live .tab-status{color:#2ecc71}
                .status-idle .tab-status{color:#f1c40f}
                .status-disconnected .tab-status{color:#e74c3c}`;
            GM_addStyle(trixCSS);

            const trixHTML = `
                <div id="trix-container">
                    <div id="trix-header">Official <a href="https://greasyfork.org/en/scripts/549132-trix-executor-beta-for-territorial-io" target="_blank">TriX</a> Proxy Bot [CONTROLLER]</div>
                    <div id="trix-body">
                        <div class="trix-input-group">
                            <label for="trix-clan-tag">Clan Tag (Customizable in AI mode)</label>
                            <input type="text" id="trix-clan-tag" placeholder="e.g., TriX">
                        </div>
                        <div class="trix-input-group">
                            <label for="trix-username">Username</label>
                            <input type="text" id="trix-username" placeholder="Enter your name">
                        </div>
                        <div class="trix-input-group">
                            <label>Mode</label>
                            <div class="trix-radio-group">
                                <label><input type="radio" name="trix-typing-style" value="custom" checked> Custom</label>
                                <label><input type="radio" name="trix-typing-style" value="ai"> AI</label>
                            </div>
                        </div>
                        <button id="trix-start-btn">Begin Typing Simulation (All Tabs)</button>
                        <div id="trix-tab-manager">
                            <div id="trix-tab-header"><span>Managed Tabs</span><span id="trix-tab-counter">0</span></div>
                            <ul id="trix-tab-list"><li class="trix-tab-item-empty">Searching for other tabs...</li></ul>
                        </div>
                    </div>
                </div>`;
            document.body.insertAdjacentHTML('beforeend', trixHTML);

            const clanTagInput = document.getElementById('trix-clan-tag');
            const usernameInput = document.getElementById('trix-username');
            const radioButtons = document.querySelectorAll('input[name="trix-typing-style"]');
            const startBtn = document.getElementById('trix-start-btn');

            const toggleUsernameInput = () => {
                const mode = document.querySelector('input[name="trix-typing-style"]:checked').value;
                usernameInput.disabled = (mode === 'ai');
                usernameInput.placeholder = (mode === 'ai') ? "AI will generate a name" : "Enter your name";
            };

            radioButtons.forEach(radio => radio.addEventListener('change', toggleUsernameInput));

            const saveSettings = () => {
                localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify({
                    clanTag: clanTagInput.value,
                    username: usernameInput.value
                }));
            };
            const loadSettings = () => {
                const settings = JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEY) || '{}');
                clanTagInput.value = settings.clanTag || '';
                usernameInput.value = settings.username || '';
            };

            loadSettings();
            toggleUsernameInput();
            clanTagInput.addEventListener('input', saveSettings);
            usernameInput.addEventListener('input', saveSettings);

            startBtn.addEventListener('click', () => {
                const payload = {
                    clanTag: clanTagInput.value.trim(),
                    mode: document.querySelector('input[name="trix-typing-style"]:checked').value,
                    customUsername: usernameInput.value.trim()
                };
                channel.postMessage({ type: 'START_TYPING', payload });
            });
            // Draggable UI
            const container = document.getElementById('trix-container');
            const header = document.getElementById('trix-header');
            let isDragging = false, offsetX, offsetY;
            header.onmousedown = (e) => {
                isDragging = true;
                offsetX = e.clientX - container.offsetLeft;
                offsetY = e.clientY - container.offsetTop;
                document.onmousemove = (ev) => { if (isDragging) { container.style.left = `${ev.clientX - offsetX}px`; container.style.top = `${ev.clientY - offsetY}px`; } };
                document.onmouseup = () => { isDragging = false; document.onmousemove = null; document.onmouseup = null; };
            };
        }
    };

    // --- 4. Worker Logic ---
    const worker = {
        status: 'IDLE', // IDLE, LIVE, DISCONNECTED
        init() {
            document.title = "[W] " + document.title;
            this.proxyWebSocket();
            this.listenForCommands();
            setInterval(() => this.sendHeartbeat(), CONFIG.HEARTBEAT_INTERVAL);
            this.sendHeartbeat(); // Send initial heartbeat
        },
        sendHeartbeat() {
            channel.postMessage({ type: 'WORKER_HEARTBEAT', payload: { id: tabId, status: this.status } });
        },
        updateStatus(newStatus) {
            if (this.status !== newStatus) {
                this.status = newStatus;
                this.sendHeartbeat(); // Send an immediate update
            }
        },
        listenForCommands() {
            channel.onmessage = (event) => {
                const { type, payload } = event.data;
                if (type === 'START_TYPING' && this.status === 'LIVE') {
                    const nameInput = document.querySelector('#input0');
                    if (!nameInput) return;

                    let fullName, username;
                    if (payload.mode === 'ai') {
                        username = CONFIG.AI_NAMES[Math.floor(Math.random() * CONFIG.AI_NAMES.length)];
                        if (!payload.clanTag) {
                            fullName = username;
                        } else {
                            const formats = ['standard', 'possessive', 'lowerTag', 'tagAfter'];
                            const chosenFormat = formats[Math.floor(Math.random() * formats.length)];
                            switch(chosenFormat) {
                                case 'standard': fullName = `[${payload.clanTag}] ${username}`; break;
                                case 'possessive': fullName = `[${payload.clanTag}]'s ${username}`; break;
                                case 'lowerTag': fullName = `[${payload.clanTag.toLowerCase()}] ${username}`; break;
                                case 'tagAfter': fullName = `${username} [${payload.clanTag.toLowerCase()}]`; break;
                            }
                        }
                    } else { // custom
                        username = payload.customUsername;
                        if (!username) return;
                        fullName = payload.clanTag ? `[${payload.clanTag}] ${username}` : username;
                    }
                    simulateTyping(nameInput, fullName);
                }
            };
        },
        proxyWebSocket() {
            const self = this;
            const OriginalWebSocket = unsafeWindow.WebSocket;
            unsafeWindow.WebSocket = function(url, protocols) {
                if (url.includes(CONFIG.MAIN_SOCKET_URL_PART)) {
                    const ws = new OriginalWebSocket(url, protocols);
                    ws.addEventListener('open', () => self.updateStatus('LIVE'));
                    ws.addEventListener('close', () => self.updateStatus('DISCONNECTED'));
                    ws.addEventListener('error', () => self.updateStatus('DISCONNECTED'));
                    return ws;
                }
                return new OriginalWebSocket(url, protocols);
            };
        }
    };

    // --- 5. Initialization and Role Election ---
    function findMultiplayerButton() {
        for (const btn of document.querySelectorAll('button')) { if (btn.innerText && btn.innerText.includes('Multiplayer')) return btn; }
        return null;
    }
    function waitForGameUI(callback) {
        if (findMultiplayerButton()) { callback(); return; }
        const observer = new MutationObserver((mutations, obs) => {
            if (findMultiplayerButton()) { obs.disconnect(); callback(); }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
    }

    function electController() {
        const currentController = localStorage.getItem(CONFIG.CONTROLLER_ID_KEY);
        if (!currentController) {
            localStorage.setItem(CONFIG.CONTROLLER_ID_KEY, tabId);
            controller.init();
        } else {
            worker.init();
        }
    }
    // Handle controller leaving
    window.addEventListener('beforeunload', () => {
        if (isController) {
            localStorage.removeItem(CONFIG.CONTROLLER_ID_KEY);
        }
    });
    // Fallback for when controller tab crashes without firing beforeunload
    setTimeout(() => {
        if (!isController) {
            const currentControllerId = localStorage.getItem(CONFIG.CONTROLLER_ID_KEY);
            // Simple check: if a controller exists but we haven't seen a message, it might be dead.
            // A more robust system would use heartbeats for the controller too.
            // For now, if the controller ID is this tab's old ID after a refresh, take over.
            if (!currentControllerId || currentControllerId === tabId) {
                localStorage.setItem(CONFIG.CONTROLLER_ID_KEY, tabId);
                window.location.reload(); // Reload to re-initialize as controller
            }
        }
    }, 5000);

    electController();

    /*
    The MIT License (MIT)
    Copyright (c) 2024 painsel
    ... (license text) ...
    */
})();