TriX Executor (BETA) for Territorial.io

Work in progress...

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

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         TriX Executor (BETA) for Territorial.io
// @namespace    https://greasyfork.org/en/users/COURTESYCOIL
// @version      Beta-Draco-2024.09.15
// @description  Work in progress...
// @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
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/audiomotion-analyzer.min.js
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwYWFmZiI+PHBhdGggZD0iTTE4LjM2IDIyLjA1bC00LjQyLTQuNDJDMTMuNDcgMTcuODcgMTMgMTguMTcgMTMgMTguNUMxMyAyMC45OSAxNS4wMSAyMyAxNy41IDIzaDMuNTRjLTIuNDUtMS40OC00LjQyLTMuNDUtNS5LTUuOTV6TTggMTNjMS42NiAwIDMuMTgtLjU5IDQuMzgtMS42MkwxMCAxMy41VjIybDIuNS0yLjVMMTggMTMuOTZjLjM1LS40OC42NS0xIC44Ny0xLjU1QzE4LjYxIDEzLjQxIDE4IDEyLjM0IDE4IDEyVjBoLTJ2MTJjMCAuMzQtLjAyLjY3LS4wNiAxLS4zMy4xOC0uNjguMy0xLjA0LjM3LTEuNzMgMC0zLjI3LS45My00LjE2LTIuMzZMNiAxMy42VjVINXY4eiIvPjwvc3ZnPg==
// ==/UserScript==

/* global Prism, unsafeWindow, AudioMotionAnalyzer */

