TriX Executor (BETA) for Territorial.io

A powerful, multi-tabbed, persistent code execution environment for Territorial.io developers.

目前為 2025-09-11 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         TriX Executor (BETA) for Territorial.io
// @namespace    https://greasyfork.org/en/users/your-username
// @version      Beta-Draco-2023.10.28
// @description  A powerful, multi-tabbed, persistent code execution environment for Territorial.io developers.
// @author       YourName
// @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_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwYWFmZiI+PHBhdGggZD0iTTE4LjM2IDIyLjA1bC00LjQyLTQuNDJDMTMuNDcgMTcuODcgMTMgMTguMTcgMTMgMTguNUMxMyAyMC45OSAxNS4wMSAyMyAxNy41IDIzaDMuNTRjLTIuNDUtMS40OC00LjQyLTMuNDUtNS45LTUuOTV6TTggMTNjMS42NiAwIDMuMTgtLjU5IDQuMzgtMS42MkwxMCAxMy41VjIybDIuNS0yLjVMMTggMTMuOTZjLjM1LS40OC42NS0xIC44Ny0xLjU1QzE4LjYxIDEzLjQxIDE4IDEyLjM0IDE4IDEyVjBoLTJ2MTJjMCAuMzQtLjAyLjY3LS4wNiAxLS4zMy4xOC0uNjguMy0xLjA0LjM3LTEuNzMgMC0zLjI3LS45My00LjE2LTIuMzZMNiAxMy42VjVINHY4eiIvPjwvc3ZnPg==
// ==/UserScript==

/* global Prism */

