// ==UserScript==
// @name Official TriX Proxy Bot Script
// @version 0.4.1
// @description A multi-tab bot for territorial.io with automatic re-connect. The first tab is the Main Controller for all sub-tabs.
// @author painsel
// @license MIT
// @homepageURL https://greasyfork.org/en/scripts/549132-trix-executor-beta-for-territorial-io
// @match *://territorial.io/*
// @match *://*/*?__cpo=*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @namespace http://tampermonkey.net/
// ==/UserScript==
(async function() {
'use strict';
// --- 1. Conditional Execution & Config ---
const isProxy = window.location.search.includes('__cpo=');
if (isProxy) {
try {
if (!atob(new URLSearchParams(window.location.search).get('__cpo')).includes('territorial.io')) return;
} catch (e) { return; }
}
const CONFIG = {
STORAGE_KEY: 'd122_bot_settings',
MAIN_TAB_KEY: 'trix_main_tab_global_v2',
TABS_STATUS_KEY: 'trix_tabs_status_v2',
COMMAND_QUEUE_KEY: 'trix_command_queue_v2',
HEARTBEAT_INTERVAL: 2500,
MAIN_TAB_TIMEOUT: 7000,
PROXY_TAB_TIMEOUT: 7000,
RECONNECT_DELAY: 3000, // 3 seconds before attempting to reconnect
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 isMainTab = false;
const tabId = Date.now() + Math.random().toString(36).substring(2);
const tabOrigin = window.location.hostname;
// --- 2. Shared Functions ---
function simulateTyping(element,text,onComplete){let i=0;element.value="",element.focus();function typeCharacter(){i<text.length?(element.value+=text.charAt(i),element.dispatchEvent(new Event("input",{bubbles:!0})),i++,setTimeout(typeCharacter,50+150*Math.random())):(element.blur(),onComplete&&onComplete())}typeCharacter()}
// --- 3. Main Tab Logic ---
const mainTab = {
uiInitialized: false,
async init() {
isMainTab = true;
document.title = "[Main] TriX Bot";
setInterval(() => this.sendHeartbeat(), CONFIG.HEARTBEAT_INTERVAL);
waitForGameUI(async () => {
await this.initializeUI();
setInterval(() => this.updateAndCleanupTabs(), CONFIG.HEARTBEAT_INTERVAL);
});
},
async sendHeartbeat() { await GM_setValue(CONFIG.MAIN_TAB_KEY, { id: tabId, timestamp: Date.now() }); },
async updateAndCleanupTabs() {
const now = Date.now();
const statuses = await GM_getValue(CONFIG.TABS_STATUS_KEY, {});
let changed = false;
for (const id in statuses) {
if (now - statuses[id].lastSeen > CONFIG.PROXY_TAB_TIMEOUT) {
delete statuses[id];
changed = true;
}
}
if (changed) { await GM_setValue(CONFIG.TABS_STATUS_KEY, statuses); }
this.updateTabListUI(statuses);
},
updateTabListUI(statuses) {
if (!this.uiInitialized) return;
const list = document.getElementById('trix-tab-list');
const counter = document.getElementById('trix-tab-counter');
const statusEntries = Object.values(statuses);
list.innerHTML = '';
if (statusEntries.length === 0) { list.innerHTML = '<li class="trix-tab-item-empty">No other tabs found.</li>'; }
else { statusEntries.forEach(tab => { const li = document.createElement('li'); li.className = `trix-tab-item status-${tab.status.toLowerCase()}`; li.innerHTML = `<span>${tab.origin}</span> <span class="tab-status">${tab.status}</span>`; list.appendChild(li); }); }
counter.textContent = statusEntries.length;
},
async initializeUI() {
if (this.uiInitialized) return; this.uiInitialized = true;
const trixCSS=`: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 TriX Proxy Bot [MAIN]</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>Sub-Tabs</span><span id="trix-tab-counter">0</span></div><ul id="trix-tab-list"></ul></div></div></div>`;
document.body.insertAdjacentHTML('beforeend',trixHTML);
const clanTagInput=document.getElementById("trix-clan-tag"),usernameInput=document.getElementById("trix-username"),radioButtons=document.querySelectorAll('input[name="trix-typing-style"]'),startBtn=document.getElementById("trix-start-btn");const toggleUsernameInput=()=>{const e=document.querySelector('input[name="trix-typing-style"]:checked').value;usernameInput.disabled="ai"===e,usernameInput.placeholder="ai"===e?"AI will generate a name":"Enter your name"};radioButtons.forEach(e=>e.addEventListener("change",toggleUsernameInput));const saveSettings=async()=>{await GM_setValue(CONFIG.STORAGE_KEY,{clanTag:clanTagInput.value,username:usernameInput.value})},loadSettings=async()=>{const e=await GM_getValue(CONFIG.STORAGE_KEY,{});clanTagInput.value=e.clanTag||"",usernameInput.value=e.username||""};await loadSettings();toggleUsernameInput();clanTagInput.addEventListener("input",saveSettings);usernameInput.addEventListener("input",saveSettings);startBtn.addEventListener("click",async()=>{const e={clanTag:clanTagInput.value.trim(),mode:document.querySelector('input[name="trix-typing-style"]:checked').value,customUsername:usernameInput.value.trim(),timestamp:Date.now()};await GM_setValue(CONFIG.COMMAND_QUEUE_KEY,e)});const container=document.getElementById("trix-container"),header=document.getElementById("trix-header");let isDragging=!1,offsetX,offsetY;header.onmousedown=e=>{isDragging=!0,offsetX=e.clientX-container.offsetLeft,offsetY=e.clientY-container.offsetTop,document.onmousemove=e=>{isDragging&&(container.style.left=`${e.clientX-offsetX}px`,container.style.top=`${e.clientY-offsetY}px`)},document.onmouseup=()=>{isDragging=!1,document.onmousemove=null,document.onmouseup=null}};
}
};
// --- 4. Sub-Tab Logic ---
const subTab = {
status: 'IDLE',
previousStatus: 'IDLE',
isReconnecting: false,
lastCommandTimestamp: 0,
init() {
document.title = "[Sub-Tab] TriX Bot";
this.proxyWebSocket();
setInterval(() => this.sendHeartbeat(), CONFIG.HEARTBEAT_INTERVAL);
setInterval(() => this.checkMainTabHealth(), CONFIG.HEARTBEAT_INTERVAL);
setInterval(() => this.listenForCommands(), 500);
},
async sendHeartbeat() {
const statuses = await GM_getValue(CONFIG.TABS_STATUS_KEY, {});
statuses[tabId] = { status: this.status, origin: tabOrigin, lastSeen: Date.now() };
await GM_setValue(CONFIG.TABS_STATUS_KEY, statuses);
},
updateStatus(newStatus) {
this.previousStatus = this.status;
if (this.status !== newStatus) {
this.status = newStatus;
this.sendHeartbeat(); // Send immediate update
// --- RE-CONNECT TRIGGER ---
if (this.previousStatus === 'LIVE' && newStatus === 'DISCONNECTED') {
this.attemptReconnect();
}
}
},
findButtonByText(text) {
const buttons = document.querySelectorAll('button');
for (const btn of buttons) {
if (btn.innerText && btn.innerText.includes(text)) {
return btn;
}
}
return null;
},
attemptReconnect() {
if (this.isReconnecting) return;
this.isReconnecting = true;
console.log(`[TriX Sub-Tab] Disconnected from a live game. Attempting to reconnect in ${CONFIG.RECONNECT_DELAY / 1000} seconds...`);
setTimeout(() => {
let playBtn = this.findButtonByText('Play');
if (playBtn) {
console.log('[TriX Sub-Tab] Found "Play" button, clicking to re-join...');
playBtn.click();
} else {
let multiplayerBtn = this.findButtonByText('Multiplayer');
if (multiplayerBtn) {
console.log('[TriX Sub-Tab] On main menu. Clicking "Multiplayer" then "Play"...');
multiplayerBtn.click();
// Wait for the UI to transition
setTimeout(() => {
playBtn = this.findButtonByText('Play');
if (playBtn) playBtn.click();
}, 500);
}
}
// Reset flag after a cooldown to allow another attempt if this one fails
setTimeout(() => { this.isReconnecting = false; }, 5000);
}, CONFIG.RECONNECT_DELAY);
},
async checkMainTabHealth() {
const mainTabInfo = await GM_getValue(CONFIG.MAIN_TAB_KEY, null);
if (!mainTabInfo || Date.now() - mainTabInfo.timestamp > CONFIG.MAIN_TAB_TIMEOUT) {
console.warn('[TriX Bot] Main tab timed out. Attempting to become new Main Tab.');
await GM_setValue(CONFIG.MAIN_TAB_KEY, { id: tabId, timestamp: Date.now() });
setTimeout(async () => {
const currentMain = await GM_getValue(CONFIG.MAIN_TAB_KEY);
if (currentMain && currentMain.id === tabId) { window.location.reload(); }
}, 500);
}
},
async listenForCommands() {
const command = await GM_getValue(CONFIG.COMMAND_QUEUE_KEY, null);
if (command && command.timestamp > this.lastCommandTimestamp) {
this.lastCommandTimestamp = command.timestamp;
if (this.status === 'LIVE') {
const nameInput=document.querySelector("#input0");if(!nameInput)return;let fullName,username;if("ai"===command.mode){username=CONFIG.AI_NAMES[Math.floor(Math.random()*CONFIG.AI_NAMES.length)],fullName=command.clanTag?(()=>{const e=["standard","possessive","lowerTag","tagAfter"][Math.floor(4*Math.random())];switch(e){case"standard":return`[${command.clanTag}] ${username}`;case"possessive":return`[${command.clanTag}]'s ${username}`;case"lowerTag":return`[${command.clanTag.toLowerCase()}] ${username}`;case"tagAfter":return`${username} [${command.clanTag.toLowerCase()}]`}})():username}else{if(!(username=command.customUsername))return;fullName=command.clanTag?`[${command.clanTag}] ${username}`:username}simulateTyping(nameInput,fullName);
}
}
},
proxyWebSocket(){const e=this,t=unsafeWindow.WebSocket;unsafeWindow.WebSocket=function(n,s){if(n.includes(CONFIG.MAIN_SOCKET_URL_PART)){const o=new t(n,s);return o.addEventListener("open",()=>e.updateStatus("LIVE")),o.addEventListener("close",()=>e.updateStatus("DISCONNECTED")),o.addEventListener("error",()=>e.updateStatus("DISCONNECTED")),o}return new t(n,s)}}
};
// --- 5. Initialization and Role Election ---
function findMultiplayerButton(){for(const e of document.querySelectorAll("button"))if(e.innerText&&e.innerText.includes("Multiplayer"))return e;return null}
function waitForGameUI(callback){if(findMultiplayerButton()){callback();return}const e=new MutationObserver((t,n)=>{findMultiplayerButton()&&(n.disconnect(),callback())});e.observe(document.documentElement,{childList:!0,subtree:!0})}
async function electRole() {
const currentMain = await GM_getValue(CONFIG.MAIN_TAB_KEY, null);
if (!currentMain || Date.now() - currentMain.timestamp > CONFIG.MAIN_TAB_TIMEOUT) {
await GM_setValue(CONFIG.MAIN_TAB_KEY, { id: tabId, timestamp: Date.now() });
mainTab.init();
} else {
subTab.init();
}
}
window.addEventListener('beforeunload', async () => {
if (isMainTab) {
await GM_setValue(CONFIG.MAIN_TAB_KEY, null);
} else {
const statuses = await GM_getValue(CONFIG.TABS_STATUS_KEY, {});
delete statuses[tabId];
await GM_setValue(CONFIG.TABS_STATUS_KEY, statuses);
}
});
electRole();
})();