TriX Executor (BETA) for Territorial.io

A powerful, multi-tabbed, persistent code execution and network logging environment with multi-language support. Now with Shadow DOM and loading retries.

当前为 2025-09-11 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TriX Executor (BETA) for Territorial.io
// @namespace    https://greasyfork.org/en/users/your-username
// @version      Beta-Andromeda-2023.11.03
// @description  A powerful, multi-tabbed, persistent code execution and network logging environment with multi-language support. Now with Shadow DOM and loading retries.
// @author       Painsel
// @match        *://territorial.io/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwYWFmZiI+PHBhdGggZD0iTTE4LjM2IDIyLjA1bC00LjQyLTQuNDJDMTMuNDcgMTcuODcgMTMgMTguMTcgMTMgMTguNUMxMyAyMC45OSAxNS4wMSAyMyAxNy41IDIzaDMuNTRjLTIuNDUtMS40OC00LjQyLTMuNDUtNS45LTUuOTV6TTggMTNjMS42NiAwIDMuMTgtLjU5IDQuMzgtMS42MkwxMCAxMy41VjIybDIuNS0yLjVMMTggMTMuOTZjLjM1LS40OC42NS0xIC44Ny0xLjU1QzE4LjYxIDEzLjQxIDE4IDEyLjM0IDE4IDEyVjBoLTJ2MTJjMCAuMzQtLjAyLjY3LS4wNiAxLS4zMy4xOC0uNjguMy0xLjA0LjM3LTEuNzMgMC0zLjI3LS45My00LjE2LTIuMzZMNiAxMy42VjVINHY4eiIvPjwvc3ZnPg==
// ==/UserScript==

/* global Prism, unsafeWindow */