(function() {
    'use strict';

    // --- 1. STYLESHEET INJECTION ---
    GM_addStyle(`
        /* PrismJS Tomorrow Night 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;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}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.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}

        /* --- 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 { 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-container[data-theme='crimson'] #trix-new-tab-btn, #trix-container[data-theme='crimson'] #trix-execute-btn { color: #ffc4c4; }

        #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-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-injector-container { display: flex; flex-direction: column; flex-grow: 1; margin-top: 10px; }
        .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-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-button:hover { background-color: #0069d9; }
        #trix-execute-btn { background-color: #28a745; }
        #trix-execute-btn:hover { background-color: #218838; }

        .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-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; }
        .trix-settings-content h3 { margin-top: 0; }
        .trix-settings-content select, .trix-settings-content button { width: 100%; padding: 8px; margin-top: 10px; border-radius: 5px; border: 1px solid #555; background: #2d2d2d; color: #fff; }
    `);

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

    // --- 3. HELPER FUNCTIONS ---
    const $ = (selector, parent = document) => parent.querySelector(selector);
    const debounce = (func, delay) => {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), delay);
        };
    };

    // --- 4. PERSISTENCE LAYER ---
    function loadState() {
        const savedState = GM_getValue('trixExecutorState');
        if (savedState) {
            state = { ...state, ...savedState };
        }
        if (!state.tabs || state.tabs.length === 0) {
            state.tabs = [{ id: Date.now(), name: "Welcome", code: "// Welcome to TriX Executor!\n// Press 'Execute' to run this code.\nconsole.log('Hello from TriX!');\nalert('Execution successful!');" }];
            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', transform: 'none' };
             state.settings.size = { width: container.style.width, height: container.style.height };
        }
        GM_setValue('trixExecutorState', state);
    }, 500);

    // --- 5. UI RENDERING & LOGIC ---
    let ui, editor;

    function createUI() {
        const toggleBtn = document.createElement('div');
        toggleBtn.id = 'trix-toggle-btn';
        toggleBtn.title = 'Toggle TriX Executor (BETA)';
        toggleBtn.innerHTML = 'X';

        const container = document.createElement('div');
        container.id = 'trix-container';
        container.classList.add('hidden');
        container.innerHTML = `
            <div id="trix-header">
                <span>TriX Executor (BETA) v${GM_info.script.version}</span>
                <div id="trix-header-controls">
                    <span id="trix-fps-display"></span>
                    <span id="trix-close-btn" title="Close">✖</span>
                </div>
            </div>
            <div id="trix-content">
                <div id="trix-injector-container">
                    <div class="trix-tabs"></div>
                    <div class="trix-editor-area"></div>
                    <div class="trix-action-bar">
                        <button id="trix-execute-btn" class="trix-button">Execute</button>
                        <button id="trix-clear-btn" class="trix-button">Clear</button>
                    </div>
                    <div class="trix-status-bar">Ready.</div>
                </div>
            </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;">Settings</button>
            </div>
        `;
        document.body.append(toggleBtn, container);
        return { toggleBtn, container };
    }

    function renderTabs() {
        const tabsContainer = $('.trix-tabs', ui.container);
        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><span class="trix-tab-close">x</span>`;
            tabsContainer.appendChild(tabEl);
        });
        const newTabBtn = document.createElement('button');
        newTabBtn.id = 'trix-new-tab-btn';
        newTabBtn.textContent = '+';
        tabsContainer.appendChild(newTabBtn);
        renderEditor();
    }

    function renderEditor() {
        const editorArea = $('.trix-editor-area', ui.container);
        const activeTab = state.tabs.find(t => t.id === state.activeTabId);
        if (!activeTab) {
            editorArea.innerHTML = ''; editor = null; return;
        }
        if (!editor) {
            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) };
            addEditorEventListeners();
        }
        editor.textarea.value = activeTab.code;
        highlightCode(activeTab.code);
    }

    function highlightCode(code) {
        if (!editor) return;
        editor.code.innerHTML = Prism.highlight(code + '\n', Prism.languages.javascript, 'javascript');
        editor.pre.scrollTop = editor.textarea.scrollTop;
        editor.pre.scrollLeft = editor.textarea.scrollLeft;
    }

    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', () => {
            editor.pre.scrollTop = editor.textarea.scrollTop;
            editor.pre.scrollLeft = editor.textarea.scrollLeft;
        });
        editor.textarea.addEventListener('keydown', e => {
            if (e.key === 'Tab') {
                e.preventDefault();
                const start = e.target.selectionStart, end = e.target.selectionEnd;
                e.target.value = e.target.value.substring(0, start) + '  ' + e.target.value.substring(end);
                e.target.selectionStart = e.target.selectionEnd = start + 2;
                editor.textarea.dispatchEvent(new Event('input'));
            }
        });
    }

    // --- 6. EVENT HANDLERS & CORE LOGIC ---
    function initEventListeners() {
        ui.toggleBtn.addEventListener('click', () => ui.container.classList.toggle('hidden'));
        $('#trix-close-btn', ui.container).addEventListener('click', () => ui.container.classList.add('hidden'));

        initDraggable(ui.container, $('#trix-header', ui.container));
        initTabLogic();

        $('#trix-execute-btn').addEventListener('click', executeScript);
        $('#trix-clear-btn').addEventListener('click', () => {
             const activeTab = state.tabs.find(t => t.id === state.activeTabId);
             if(activeTab) { activeTab.code = ''; renderEditor(); saveState(); }
        });
        $('#trix-settings-btn').addEventListener('click', showSettingsModal);

        const resizeObserver = new ResizeObserver(debounce(saveState, 500));
        resizeObserver.observe(ui.container);
    }

    function initDraggable(container, handle) {
        let isDragging = false, offsetX, offsetY;
        handle.addEventListener('mousedown', e => {
            if (e.target.id === 'trix-close-btn') 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 initTabLogic() {
        $('.trix-tabs', ui.container).addEventListener('click', e => {
            const target = e.target;
            const tabEl = target.closest('.trix-tab');

            if (tabEl && !target.classList.contains('trix-tab-close')) {
                const tabId = parseInt(tabEl.dataset.tabId, 10);
                if (tabId !== state.activeTabId) { state.activeTabId = tabId; renderTabs(); 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.length > 0 ? state.tabs[0].id : null;
                }
                if (state.tabs.length === 0) editor = null;
                renderTabs(); saveState();
            }
            if (target.id === 'trix-new-tab-btn') {
                const newId = Date.now();
                const newName = `Script ${state.tabs.length + 1}`;
                state.tabs.push({ id: newId, name: newName, code: `// ${newName}` });
                state.activeTabId = newId;
                renderTabs(); saveState();
            }
        });
    }

    function executeScript() {
        const statusBar = $('.trix-status-bar', ui.container);
        const activeTab = state.tabs.find(t => t.id === state.activeTabId);
        if (!activeTab || !activeTab.code) {
            statusBar.textContent = 'Nothing to execute.'; statusBar.className = 'trix-status-bar'; return;
        }
        try {
            new Function(activeTab.code)();
            statusBar.textContent = `Success: Executed '${activeTab.name}' at ${new Date().toLocaleTimeString()}`;
            statusBar.className = 'trix-status-bar trix-status-success';
        } catch (error) {
            console.error('TriX Executor Error:', error);
            statusBar.textContent = `Error: ${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>Settings</h3>
                <label for="trix-theme-select">Theme:</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>
                <button id="trix-settings-close">Close</button>
            </div>`;
        document.body.appendChild(modal);
        $('#trix-theme-select').value = state.settings.theme;
        $('#trix-theme-select').addEventListener('change', e => {
            state.settings.theme = e.target.value; applySettings(); saveState();
        });
        $('#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);
    }

    // --- 7. INITIALIZATION ---
    function init() {
        loadState();
        ui = createUI();
        applySettings();
        renderTabs();
        initEventListeners();

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

    const observer = new MutationObserver((mutations, obs) => {
        if (document.getElementById('canvasA')) {
            console.log('TriX Executor: Canvas detected. Initializing.');
            init();
            obs.disconnect();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();