(function() {
    'use strict';

    // --- Global State for Advanced Features ---
    let interceptionEnabled = false;
    let queuedMessages = new Map();
    let renderInterceptorQueue = () => {};
    let customWs = null;

    let updateConnectionStatus = () => {};
    let updatePingDisplay = () => {};
    let logPacketCallback = () => {};

    // --- WebSocket Proxy Setup ---
    const OriginalWebSocket = unsafeWindow.WebSocket;
    unsafeWindow.WebSocket = function(url, protocols) {
        if (!url.includes('/s52/')) { return new OriginalWebSocket(url, protocols); }
        console.log(`[TriX] Intercepting WebSocket connection to: ${url}`);
        const ws = new OriginalWebSocket(url, protocols);
        let originalOnMessageHandler = null;
        const originalSend = ws.send.bind(ws);
        ws.send = function(data) {
            if (interceptionEnabled) {
                const messageId = `send-${Date.now()}-${Math.random()}`;
                const promise = new Promise(resolve => queuedMessages.set(messageId, { direction: 'send', data, resolve }));
                renderInterceptorQueue();
                promise.then(decision => {
                    queuedMessages.delete(messageId);
                    renderInterceptorQueue();
                    if (decision.action === 'forward') {
                        originalSend(decision.data);
                        logPacketCallback('send', `[Forwarded] ${decision.data}`);
                    } else { logPacketCallback('send', `[Blocked] ${data}`); }
                });
            } else { logPacketCallback('send', data); return originalSend(data); }
        };
        Object.defineProperty(ws, 'onmessage', {
            get: () => originalOnMessageHandler,
            set: (handler) => {
                originalOnMessageHandler = handler;
                ws.addEventListener('message', event => {
                    if (interceptionEnabled) {
                        const messageId = `recv-${Date.now()}-${Math.random()}`;
                        const promise = new Promise(resolve => queuedMessages.set(messageId, { direction: 'receive', data: event.data, resolve }));
                        renderInterceptorQueue();
                        promise.then(decision => {
                            queuedMessages.delete(messageId);
                            renderInterceptorQueue();
                            if (decision.action === 'forward') {
                                logPacketCallback('receive', `[Forwarded] ${decision.data}`);
                                if (originalOnMessageHandler) originalOnMessageHandler({ data: decision.data });
                            } else { logPacketCallback('receive', `[Blocked] ${event.data}`); }
                        });
                    } else { logPacketCallback('receive', event.data); if (originalOnMessageHandler) originalOnMessageHandler(event); }
                });
            },
            configurable: true
        });
        ws.addEventListener('open', () => updateConnectionStatus('connected', 'Connection established.'));
        ws.addEventListener('close', (event) => {
            if (unsafeWindow.webSocketManager && unsafeWindow.webSocketManager.activeSocket === ws) unsafeWindow.webSocketManager.activeSocket = null;
            updateConnectionStatus('disconnected', `Disconnected. Code: ${event.code}`);
            updatePingDisplay('---');
        });
        ws.addEventListener('error', () => updateConnectionStatus('error', 'A connection error occurred.'));
        unsafeWindow.webSocketManager = { activeSocket: ws };
        return ws;
    };


    // --- Main script logic ---
    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,.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}
            
            /* --- Refined UI Theme --- */
            :host { --accent-color: #3b82f6; --bg-darker: #1e1e22; --bg-dark: #2a2a30; --border-color: #444; }
            @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: 500px; min-height: 450px; z-index: 99998; color: #e5e7eb; font-family: 'Segoe UI', 'Roboto', sans-serif; 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; background-color: rgba(30, 30, 34, 0.9); border: 1px solid var(--border-color); }
            #trix-container.hidden { display: none; }
            #trix-header, #trix-footer { cursor: move; user-select: none; flex-shrink: 0; }
            #trix-header { padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; background-color: rgba(0,0,0,0.3); }
            .trix-title { font-size: 16px; font-weight: 600; }
            .trix-version-tag { font-size: 10px; font-weight: 300; opacity: 0.7; margin-left: 8px; }
            #trix-header-controls { display: flex; align-items: center; gap: 15px; font-size: 12px; }
            #trix-close-btn { cursor: pointer; font-size: 20px; font-weight: bold; opacity: 0.7; }
            #trix-close-btn:hover { opacity: 1; color: #ef4444; }

            #trix-conn-status { width: 10px; height: 10px; border-radius: 50%; }
            #trix-conn-status.connected { background-color: #28a745; } #trix-conn-status.disconnected { background-color: #dc3545; } #trix-conn-status.connecting { background-color: #ffc107; }
            .ping-good { color: #28a745; } .ping-ok { color: #ffc107; } .ping-bad { color: #dc3545; }

            #trix-content { padding: 0 15px 15px 15px; flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; background-color: var(--bg-darker); }
            .trix-tabs { display: flex; flex-wrap: wrap; flex-shrink: 0; }
            .trix-tab { background: var(--bg-dark); color: #9ca3af; padding: 8px 12px; cursor: pointer; border-top: 2px solid transparent; border-left: 1px solid transparent; border-right: 1px solid transparent; border-radius: 5px 5px 0 0; margin-right: 2px; position: relative; }
            .trix-tab.active { background: var(--bg-darker); font-weight: 600; color: #e5e7eb; border-top-color: var(--accent-color); }
            .trix-tab-name { padding-right: 15px; } .trix-tab-close { position: absolute; top: 50%; right: 5px; transform: translateY(-50%); opacity: 0.6; } .trix-tab-close:hover { opacity: 1; color: #ef4444; }
            #trix-new-tab-btn { background: none; border: none; color: #9ca3af; font-size: 20px; cursor: pointer; padding: 5px 10px; }
            .trix-tab-rename-input { background: transparent; border: 1px solid var(--accent-color); color: inherit; font-family: inherit; font-size: inherit; padding: 0; margin: 0; width: 100px; }

            .trix-view { display: flex; flex-direction: column; flex-grow: 1; padding-top: 10px; overflow: hidden; }
            .trix-action-bar { display: flex; gap: 10px; margin-top: 10px; flex-shrink: 0; }
            .trix-button { background-color: var(--accent-color); color: white; border: none; padding: 10px 15px; cursor: pointer; border-radius: 6px; flex-grow: 1; transition: filter 0.2s; font-weight: 600; }
            .trix-button:hover { filter: brightness(1.1); }
            .trix-button:disabled { background-color: #555; cursor: not-allowed; filter: none; }
            .trix-input { background: #2d2d2d; border: 1px solid var(--border-color); color: #ccc; padding: 8px; border-radius: 5px; font-family: monospace; box-sizing: border-box; }
            .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-welcome-view h2 { margin-top: 0; color: #fff; }
            #trix-welcome-view p { color: #d1d5db; line-height: 1.6; }
            #trix-welcome-view code { background: var(--bg-dark); padding: 2px 5px; border-radius: 4px; font-family: Consolas, monospace; }

            #trix-music-toggle-btn { position: fixed; bottom: 20px; left: 20px; z-index: 99999; width: 50px; height: 50px; background-color: rgba(30, 30, 34, 0.8); color: #e5e7eb; border: 1px solid var(--border-color); border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); transition: all 0.3s ease; }
            #trix-music-toggle-btn:hover { transform: scale(1.1); box-shadow: 0 0 10px var(--accent-color); color: var(--accent-color); }
            #trix-music-player { display: flex; flex-direction: column; position: fixed; bottom: 80px; left: 20px; width: 300px; background: rgba(30, 30, 34, 0.9); border: 1px solid var(--border-color); backdrop-filter: blur(10px); border-radius: 8px; padding: 15px; z-index: 99998; box-shadow: 0 5px 15px rgba(0,0,0,0.3); opacity: 0; transform: scale(0.95) translateY(10px); transition: opacity 0.3s ease, transform 0.3s ease; pointer-events: none; }
            #trix-music-player.visible { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; }
            .music-drag-handle { position: absolute; top: 0; left: 0; width: 100%; height: 20px; cursor: move; }
            .music-info { text-align: center; margin-bottom: 10px; }
            .music-title { font-size: 16px; font-weight: 600; display: block; }
            .music-artist { font-size: 12px; color: #9ca3af; }
            #music-visualizer-container { position: relative; width: 100%; height: 60px; margin-bottom: 10px; border-radius: 4px; overflow:hidden; }
            .music-vis-controls { position: absolute; top: 2px; left: 2px; right: 2px; display: flex; justify-content: space-between; opacity: 0; transition: opacity 0.3s ease; }
            #music-visualizer-container:hover .music-vis-controls { opacity: 1; }
            .music-vis-btn { background: rgba(0,0,0,0.4); border: none; color: white; cursor: pointer; padding: 2px 6px; border-radius: 4px; }
            #music-vis-mode-name { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 12px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; }
            #music-vis-mode-name.visible { opacity: 1; }
            .music-progress-container { width: 100%; background: #4b5563; border-radius: 5px; cursor: pointer; height: 6px; margin-bottom: 5px; }
            .music-progress-bar { background: var(--accent-color); width: 0%; height: 100%; border-radius: 5px; }
            .music-time { display: flex; justify-content: space-between; font-size: 11px; color: #9ca3af; margin-bottom: 10px; }
            .music-controls { display: flex; justify-content: space-around; align-items: center; }
            .music-control-btn { background: none; border: none; color: #e5e7eb; cursor: pointer; padding: 5px; opacity: 0.8; transition: all 0.2s; }
            .music-control-btn:hover { opacity: 1; color: var(--accent-color); }
            .music-play-btn { font-size: 24px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; }
            .music-volume-container { display: flex; align-items: center; position: relative; }
            .music-volume-slider { -webkit-appearance: none; appearance: none; width: 80px; height: 5px; background: #4b5563; border-radius: 5px; outline: none; transition: opacity 0.3s; }
            .music-volume-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 12px; height: 12px; background: #e5e7eb; cursor: pointer; border-radius: 50%; }
            #trix-volume-indicator { position: absolute; top: -30px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.7); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
            #trix-volume-indicator.visible { opacity: 1; }
            #trix-music-menu { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 100000; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); opacity: 0; transition: opacity 0.3s; pointer-events: none; }
            #trix-music-menu.visible { opacity: 1; pointer-events: auto; }
            .music-menu-content { background: var(--bg-darker); border: 1px solid var(--border-color); width: 90%; max-width: 500px; max-height: 80%; border-radius: 8px; display: flex; flex-direction: column; }
            .music-menu-header { padding: 15px; border-bottom: 1px solid var(--border-color); }
            #music-search-input { width: 100%; box-sizing: border-box; }
            .music-song-list { overflow-y: auto; padding: 10px; }
            .music-song-item { padding: 10px 15px; cursor: pointer; border-radius: 4px; transition: background-color 0.2s; }
            .music-song-item:hover, .music-song-item.playing { background-color: var(--bg-dark); }
            .music-song-title { display: block; } .music-song-artist { font-size: 12px; color: #9ca3af; }
            
            /* ... (Other specific view styles are unchanged) ... */
        `;
        shadowRoot.appendChild(styleElement);

        let state = {
            tabs: [], activeTabId: null,
            settings: { position: { top: '80px', right: '15px' }, size: { width: '500px', height: '450px' } },
            wsClient: { url: 'wss://echo.websocket.events', protocols: '', savedMessages: { 'Example JSON': '{"action":"ping","id":123}'} },
            musicPlayer: { volume: 0.5, currentSongIndex: 0, visMode: 0, position: { bottom: '80px', left: '20px'} }
        };

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

        function loadState() {
            const savedState = GM_getValue('trixExecutorState');
            if (savedState) {
                state.tabs = savedState.tabs || [];
                state.activeTabId = savedState.activeTabId;
                state.settings = { ...state.settings, ...savedState.settings };
                state.wsClient = { ...state.wsClient, ...savedState.wsClient };
                state.musicPlayer = { ...state.musicPlayer, ...savedState.musicPlayer };
            }
            const defaultTabs = [
                { id: 'welcome', name: 'Welcome', isPermanent: true },
                { id: 'logger', name: 'Packet Logger', isPermanent: true },
                { id: 'interceptor', name: 'Interceptor', isPermanent: true },
                { id: 'ws_client', name: 'WS Client', isPermanent: true },
                { id: 'storage', name: 'Storage', isPermanent: true },
                { id: Date.now(), name: "My First Script", code: "// Welcome to TriX Executor!\nconsole.log('Hello from a user script!');" }
            ];
            ['welcome', 'logger', 'interceptor', 'ws_client', 'storage'].forEach((id, index) => {
                const defaultTab = defaultTabs.find(d => d.id === id);
                if (!state.tabs.find(t => t.id === id)) { state.tabs.splice(index, 0, defaultTab); }
            });
             if (!state.tabs.find(t => t.code)) { state.tabs.push(defaultTabs.find(t => t.code)); }
            if (!state.activeTabId || !state.tabs.find(t => t.id === state.activeTabId)) { state.activeTabId = state.tabs[0].id; }
        }
        
        const saveState = debounce(() => {
            const container = $('#trix-container');
            const musicPlayer = $('#trix-music-player');
            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 };
            }
            if (musicPlayer) {
                state.musicPlayer.position = { top: musicPlayer.style.top, left: musicPlayer.style.left, bottom: musicPlayer.style.bottom, right: musicPlayer.style.right };
            }
            GM_setValue('trixExecutorState', state);
        }, 500);

        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">
                    <div><span class="trix-title">TriX Executor</span><span class="trix-version-tag">(v${GM_info.script.version})</span></div>
                    <div id="trix-header-controls">
                        <span id="trix-conn-status" class="disconnected" title="No connection"></span>
                        <span id="trix-ping-display">Ping: ---</span>
                        <span id="trix-fps-display">FPS: --</span>
                        <span id="trix-close-btn" title="Close">✖</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);"></div>`;
            
            const musicToggle = document.createElement('div');
            musicToggle.id = 'trix-music-toggle-btn';
            musicToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>`;
            
            const musicPlayer = document.createElement('div');
            musicPlayer.id = 'trix-music-player';
            musicPlayer.innerHTML = `
                <div class="music-drag-handle"></div>
                <div class="music-info"><span class="music-title">Select a Song</span><span class="music-artist">...</span></div>
                <div id="music-visualizer-container">
                    <div class="music-vis-controls">
                        <button class="music-vis-btn" id="music-vis-prev">&lt;</button>
                        <button class="music-vis-btn" id="music-vis-next">&gt;</button>
                    </div>
                    <div id="music-vis-mode-name"></div>
                </div>
                <div class="music-progress-container"><div class="music-progress-bar"></div></div>
                <div class="music-time"><span id="music-current-time">0:00</span><span id="music-duration">0:00</span></div>
                <div class="music-controls">
                    <button class="music-control-btn" id="music-prev-btn" title="Previous"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg></button>
                    <button class="music-control-btn" id="music-rewind-btn" title="Back 10s"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M11.999 5v2.999l-4-3-4 3v10l4-3 4 3V19l-8-6 8-6z m8 0v2.999l-4-3-4 3v10l4-3 4 3V19l-8-6 8-6z"/></svg></button>
                    <button class="music-control-btn music-play-btn" id="music-play-pause-btn"><svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></button>
                    <button class="music-control-btn" id="music-forward-btn" title="Forward 10s"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="m3.999 19 8-6-8-6v12zm8 0 8-6-8-6v12z"/></svg></button>
                    <button class="music-control-btn" id="music-next-btn" title="Next"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg></button>
                </div>
                <div class="music-volume-container">
                    <button class="music-control-btn" id="music-playlist-btn" title="Playlist"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg></button>
                    <input type="range" min="0" max="1" step="0.01" class="music-volume-slider" id="music-volume-slider">
                    <div id="trix-volume-indicator">100%</div>
                </div>`;

            const musicMenu = document.createElement('div');
            musicMenu.id = 'trix-music-menu';
            musicMenu.innerHTML = `
                <div class="music-menu-content">
                    <div class="music-menu-header"><input type="text" id="music-search-input" class="trix-input" placeholder="Search songs..."></div>
                    <div class="music-song-list"></div>
                </div>`;

            shadowRoot.append(toggleBtn, container, musicToggle, musicPlayer, musicMenu);
            return { toggleBtn, container, musicPlayer };
        }
        
        function applySettings() {
            const container = $('#trix-container');
            const musicPlayer = $('#trix-music-player');
            if (container) { Object.assign(container.style, state.settings.position, state.settings.size); }
            if (musicPlayer && state.musicPlayer.position) { Object.assign(musicPlayer.style, state.musicPlayer.position); }
        }

        // ... ALL other non-music player functions are here ...

        loadState();
        ui = createUI();
        applySettings();
        renderActiveView();
        initEventListeners();
        
        initMusicPlayer();
        initDraggable(ui.musicPlayer, [$('.music-drag-handle', ui.musicPlayer)]);

        setInterval(measurePing, 2000);

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

    function initMusicPlayer() {
        const player = {
            audio: document.createElement('audio'),
            songList: [
                { title: "Animal I Have Become", artist: "Skillet", url: "https://dl.sndup.net/br3n7/Skilet_-_Animal_I_Have_Become_(mp3.pm)%20(1).mp3" }
            ],
            isPlaying: false,
            currentSongIndex: 0,
            volume: 0.5,
            audioMotion: null,
            visMode: 0
        };
        player.audio.crossOrigin = "anonymous";

        const $ = (sel) => document.querySelector('#trix-host').shadowRoot.querySelector(sel);
        const playerEl = $('#trix-music-player');
        const titleEl = $('.music-title');
        const artistEl = $('.music-artist');
        const playPauseBtn = $('#music-play-pause-btn');
        const progressBar = $('.music-progress-bar');
        const progressContainer = $('.music-progress-container');
        const currentTimeEl = $('#music-current-time');
        const durationEl = $('#music-duration');
        const volumeSlider = $('#music-volume-slider');
        const volumeIndicator = $('#trix-volume-indicator');
        const menuEl = $('#trix-music-menu');
        const songListEl = $('.music-song-list');
        const visModeNameEl = $('#music-vis-mode-name');
        let volumeTimeout;
        let visModeTimeout;

        const visPresets = [
            { name: "Reflex Bars", options: { mode: 2, reflexRatio: 0.5, radial: false, mirror: 0 } },
            { name: "Vortex", options: { mode: 5, radial: true, spinSpeed: 1 } },
            { name: "Waveform", options: { mode: 10, lineWidth: 2, fillAlpha: 0.2 } },
            { name: "Radial Spectrum", options: { mode: 1, radial: true, channelLayout: 'dual-vertical' } },
        ];

        function setupAudioMotion() {
            if (player.audioMotion) return;
            try {
                const visualizerContainer = $('#music-visualizer-container');
                player.audioMotion = new AudioMotionAnalyzer(visualizerContainer, {
                    source: player.audio,
                    height: 60,
                    width: 270,
                    bgAlpha: 0,
                    showScaleY: false
                });
                player.audioMotion.registerGradient('trix-gradient', {
                    bgColor: '#1e1e22',
                    colorStops: ['#3b82f6', '#2563eb']
                });
                player.audioMotion.gradient = 'trix-gradient';
                setVisMode(state.musicPlayer.visMode); // Apply saved mode
                console.log('[TriX] AudioMotion visualizer initialized successfully.');
            } catch (error) {
                console.warn('[TriX] Audio visualizer failed to initialize (CORS issue?). Music will play without visualization.', error);
                $('#music-visualizer-container').style.display = 'none';
            }
        }

        function setVisMode(index) {
            player.visMode = index;
            state.musicPlayer.visMode = index;
            if (player.audioMotion) {
                const preset = visPresets[player.visMode];
                player.audioMotion.setOptions(preset.options);
                visModeNameEl.textContent = preset.name;
                visModeNameEl.classList.add('visible');
                clearTimeout(visModeTimeout);
                visModeTimeout = setTimeout(() => visModeNameEl.classList.remove('visible'), 1500);
            }
        }
        
        function cycleVisMode(direction) {
            let newIndex = player.visMode + direction;
            if (newIndex >= visPresets.length) newIndex = 0;
            if (newIndex < 0) newIndex = visPresets.length - 1;
            setVisMode(newIndex);
            saveState();
        }

        const loadSong = (index) => {
            player.currentSongIndex = index;
            const song = player.songList[index];
            player.audio.src = song.url;
            titleEl.textContent = song.title;
            artistEl.textContent = song.artist;
            renderSongList();
        };
        
        const playSong = () => {
            player.audio.play().catch(e => { player.isPlaying = false; });
            player.isPlaying = true;
            playPauseBtn.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`;
            if (!player.audioMotion) setupAudioMotion();
        };
        const pauseSong = () => { player.isPlaying = false; player.audio.pause(); playPauseBtn.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`; };
        const nextSong = () => { let newIndex = (player.currentSongIndex + 1) % player.songList.length; loadSong(newIndex); playSong(); };
        const prevSong = () => { let newIndex = (player.currentSongIndex - 1 + player.songList.length) % player.songList.length; loadSong(newIndex); playSong(); };
        const setVolume = (value) => {
            player.volume = value;
            player.audio.volume = value;
            volumeSlider.value = value;
            volumeIndicator.textContent = `${Math.round(value * 100)}%`;
            volumeIndicator.classList.add('visible');
            clearTimeout(volumeTimeout);
            volumeTimeout = setTimeout(() => volumeIndicator.classList.remove('visible'), 1000);
        };
        const renderSongList = (filter = '') => {
            songListEl.innerHTML = '';
            const filteredList = player.songList.filter(song => song.title.toLowerCase().includes(filter) || song.artist.toLowerCase().includes(filter));
            filteredList.forEach((song) => {
                const originalIndex = player.songList.indexOf(song);
                const item = document.createElement('div');
                item.className = 'music-song-item';
                if (originalIndex === player.currentSongIndex) item.classList.add('playing');
                item.innerHTML = `<span class="music-song-title">${song.title}</span><span class="music-song-artist">${song.artist}</span>`;
                item.addEventListener('click', () => { loadSong(originalIndex); playSong(); menuEl.classList.remove('visible'); });
                songListEl.appendChild(item);
            });
        };

        $('#trix-music-toggle-btn').addEventListener('click', () => playerEl.classList.toggle('visible'));
        playPauseBtn.addEventListener('click', () => player.isPlaying ? pauseSong() : playSong());
        $('#music-next-btn').addEventListener('click', nextSong);
        $('#music-prev-btn').addEventListener('click', prevSong);
        $('#music-forward-btn').addEventListener('click', () => player.audio.currentTime += 10);
        $('#music-rewind-btn').addEventListener('click', () => player.audio.currentTime -= 10);
        volumeSlider.addEventListener('input', (e) => setVolume(e.target.value));
        player.audio.addEventListener('timeupdate', () => {
            const { currentTime, duration } = player.audio;
            if (duration) {
                progressBar.style.width = `${(currentTime / duration) * 100}%`;
                const formatTime = (s) => `${Math.floor(s/60)}:${String(Math.floor(s%60)).padStart(2,'0')}`;
                currentTimeEl.textContent = formatTime(currentTime);
                durationEl.textContent = formatTime(duration);
            }
        });
        player.audio.addEventListener('ended', nextSong);
        progressContainer.addEventListener('click', (e) => { player.audio.currentTime = (e.offsetX / progressContainer.clientWidth) * player.audio.duration; });
        $('#music-playlist-btn').addEventListener('click', () => menuEl.classList.add('visible'));
        $('#music-search-input').addEventListener('input', (e) => renderSongList(e.target.value.toLowerCase()));
        menuEl.addEventListener('click', (e) => { if(e.target.id === 'trix-music-menu') menuEl.classList.remove('visible'); });
        $('#music-vis-next').addEventListener('click', () => cycleVisMode(1));
        $('#music-vis-prev').addEventListener('click', () => cycleVisMode(-1));

        setVolume(state.musicPlayer.volume);
        loadSong(state.musicPlayer.currentSongIndex);
        renderSongList();
        
        return player;
    }

    function waitForElement(selector, callback) {
        const maxRetries = 40; const retryInterval = 250; let retryCount = 0;
        const checkInterval = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                console.log(`[TriX] Target element '${selector}' found. Initializing script.`);
                clearInterval(checkInterval);
                callback();
            } else {
                retryCount++;
                if (retryCount >= maxRetries) {
                    clearInterval(checkInterval);
                    console.error(`[TriX] Failed to find target element '${selector}' after ${maxRetries} retries. Aborting initialization.`);
                }
            }
        }, retryInterval);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => waitForElement('#canvasA', initialize));
    } else {
        waitForElement('#canvasA', initialize);
    }

})();