(function() {
    'use strict';

    // --- WebSocket Proxy Setup (MUST run before page scripts) ---
    const OriginalWebSocket = unsafeWindow.WebSocket;
    let logPacketCallback = () => {};

    unsafeWindow.WebSocket = function(url, protocols) {
        if (!url.includes('/s52/')) { // Only proxy the game's main websocket
            return new OriginalWebSocket(url, protocols);
        }
        console.log(`[TriX] Intercepting WebSocket connection to: ${url}`);
        const ws = new OriginalWebSocket(url, protocols);

        const originalSend = ws.send.bind(ws);
        ws.send = function(data) {
            logPacketCallback('send', data);
            return originalSend(data);
        };
        ws.addEventListener('message', (event) => {
            logPacketCallback('receive', event.data);
        });
        return ws;
    };

    // --- Main script logic (runs after DOM is ready) ---
    function initialize() {
        const shadowHost = document.createElement('div');
        shadowHost.id = 'trix-host';
        document.body.appendChild(shadowHost);
        const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

        const styleElement = document.createElement('style');
        styleElement.textContent = `
            /* PrismJS Theme */
            code[class*="language-"],pre[class*="language-"]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;tab-size:4;}pre[class*="language-"]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*="language-"],pre[class*="language-"]{background:#2d2d2d}:not(pre)>code[class*="language-"]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment{color:#999}.token.punctuation{color:#ccc}.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}

            /* --- TriX Executor UI --- */
            @keyframes trix-fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
            @keyframes trix-slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }

            #trix-toggle-btn { position: fixed; top: 15px; right: 15px; z-index: 99999; width: 50px; height: 50px; background-color: rgba(30, 30, 34, 0.8); color: #00aaff; border: 2px solid #00aaff; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 24px; transition: all 0.3s ease; backdrop-filter: blur(5px); font-family: monospace; box-shadow: 0 0 10px rgba(0, 170, 255, 0.5); }
            #trix-toggle-btn:hover { transform: scale(1.1) rotate(15deg); box-shadow: 0 0 15px #00aaff; }

            #trix-container { position: fixed; top: 80px; right: 15px; width: 450px; min-height: 400px; z-index: 99998; color: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 25px rgba(0,0,0,0.5); display: flex; flex-direction: column; backdrop-filter: blur(10px); animation: trix-slide-in 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; resize: both; }
            #trix-container.hidden { display: none; }

            #trix-container[data-theme='dark-knight'] { background-color: rgba(30, 30, 34, 0.9); border: 1px solid #444; }
            #trix-container[data-theme='arctic-light'] { background-color: rgba(240, 240, 255, 0.9); border: 1px solid #ccc; color: #111; }
            #trix-container[data-theme='crimson'] { background-color: rgba(43, 8, 8, 0.9); border: 1px solid #8B0000; color: #f1f1f1; }
            #trix-container[data-theme='arctic-light'] .trix-tab, #trix-container[data-theme='arctic-light'] .trix-status-bar, #trix-container[data-theme='arctic-light'] #trix-packet-log { background: #e0e0e8; }
            #trix-container[data-theme='arctic-light'] .trix-tab.active { background: #f0f0ff; color: #0055aa; }
            #trix-container[data-theme='arctic-light'] .trix-editor-area { background: #f0f0ff; border-color: #aaa; }
            #trix-container[data-theme='crimson'] .trix-tab.active { color: #ff8b8b; }

            #trix-header { padding: 10px 15px; cursor: move; user-select: none; font-weight: bold; font-size: 16px; display: flex; justify-content: space-between; align-items: center; }
            #trix-container[data-theme='dark-knight'] #trix-header { background-color: rgba(0,0,0,0.3); }
            #trix-container[data-theme='arctic-light'] #trix-header { background-color: rgba(0,0,0,0.1); }
            #trix-container[data-theme='crimson'] #trix-header { background-color: rgba(139, 0, 0, 0.5); }
            .trix-title-3d { color: #E0E4E8; font-family: 'Segoe UI', 'Roboto', sans-serif; font-weight: 900; font-size: 18px; letter-spacing: 1px; text-shadow: 0 0 2px rgba(255, 255, 255, 0.6), 0 0 10px #00aaff, 1px 1px 0 #0f121a, 2px 2px 0 #0f121a, 3px 3px 0 #0f121a, 4px 4px 5px rgba(0, 0, 0, 0.5); }
            .trix-version-tag { font-size: 10px; font-weight: 300; opacity: 0.7; margin-left: 8px; vertical-align: middle; }
            #trix-header-controls { display: flex; align-items: center; gap: 15px; }
            #trix-fps-display { font-size: 12px; font-weight: normal; opacity: 0.7; }
            #trix-close-btn { cursor: pointer; font-size: 20px; font-weight: bold; padding: 0 5px; }
            #trix-close-btn:hover { color: #ff5555; }

            #trix-content { padding: 0 15px 15px 15px; flex-grow: 1; display: flex; flex-direction: column; }
            .trix-tabs { display: flex; flex-wrap: wrap; border-bottom: 1px solid #555; }
            .trix-tab { background: #2a2a30; padding: 8px 12px; cursor: pointer; border-radius: 5px 5px 0 0; margin-right: 4px; position: relative; transition: background 0.2s; }
            .trix-tab:hover { background: #3a3a42; }
            .trix-tab.active { background: #1e1e22; font-weight: bold; color: #00aaff; }
            .trix-tab-name { padding-right: 15px; }
            .trix-tab-close { position: absolute; top: 50%; right: 5px; transform: translateY(-50%); font-size: 14px; opacity: 0.6; }
            .trix-tab-close:hover { opacity: 1; color: #ff5555; }
            #trix-new-tab-btn { background: none; border: none; color: #00aaff; font-size: 20px; cursor: pointer; padding: 5px 10px; }

            .trix-view { display: flex; flex-direction: column; flex-grow: 1; margin-top: 10px; }
            .trix-editor-area { position: relative; flex-grow: 1; margin-top: -1px; background: #2d2d2d; border: 1px solid #555; border-radius: 0 0 5px 5px; }
            .trix-editor-area textarea, .trix-editor-area pre { margin: 0; padding: 10px; font-family: 'Fira Code', 'Consolas', monospace; font-size: 14px; line-height: 1.5; white-space: pre; word-wrap: normal; width: 100%; height: 100%; box-sizing: border-box; position: absolute; top: 0; left: 0; overflow: auto; }
            .trix-editor-area textarea { z-index: 1; background: transparent; color: inherit; resize: none; border: none; outline: none; -webkit-text-fill-color: transparent; }
            .trix-editor-area pre { z-index: 0; pointer-events: none; }

            .trix-action-bar { display: flex; gap: 10px; margin-top: 10px; }
            .trix-button { background-color: #007bff; color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 5px; transition: background-color 0.2s; flex-grow: 1; }
            #trix-execute-btn { background-color: #28a745; }
            .trix-status-bar { margin-top: 10px; padding: 5px; background: rgba(0,0,0,0.2); font-size: 12px; border-radius: 3px; min-height: 1em; }
            .trix-status-success { color: #28a745; }
            .trix-status-error { color: #dc3545; }

            #trix-packet-log-view { flex-grow: 1; display: flex; flex-direction: column; }
            #trix-packet-log { flex-grow: 1; background: #1e1e22; border: 1px solid #555; border-radius: 5px; padding: 10px; overflow-y: auto; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; }
            .packet-item { padding: 2px 0; border-bottom: 1px solid #333; }
            .packet-send { color: #7ec699; }
            .packet-receive { color: #cc99cd; }
            .packet-meta { opacity: 0.6; font-size: 10px; margin-right: 10px; }

            #trix-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 100000; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); }
            .trix-settings-content { background: #1e1e22; padding: 20px; border-radius: 8px; width: 300px; box-shadow: 0 0 20px rgba(0,0,0,0.5); animation: trix-fade-in 0.3s; }
        `;
        shadowRoot.appendChild(styleElement);

        const translations = {
            en: { toggleTitle: "Toggle TriX Executor (BETA)", headerTitle: "TriX Executor", versionPrefix: "(v{version})", fpsCounter: "FPS:", closeButton: "Close", executeButton: "Execute", clearButton: "Clear", statusBarReady: "Ready.", settingsButton: "Settings", settingsTitle: "Settings", themeLabel: "Theme:", languageLabel: "Language:", settingsCloseButton: "Close", newTabButton: "+", defaultTabName: "Welcome", defaultTabCode: "// Welcome to TriX Executor!\nconsole.log('Hello from TriX!');", packetLoggerTab: "Packet Logger", clearLogButton: "Clear Log", packetSend: "[SEND]", packetReceive: "[RECEIVE]", statusSuccess: "Success: Executed '{tabName}' at {time}", statusError: "Error: {errorMessage}", statusNothing: "Nothing to execute.", langName: "English" },
            ru: { toggleTitle: "Переключить TriX Executor (БЕТА)", headerTitle: "TriX Executor", versionPrefix: "(v{version})", fpsCounter: "Кадров/с:", closeButton: "Закрыть", executeButton: "Выполнить", clearButton: "Очистить", statusBarReady: "Готово.", settingsButton: "Настройки", settingsTitle: "Настройки", themeLabel: "Тема:", languageLabel: "Язык:", settingsCloseButton: "Закрыть", newTabButton: "+", defaultTabName: "Приветствие", defaultTabCode: "// Добро пожаловать в TriX Executor!\nconsole.log('Привет от TriX!');", packetLoggerTab: "Логгер пакетов", clearLogButton: "Очистить лог", packetSend: "[ОТПР]", packetReceive: "[ПОЛУЧ]", statusSuccess: "Успех: Выполнено '{tabName}' в {time}", statusError: "Ошибка: {errorMessage}", statusNothing: "Нечего выполнять.", langName: "Русский" },
            fr: { toggleTitle: "Basculer TriX Executor (BÊTA)", headerTitle: "TriX Executor", versionPrefix: "(v{version})", fpsCounter: "IPS:", closeButton: "Fermer", executeButton: "Exécuter", clearButton: "Effacer", statusBarReady: "Prêt.", settingsButton: "Paramètres", settingsTitle: "Paramètres", themeLabel: "Thème:", languageLabel: "Langue:", settingsCloseButton: "Fermer", newTabButton: "+", defaultTabName: "Bienvenue", defaultTabCode: "// Bienvenue dans TriX Executor !\nconsole.log('Bonjour de TriX !');", packetLoggerTab: "Logger de paquets", clearLogButton: "Effacer le log", packetSend: "[ENVOI]", packetReceive: "[RÉCEP]", statusSuccess: "Succès : '{tabName}' exécuté à {time}", statusError: "Erreur : {errorMessage}", statusNothing: "Rien à exécuter.", langName: "Français" },
            pl: { toggleTitle: "Przełącz TriX Executor (BETA)", headerTitle: "TriX Executor", versionPrefix: "(v{version})", fpsCounter: "Kl/s:", closeButton: "Zamknij", executeButton: "Wykonaj", clearButton: "Wyczyść", statusBarReady: "Gotowy.", settingsButton: "Ustawienia", settingsTitle: "Ustawienia", themeLabel: "Motyw:", languageLabel: "Język:", settingsCloseButton: "Zamknij", newTabButton: "+", defaultTabName: "Witamy", defaultTabCode: "// Witaj w TriX Executor!\nconsole.log('Pozdrowienia od TriX!');", packetLoggerTab: "Logger pakietów", clearLogButton: "Wyczyść log", packetSend: "[WYŚLIJ]", packetReceive: "[ODBIÓR]", statusSuccess: "Sukces: Wykonano '{tabName}' o {time}", statusError: "Błąd: {errorMessage}", statusNothing: "Brak kodu do wykonania.", langName: "Polski" }
        };

        let state = {
            tabs: [], activeTabId: null,
            settings: { theme: 'dark-knight', language: 'en', position: { top: '80px', left: 'auto', right: '15px' }, size: { width: '450px', height: '400px' } }
        };

        const t = (key, replacements = {}) => {
            const lang = state.settings.language;
            let str = (translations[lang] && translations[lang][key]) || translations.en[key] || key;
            Object.entries(replacements).forEach(([placeholder, value]) => { str = str.replace(`{${placeholder}}`, value); });
            return str;
        };

        const $ = (selector, parent = shadowRoot) => parent.querySelector(selector);
        const debounce = (func, delay) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; };

        function loadState() {
            const savedState = GM_getValue('trixExecutorState');
            if (savedState) state = { ...state, ...savedState };
            const defaultTabs = [
                { id: 'logger', name: t('packetLoggerTab'), code: null, isPermanent: true },
                { id: Date.now(), name: t('defaultTabName'), code: t('defaultTabCode') }
            ];
            if (!state.tabs || state.tabs.length === 0) state.tabs = defaultTabs;
            if (!state.tabs.find(t => t.id === 'logger')) state.tabs.unshift(defaultTabs[0]); else state.tabs[0].name = t('packetLoggerTab');
            if (!state.activeTabId || !state.tabs.find(t => t.id === state.activeTabId)) state.activeTabId = state.tabs[0].id;
        }

        const saveState = debounce(() => {
            const container = $('#trix-container');
            if (container) {
                state.settings.position = { top: container.style.top, left: container.style.left, right: container.style.right, bottom: 'auto' };
                state.settings.size = { width: container.style.width, height: container.style.height };
            }
            GM_setValue('trixExecutorState', state);
        }, 500);

        let ui, editor;

        function reRenderUI() {
            const host = document.getElementById('trix-host');
            if (host) host.remove();
            initialize();
        }

        function createUI() {
            const toggleBtn = document.createElement('div');
            toggleBtn.id = 'trix-toggle-btn'; toggleBtn.title = t('toggleTitle'); toggleBtn.innerHTML = 'X';
            const container = document.createElement('div');
            container.id = 'trix-container'; container.classList.add('hidden');
            container.innerHTML = `
                <div id="trix-header">
                    <div><span class="trix-title-3d">${t('headerTitle')}</span><span class="trix-version-tag">${t('versionPrefix', {version: GM_info.script.version})}</span></div>
                    <div id="trix-header-controls"><span id="trix-fps-display"></span><span id="trix-close-btn" title="${t('closeButton')}">✖</span></div>
                </div>
                <div id="trix-content"></div>
                <div id="trix-footer" style="padding:10px; text-align:center; background:rgba(0,0,0,0.2);">
                     <button id="trix-settings-btn" class="trix-button" style="flex-grow:0; padding: 5px 15px;">${t('settingsButton')}</button>
                </div>`;
            shadowRoot.append(toggleBtn, container);
            return { toggleBtn, container };
        }

        function renderTabs() {
            const tabsContainer = $('.trix-tabs');
            if (!tabsContainer) return;
            tabsContainer.innerHTML = '';
            state.tabs.forEach(tab => {
                const tabEl = document.createElement('div');
                tabEl.className = 'trix-tab'; tabEl.dataset.tabId = tab.id;
                if (tab.id === state.activeTabId) tabEl.classList.add('active');
                tabEl.innerHTML = `<span class="trix-tab-name">${tab.name}</span>` + (!tab.isPermanent ? `<span class="trix-tab-close">x</span>` : '');
                tabsContainer.appendChild(tabEl);
            });
            const newTabBtn = document.createElement('button');
            newTabBtn.id = 'trix-new-tab-btn'; newTabBtn.textContent = t('newTabButton');
            tabsContainer.appendChild(newTabBtn);
            renderEditor();
        }

        function renderActiveView() {
            const content = $('#trix-content');
            const activeTab = state.tabs.find(t => t.id === state.activeTabId);
            content.innerHTML = '';

            if (activeTab.id === 'logger') {
                content.innerHTML = `
                    <div id="trix-packet-log-view" class="trix-view">
                        <div class="trix-tabs"></div><div id="trix-packet-log"></div>
                        <div class="trix-action-bar"><button id="trix-clear-log-btn" class="trix-button">${t('clearLogButton')}</button></div>
                    </div>`;
                $('#trix-clear-log-btn').addEventListener('click', () => $('#trix-packet-log').innerHTML = '');
                renderTabs();
            } else {
                content.innerHTML = `
                    <div id="trix-injector-container" class="trix-view">
                        <div class="trix-tabs"></div><div class="trix-editor-area"></div>
                        <div class="trix-action-bar">
                            <button id="trix-execute-btn" class="trix-button">${t('executeButton')}</button>
                            <button id="trix-clear-btn" class="trix-button">${t('clearButton')}</button>
                        </div>
                        <div class="trix-status-bar">${t('statusBarReady')}</div>
                    </div>`;
                renderTabs();
            }
        }

        function renderEditor() {
            const editorArea = $('.trix-editor-area');
            if (!editorArea) { editor = null; return; }
            const activeTab = state.tabs.find(t => t.id === state.activeTabId);
            if (!activeTab) { editorArea.innerHTML = ''; editor = null; return; }

            editorArea.innerHTML = `<textarea spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off"></textarea><pre class="language-js"><code></code></pre>`;
            editor = { textarea: $('textarea', editorArea), pre: $('pre', editorArea), code: $('code', editorArea) };
            editor.textarea.value = activeTab.code;
            highlightCode(activeTab.code);
            addEditorEventListeners();
        }

        function highlightCode(code) { if (editor) editor.code.innerHTML = Prism.highlight(code + '\n', Prism.languages.javascript, 'javascript'); }

        function addEditorEventListeners() {
            if (!editor) return;
            editor.textarea.addEventListener('input', () => {
                const activeTab = state.tabs.find(t => t.id === state.activeTabId);
                if (activeTab) { activeTab.code = editor.textarea.value; highlightCode(activeTab.code); saveState(); }
            });
            editor.textarea.addEventListener('scroll', () => { if (editor) { editor.pre.scrollTop = editor.textarea.scrollTop; editor.pre.scrollLeft = editor.textarea.scrollLeft; }});
            editor.textarea.addEventListener('keydown', e => {
                if (e.key === 'Tab') {
                    e.preventDefault(); const s = e.target.selectionStart, end = e.target.selectionEnd;
                    e.target.value = e.target.value.substring(0, s) + '  ' + e.target.value.substring(end);
                    e.target.selectionStart = e.target.selectionEnd = s + 2;
                    editor.textarea.dispatchEvent(new Event('input'));
                }
            });
        }

        function initEventListeners() {
            ui.toggleBtn.addEventListener('click', () => ui.container.classList.toggle('hidden'));
            $('#trix-close-btn').addEventListener('click', () => ui.container.classList.add('hidden'));
            initDraggable(ui.container, $('#trix-header'));
            ui.container.addEventListener('click', handleContainerClick);
            $('#trix-settings-btn').addEventListener('click', showSettingsModal);
            const resizeObserver = new ResizeObserver(debounce(saveState, 500));
            resizeObserver.observe(ui.container);
        }

        function handleContainerClick(e) {
            const target = e.target;
            const tabEl = target.closest('.trix-tab');
            if (tabEl && !target.classList.contains('trix-tab-close')) {
                const tabId = tabEl.dataset.tabId === 'logger' ? 'logger' : parseInt(tabEl.dataset.tabId, 10);
                if (tabId !== state.activeTabId) { state.activeTabId = tabId; renderActiveView(); saveState(); }
            }
            if (target.classList.contains('trix-tab-close')) {
                const tabId = parseInt(tabEl.dataset.tabId, 10);
                state.tabs = state.tabs.filter(t => t.id !== tabId);
                if (state.activeTabId === tabId) state.activeTabId = state.tabs[0].id;
                renderActiveView(); saveState();
            }
            if (target.id === 'trix-new-tab-btn') {
                const newId = Date.now(), newName = `Script ${state.tabs.length}`;
                state.tabs.push({ id: newId, name: newName, code: `// ${newName}` });
                state.activeTabId = newId;
                renderActiveView(); saveState();
            }
            if (target.id === 'trix-execute-btn') executeScript();
            if (target.id === 'trix-clear-btn') {
                 const activeTab = state.tabs.find(t => t.id === state.activeTabId);
                 if (activeTab) { activeTab.code = ''; renderEditor(); saveState(); }
            }
        }

        function initDraggable(container, handle) {
            let isDragging = false, offsetX, offsetY;
            handle.addEventListener('mousedown', e => {
                if (e.target.closest('#trix-header-controls') || e.target.closest('.trix-title-3d')) return;
                isDragging = true; offsetX = e.clientX - container.offsetLeft; offsetY = e.clientY - container.offsetTop;
                container.style.right = 'auto'; container.style.bottom = 'auto'; document.body.style.userSelect = 'none';
            });
            document.addEventListener('mousemove', e => { if (isDragging) { container.style.left = `${e.clientX - offsetX}px`; container.style.top = `${e.clientY - offsetY}px`; } });
            document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; document.body.style.userSelect = ''; saveState(); } });
        }

        function executeScript() {
            const statusBar = $('.trix-status-bar');
            if (!statusBar) return;
            const activeTab = state.tabs.find(t => t.id === state.activeTabId);
            if (!activeTab || !activeTab.code) { statusBar.textContent = t('statusNothing'); statusBar.className = 'trix-status-bar'; return; }
            try {
                new Function(activeTab.code)();
                statusBar.textContent = t('statusSuccess', {tabName: activeTab.name, time: new Date().toLocaleTimeString()});
                statusBar.className = 'trix-status-bar trix-status-success';
            } catch (error) {
                console.error('TriX Executor Error:', error);
                statusBar.textContent = t('statusError', {errorMessage: error.message});
                statusBar.className = 'trix-status-bar trix-status-error';
            }
        }

        function showSettingsModal() {
            const modal = document.createElement('div');
            modal.id = 'trix-settings-modal';
            modal.innerHTML = `
                <div class="trix-settings-content">
                    <h3>${t('settingsTitle')}</h3>
                    <label for="trix-theme-select">${t('themeLabel')}</label>
                    <select id="trix-theme-select">
                        <option value="dark-knight">Dark Knight</option> <option value="arctic-light">Arctic Light</option> <option value="crimson">Crimson</option>
                    </select>
                    <label for="trix-lang-select">${t('languageLabel')}</label>
                    <select id="trix-lang-select">
                        <option value="en">${translations.en.langName}</option> <option value="ru">${translations.ru.langName}</option>
                        <option value="fr">${translations.fr.langName}</option> <option value="pl">${translations.pl.langName}</option>
                    </select>
                    <button id="trix-settings-close">${t('settingsCloseButton')}</button>
                </div>`;
            shadowRoot.appendChild(modal);
            $('#trix-theme-select').value = state.settings.theme;
            $('#trix-lang-select').value = state.settings.language;
            $('#trix-theme-select').addEventListener('change', e => { state.settings.theme = e.target.value; applySettings(); saveState(); });
            $('#trix-lang-select').addEventListener('change', e => {
                state.settings.language = e.target.value;
                saveState();
                modal.remove();
                reRenderUI();
            });
            $('#trix-settings-close').addEventListener('click', () => modal.remove());
            modal.addEventListener('click', e => { if (e.target.id === 'trix-settings-modal') modal.remove(); });
        }

        function applySettings() {
            const container = $('#trix-container');
            container.dataset.theme = state.settings.theme;
            Object.assign(container.style, state.settings.position, state.settings.size);
        }

        logPacketCallback = function(type, data) {
            const logContainer = $('#trix-packet-log');
            if (!logContainer) return;
            const item = document.createElement('div');
            item.className = `packet-item packet-${type}`;
            let formattedData = data;
            if (data instanceof ArrayBuffer) formattedData = `[ArrayBuffer, ${data.byteLength} bytes]`;
            else if (data instanceof Blob) formattedData = `[Blob, ${data.size} bytes, type: ${data.type}]`;
            const meta = type === 'send' ? t('packetSend') : t('packetReceive');
            item.innerHTML = `<span class="packet-meta">${meta}</span> ${formattedData}`;
            logContainer.appendChild(item);
            logContainer.scrollTop = logContainer.scrollHeight;
        };

        loadState();
        ui = createUI();
        applySettings();
        renderActiveView();
        initEventListeners();

        let lastFrameTime = performance.now(), frameCount = 0;
        const fpsDisplay = $('#trix-fps-display');
        function updateFPS(now) {
            frameCount++;
            if (now >= lastFrameTime + 1000) { fpsDisplay.textContent = `${t('fpsCounter')} ${frameCount}`; lastFrameTime = now; frameCount = 0; }
            requestAnimationFrame(updateFPS);
        }
        requestAnimationFrame(updateFPS);
    }

    // --- Robust Element Loading with Retries ---
    function waitForElement(selector, callback) {
        const maxRetries = 40; // 40 * 250ms = 10 seconds
        const retryInterval = 250;
        let retryCount = 0;
        const checkInterval = setInterval(() => {
            // FIX: Check for document.body first
            if (document.body && document.querySelector(selector)) {
                console.log(`[TriX] Target element '${selector}' found. Initializing script.`);
                clearInterval(checkInterval);
                callback();
            } else {
                retryCount++;
                console.log(`[TriX] Waiting for page elements... Retry ${retryCount}/${maxRetries}...`);
                if (retryCount >= maxRetries) {
                    clearInterval(checkInterval);
                    console.error(`[TriX] Failed to find target element '${selector}' after ${maxRetries} retries. Aborting.`);
                }
            }
        }, retryInterval);
    }

    waitForElement('#canvasA', initialize);
})();