HTML5 Universal Speed Hack PLUS (Userscript Edition)

Time speed control + autoclicker panel for HTML5 games (idle/incremental) — per-site profiles, multi-point clicking, hotkeys

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         HTML5 Universal Speed Hack PLUS (Userscript Edition)
// @namespace    https://github.com/RuiPVMachado
// @version      1.6
// @description  Time speed control + autoclicker panel for HTML5 games (idle/incremental) — per-site profiles, multi-point clicking, hotkeys
// @author       RuiPVMachado (based on TheByteyear & HUSH+)
// @license      MIT
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // Referências nativas capturadas ANTES de qualquer patch — o autoclicker usa
    // sempre estas, para o intervalo dele não ser afetado pelo speed hack.
    const nativeSetInterval = window.setInterval.bind(window);
    const nativeClearInterval = window.clearInterval.bind(window);

    const CONFIG_KEY = 'hush-config';                          // canal "ao vivo" partilhado por todas as frames
    const SITE_CONFIG_KEY = 'hush-site-' + location.hostname;  // perfil guardado por site (só frame de topo)
    const AC_KEY = 'hush-autoclicker';
    const POS_KEY = 'hush-panel-pos';
    const COLLAPSED_KEY = 'hush-panel-collapsed';
    const OPACITY_KEY = 'hush-panel-opacity';
    const ENABLED_KEY = 'hush-enabled-' + location.hostname;
    const SPEED_MIN = 0.05;
    const SPEED_MAX = 50;
    const PRESETS = [0.5, 1, 2, 5, 10, 25]; // Shift+1..Shift+6

    const defaultConfig = {
        speed: 1.0,
        cbSetIntervalChecked: true,
        cbSetTimeoutChecked: true,
        cbPerformanceNowChecked: true,
        cbDateNowChecked: true,
        cbRequestAnimationFrameChecked: false,
        cbVideoSpeedChecked: false,
    };

    const defaultAcConfig = {
        intervalMin: 0,
        intervalSec: 0,
        intervalMs: 100,
        mode: 'cursor',        // 'cursor' | 'points'
        clickType: 'full',     // 'full' | 'mousedown' | 'double'
        trigger: 'toggle',     // 'toggle' | 'hold'
        points: [],            // lista de {x, y} — clica em ciclo
        hotkey: 'F6',
    };

    function parseOr(raw, defaults) {
        try {
            if (raw) return Object.assign({}, defaults, JSON.parse(raw));
        } catch (e) {}
        return Object.assign({}, defaults);
    }

    function loadSiteConfig() {
        // perfil do site tem prioridade; senão usa o último global; senão defaults
        const site = GM_getValue(SITE_CONFIG_KEY, null);
        if (site) return parseOr(site, defaultConfig);
        return parseOr(GM_getValue(CONFIG_KEY, null), defaultConfig);
    }
    function saveConfig(cfg) {
        const json = JSON.stringify(cfg);
        GM_setValue(CONFIG_KEY, json);            // sincroniza todas as frames
        GM_setValue(SITE_CONFIG_KEY, json);       // guarda perfil deste site
    }

    function loadAutoclickerConfig() {
        return parseOr(GM_getValue(AC_KEY, null), defaultAcConfig);
    }
    function saveAutoclickerConfig(cfg) { GM_setValue(AC_KEY, JSON.stringify(cfg)); }

    function clamp(v, min, max) { return Math.min(Math.max(v, min), max); }

    // --- Ativação por site ---
    let siteEnabled = GM_getValue(ENABLED_KEY, false);

    GM_registerMenuCommand(siteEnabled ? '🔴 Desativar HUSH+ neste site' : '🟢 Ativar HUSH+ neste site', () => {
        GM_setValue(ENABLED_KEY, !siteEnabled);
        location.reload();
    });

    if (!siteEnabled) return;

    GM_registerMenuCommand('↺ Repor velocidade (1x)', () => {
        const cfg = loadSiteConfig();
        cfg.speed = 1.0;
        saveConfig(cfg);
    });

    GM_registerMenuCommand('📌 Repor posição do painel', () => {
        GM_setValue(POS_KEY, null);
        location.reload();
    });

    // --- Parte 1: pageScript, injetado no contexto real da página ---
    function pageScript() {
        let speedConfig = {
            speed: 1.0,
            cbSetIntervalChecked: true,
            cbSetTimeoutChecked: true,
            cbPerformanceNowChecked: true,
            cbDateNowChecked: true,
            cbRequestAnimationFrameChecked: false,
            cbVideoSpeedChecked: false,
        };

        const native = {
            setInterval: window.setInterval.bind(window),
            setTimeout: window.setTimeout.bind(window),
            clearInterval: window.clearInterval.bind(window),
            clearTimeout: window.clearTimeout.bind(window),
            performanceNow: performance.now.bind(performance),
            dateNow: Date.now.bind(Date),
            requestAnimationFrame: window.requestAnimationFrame.bind(window),
            cancelAnimationFrame: window.cancelAnimationFrame.bind(window),
        };

        let perfOrigin = native.performanceNow();
        let dateOrigin = native.dateNow();

        function fakePerformanceNow() {
            return perfOrigin + (native.performanceNow() - perfOrigin) * speedConfig.speed;
        }
        function fakeDateNow() {
            return dateOrigin + (native.dateNow() - dateOrigin) * speedConfig.speed;
        }

        let intervalRegistry = new Map();
        let timeoutRegistry = new Map();
        let idCounter = 1;

        function patchTimers() {
            window.setInterval = function(fn, delay, ...args) {
                delay = delay || 0;
                const fakeId = idCounter++;
                const adjusted = speedConfig.cbSetIntervalChecked ? delay / speedConfig.speed : delay;
                const realId = native.setInterval(fn, adjusted, ...args);
                intervalRegistry.set(fakeId, { realId, fn, delay, args });
                return fakeId;
            };
            window.clearInterval = function(fakeId) {
                const entry = intervalRegistry.get(fakeId);
                if (entry) { native.clearInterval(entry.realId); intervalRegistry.delete(fakeId); }
                else { native.clearInterval(fakeId); }
            };
            window.setTimeout = function(fn, delay, ...args) {
                delay = delay || 0;
                const fakeId = idCounter++;
                const adjusted = speedConfig.cbSetTimeoutChecked ? delay / speedConfig.speed : delay;
                const realId = native.setTimeout(() => { timeoutRegistry.delete(fakeId); fn(...args); }, adjusted);
                timeoutRegistry.set(fakeId, { realId, fn, delay, args });
                return fakeId;
            };
            window.clearTimeout = function(fakeId) {
                const entry = timeoutRegistry.get(fakeId);
                if (entry) { native.clearTimeout(entry.realId); timeoutRegistry.delete(fakeId); }
                else { native.clearTimeout(fakeId); }
            };
        }

        function patchTimeFunctions() {
            performance.now = () => speedConfig.cbPerformanceNowChecked ? fakePerformanceNow() : native.performanceNow();
            Date.now = () => speedConfig.cbDateNowChecked ? fakeDateNow() : native.dateNow();
        }

        function patchRAF() {
            if (speedConfig.cbRequestAnimationFrameChecked) {
                window.requestAnimationFrame = (cb) => native.setTimeout(() => cb(fakePerformanceNow()), 16.67 / speedConfig.speed);
                window.cancelAnimationFrame = (id) => native.clearTimeout(id);
            } else {
                window.requestAnimationFrame = native.requestAnimationFrame;
                window.cancelAnimationFrame = native.cancelAnimationFrame;
            }
        }

        function applyVideoSpeed() {
            if (!speedConfig.cbVideoSpeedChecked) return;
            const rate = Math.min(speedConfig.speed, 16);
            document.querySelectorAll('video, audio').forEach(el => {
                try { el.playbackRate = rate; } catch (e) {}
            });
        }
        native.setInterval(applyVideoSpeed, 1000);

        function reloadTimers() {
            for (const [fakeId, entry] of intervalRegistry.entries()) {
                native.clearInterval(entry.realId);
                const adjusted = speedConfig.cbSetIntervalChecked ? entry.delay / speedConfig.speed : entry.delay;
                entry.realId = native.setInterval(entry.fn, adjusted, ...entry.args);
            }
            patchTimeFunctions();
            patchRAF();
            applyVideoSpeed();
        }

        patchTimers();
        patchTimeFunctions();
        patchRAF();

        window.addEventListener('message', (e) => {
            if (e.source !== window || !e.data) return;
            if (e.data.command === 'setSpeedConfig') {
                speedConfig = e.data.config;
                reloadTimers();
            }
        });

        window.postMessage({ command: 'getSpeedConfig' }, '*');
    }

    function injectScript() {
        const script = document.createElement('script');
        script.textContent = `!${pageScript.toString()}()\n//# sourceURL=pageScript.js`;
        document.documentElement.appendChild(script);
        script.remove();
    }

    injectScript();

    // --- Parte 2: ponte de comunicação — corre em TODAS as frames ---
    let speedConfig = window.top === window ? loadSiteConfig() : parseOr(GM_getValue(CONFIG_KEY, null), defaultConfig);

    window.addEventListener('message', (e) => {
        if (e.source !== window || !e.data) return;
        if (e.data.command === 'getSpeedConfig') {
            window.postMessage({ command: 'setSpeedConfig', config: speedConfig }, '*');
        }
    });

    GM_addValueChangeListener(CONFIG_KEY, (name, oldValue, newValue) => {
        try {
            speedConfig = Object.assign({}, defaultConfig, JSON.parse(newValue));
        } catch (e) { return; }
        window.postMessage({ command: 'setSpeedConfig', config: speedConfig }, '*');
        if (window.top === window) syncPanelUI();
    });

    function updateConfig(newConfig) {
        Object.assign(speedConfig, newConfig);
        saveConfig(speedConfig);
        window.postMessage({ command: 'setSpeedConfig', config: speedConfig }, '*');
    }

    // A frame de topo empurra o perfil do site para o canal global no arranque,
    // para que os iframes do jogo recebam logo a velocidade guardada deste site
    if (window.top === window) {
        GM_setValue(CONFIG_KEY, JSON.stringify(speedConfig));
    }

    // --- Parte 3: painel de controlo — só na frame de topo ---
    if (window.top !== window) return;

    let autoclickerConfig = loadAutoclickerConfig();
    let panelOpacity = GM_getValue(OPACITY_KEY, 0.92);

    GM_addStyle(`
        #hush-control-panel {
            position: fixed; top: 10px; right: 10px; z-index: 999999;
            background: rgba(30, 30, 30, var(--hush-opacity, 0.92)); color: white; padding: 15px;
            border-radius: 8px; font-family: Arial, sans-serif; font-size: 13px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3); border: 1px solid #555;
            width: 290px; backdrop-filter: blur(5px); cursor: move; user-select: none;
            max-height: 90vh; overflow-y: auto;
        }
        #hush-control-panel .header { font-weight: bold; margin-bottom: 10px; text-align: center; color: #ffa500; cursor: move; }
        #hush-control-panel .slider-container { margin-bottom: 12px; }
        #hush-control-panel .slider-label { display: flex; justify-content: space-between; margin-bottom: 5px; }
        #hush-control-panel .speed-row { display: flex; align-items: center; gap: 8px; }
        #hush-control-panel input[type=range] { flex: 1; cursor: pointer; }
        #hush-control-panel input[type=number] {
            background: #222; color: #ffa500; border: 1px solid #555;
            border-radius: 4px; padding: 3px 4px; font-weight: bold; text-align: center;
        }
        #hush-control-panel .speed-row input[type=number] { width: 56px; }
        #hush-control-panel .minimap-marks { display: flex; justify-content: space-between; margin-top: 4px; color: #aaa; font-size: 10px; padding: 0 2px; }
        #hush-control-panel .preset-group { display: flex; gap: 4px; margin-top: 8px; flex-wrap: wrap; }
        #hush-control-panel .preset-group button {
            flex: 1; background: #333; color: #ddd; border: 1px solid #555; border-radius: 4px;
            padding: 3px 0; cursor: pointer; font-size: 11px;
        }
        #hush-control-panel .preset-group button:hover { background: #ffa500; color: #111; }
        #hush-control-panel .preset-hint { font-size: 9px; color: #777; text-align: center; margin-top: 3px; }
        #hush-control-panel .reset-btn {
            width: 100%; margin-top: 8px; background: #444; color: #ddd; border: 1px solid #555;
            border-radius: 4px; padding: 5px 0; cursor: pointer; font-size: 12px;
        }
        #hush-control-panel .reset-btn:hover { background: #666; }
        #hush-control-panel .checkbox-group { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
        #hush-control-panel .checkbox-group label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
        #hush-control-panel .toggle-btn { position: absolute; top: 8px; right: 10px; cursor: pointer; color: #aaa; font-size: 12px; }
        #hush-control-panel .section-divider { margin: 14px 0 8px; padding-top: 10px; border-top: 1px solid #444; font-weight: bold; color: #ffa500; font-size: 12px; }
        #hush-control-panel .ac-status-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 12px; }
        #hush-control-panel .ac-status { color: #999; }
        #hush-control-panel .ac-status.running { color: #4caf50; }
        #hush-control-panel .ac-hotkey-display { background: #222; border: 1px solid #555; border-radius: 4px; padding: 1px 6px; color: #ffa500; font-size: 11px; }
        #hush-control-panel .ac-counter { color: #aaa; font-size: 11px; }
        #hush-control-panel .ac-interval-row { display: flex; gap: 6px; margin-bottom: 8px; }
        #hush-control-panel .ac-field { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; }
        #hush-control-panel .ac-field label { font-size: 10px; color: #aaa; }
        #hush-control-panel .ac-field input { width: 100%; box-sizing: border-box; }
        #hush-control-panel .ac-select-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 12px; }
        #hush-control-panel .ac-select-row select {
            flex: 1; background: #222; color: #ddd; border: 1px solid #555; border-radius: 4px; padding: 3px;
        }
        #hush-control-panel .ac-mode-row { display: flex; flex-direction: column; gap: 4px; font-size: 12px; margin-bottom: 8px; }
        #hush-control-panel .ac-mode-row label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
        #hush-control-panel .ac-points-row { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 11px; flex-wrap: wrap; }
        #hush-control-panel .ac-points-row button { background: #333; color: #ddd; border: 1px solid #555; border-radius: 4px; padding: 3px 6px; cursor: pointer; font-size: 11px; }
        #hush-control-panel #hush-ac-points-display { color: #aaa; }
        #hush-control-panel .ac-buttons-row { display: flex; gap: 6px; }
        #hush-control-panel .ac-toggle-btn { flex: 1; background: #2e7d32; color: white; border: none; border-radius: 4px; padding: 6px 0; cursor: pointer; font-weight: bold; }
        #hush-control-panel .ac-toggle-btn.running { background: #c62828; }
        #hush-control-panel #hush-ac-hotkey-btn { flex: 1; background: #444; color: #ddd; border: 1px solid #555; border-radius: 4px; padding: 6px 0; cursor: pointer; font-size: 11px; }
        #hush-control-panel .ac-hint { margin-top: 8px; font-size: 10px; color: #888; line-height: 1.4; }
        #hush-control-panel .util-row { display: flex; gap: 6px; margin-top: 8px; }
        #hush-control-panel .util-row button { flex: 1; background: #333; color: #ddd; border: 1px solid #555; border-radius: 4px; padding: 4px 0; cursor: pointer; font-size: 11px; }
        #hush-control-panel .opacity-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; font-size: 11px; color: #aaa; }
        .hush-point-marker {
            position: fixed; width: 18px; height: 18px; margin: -9px 0 0 -9px;
            border: 2px solid #ffa500; border-radius: 50%; background: rgba(255,165,0,0.25);
            z-index: 999998; pointer-events: none; color: #ffa500; font-size: 10px;
            display: flex; align-items: center; justify-content: center; font-family: Arial;
        }
    `);

    function pointsLabel() {
        const n = autoclickerConfig.points.length;
        return n === 0 ? 'nenhum ponto' : (n === 1 ? '1 ponto' : n + ' pontos');
    }

    function buildPanel() {
        const panel = document.createElement('div');
        panel.id = 'hush-control-panel';
        panel.style.setProperty('--hush-opacity', panelOpacity);
        panel.innerHTML = `
            <div class="header" id="hush-drag-handle">⚡ HUSH+ Control</div>
            <span class="toggle-btn" id="hush-toggle">▁</span>
            <div id="hush-body">
                <div class="slider-container">
                    <div class="slider-label"><span>Speed</span></div>
                    <div class="speed-row">
                        <input type="range" id="hush-speed-slider" min="${SPEED_MIN}" max="${SPEED_MAX}" step="0.05" value="${speedConfig.speed}">
                        <input type="number" id="hush-speed-input" min="${SPEED_MIN}" max="${SPEED_MAX}" step="0.05" value="${speedConfig.speed.toFixed(2)}">
                    </div>
                    <div class="minimap-marks"><span>0x</span><span>10x</span><span>20x</span><span>30x</span><span>40x</span><span>50x</span></div>
                    <div class="preset-group">
                        ${PRESETS.map(p => `<button data-speed="${p}">${p}x</button>`).join('')}
                    </div>
                    <div class="preset-hint">Atalhos: Shift+1 a Shift+6</div>
                    <button class="reset-btn" id="hush-reset">↺ Repor 1x</button>
                </div>
                <div class="checkbox-group">
                    <label><input type="checkbox" id="cb-si" ${speedConfig.cbSetIntervalChecked ? 'checked' : ''}> <span>Override setInterval</span></label>
                    <label><input type="checkbox" id="cb-st" ${speedConfig.cbSetTimeoutChecked ? 'checked' : ''}> <span>Override setTimeout</span></label>
                    <label><input type="checkbox" id="cb-pn" ${speedConfig.cbPerformanceNowChecked ? 'checked' : ''}> <span>Override performance.now</span></label>
                    <label><input type="checkbox" id="cb-dn" ${speedConfig.cbDateNowChecked ? 'checked' : ''}> <span>Override Date.now</span></label>
                    <label><input type="checkbox" id="cb-raf" ${speedConfig.cbRequestAnimationFrameChecked ? 'checked' : ''}> <span>Override requestAnimationFrame</span></label>
                    <label><input type="checkbox" id="cb-vid" ${speedConfig.cbVideoSpeedChecked ? 'checked' : ''}> <span>Aplicar a vídeos (playbackRate)</span></label>
                </div>

                <div class="section-divider">🖱️ Autoclicker</div>
                <div class="ac-status-row">
                    <span class="ac-status" id="hush-ac-status">● Parado</span>
                    <span class="ac-counter" id="hush-ac-counter">0 cliques</span>
                    <span class="ac-hotkey-display" id="hush-ac-hotkey-display">${autoclickerConfig.hotkey}</span>
                </div>
                <div class="ac-interval-row">
                    <div class="ac-field"><label>Min</label><input type="number" id="hush-ac-min" min="0" value="${autoclickerConfig.intervalMin}"></div>
                    <div class="ac-field"><label>Seg</label><input type="number" id="hush-ac-sec" min="0" max="59" value="${autoclickerConfig.intervalSec}"></div>
                    <div class="ac-field"><label>Ms</label><input type="number" id="hush-ac-ms" min="0" max="999" value="${autoclickerConfig.intervalMs}"></div>
                </div>
                <div class="ac-select-row">
                    <span>Tipo</span>
                    <select id="hush-ac-clicktype">
                        <option value="full" ${autoclickerConfig.clickType === 'full' ? 'selected' : ''}>Clique completo</option>
                        <option value="mousedown" ${autoclickerConfig.clickType === 'mousedown' ? 'selected' : ''}>Só mousedown (compatibilidade)</option>
                        <option value="double" ${autoclickerConfig.clickType === 'double' ? 'selected' : ''}>Duplo clique</option>
                    </select>
                </div>
                <div class="ac-select-row">
                    <span>Tecla</span>
                    <select id="hush-ac-trigger">
                        <option value="toggle" ${autoclickerConfig.trigger === 'toggle' ? 'selected' : ''}>Ligar/desligar (toggle)</option>
                        <option value="hold" ${autoclickerConfig.trigger === 'hold' ? 'selected' : ''}>Só enquanto pressionada (hold)</option>
                    </select>
                </div>
                <div class="ac-mode-row">
                    <label><input type="radio" name="ac-mode" id="hush-ac-mode-cursor" ${autoclickerConfig.mode === 'cursor' ? 'checked' : ''}> Seguir cursor</label>
                    <label><input type="radio" name="ac-mode" id="hush-ac-mode-points" ${autoclickerConfig.mode === 'points' ? 'checked' : ''}> Pontos fixos (clica em ciclo)</label>
                </div>
                <div class="ac-points-row" id="hush-ac-points-row" style="display:${autoclickerConfig.mode === 'points' ? 'flex' : 'none'}">
                    <button id="hush-ac-add-point">➕ Adicionar ponto</button>
                    <button id="hush-ac-clear-points">🗑 Limpar</button>
                    <button id="hush-ac-show-points">👁 Mostrar</button>
                    <span id="hush-ac-points-display">${pointsLabel()}</span>
                </div>
                <div class="ac-buttons-row">
                    <button id="hush-ac-toggle" class="ac-toggle-btn">Iniciar</button>
                    <button id="hush-ac-hotkey-btn">Definir tecla</button>
                </div>
                <div class="ac-hint">Nota: jogos Canvas/WebGL que verificam isTrusted ignoram cliques de scripts — nesses casos usa um autoclicker de sistema (OP Auto Clicker).</div>

                <div class="section-divider">⚙️ Painel</div>
                <div class="opacity-row">
                    <span>Opacidade</span>
                    <input type="range" id="hush-opacity" min="0.3" max="1" step="0.05" value="${panelOpacity}">
                </div>
                <div class="util-row">
                    <button id="hush-export">📤 Exportar config</button>
                    <button id="hush-import">📥 Importar</button>
                </div>
            </div>
        `;
        document.body.appendChild(panel);

        // Aplica posição guardada, mas SEMPRE limitada à área visível —
        // evita o painel "desaparecer" por ter ficado guardado fora do ecrã
        applySavedPosition(panel);
        window.addEventListener('resize', () => clampToViewport(panel));
        return panel;
    }

    function applySavedPosition(panel) {
        const saved = GM_getValue(POS_KEY, null);
        if (saved && typeof saved.x === 'number' && typeof saved.y === 'number') {
            panel.style.left = saved.x + 'px';
            panel.style.top = saved.y + 'px';
            panel.style.right = 'auto';
        }
        clampToViewport(panel);
    }

    function clampToViewport(panel) {
        const rect = panel.getBoundingClientRect();
        const maxX = Math.max(window.innerWidth - rect.width, 0);
        const maxY = Math.max(window.innerHeight - 40, 0);
        let x = rect.left, y = rect.top;
        const cx = clamp(x, 0, maxX);
        const cy = clamp(y, 0, maxY);
        if (cx !== x || cy !== y) {
            panel.style.left = cx + 'px';
            panel.style.top = cy + 'px';
            panel.style.right = 'auto';
        }
    }

    let els = {};
    let panelEl = null;

    function collectCheckboxes() {
        return {
            cbSetIntervalChecked: els.cbSi.checked,
            cbSetTimeoutChecked: els.cbSt.checked,
            cbPerformanceNowChecked: els.cbPn.checked,
            cbDateNowChecked: els.cbDn.checked,
            cbRequestAnimationFrameChecked: els.cbRaf.checked,
            cbVideoSpeedChecked: els.cbVid.checked,
        };
    }

    function applySpeed(rawValue) {
        const value = clamp(parseFloat(rawValue) || 1, SPEED_MIN, SPEED_MAX);
        els.slider.value = value;
        els.input.value = value.toFixed(2);
        updateConfig(Object.assign({ speed: value }, collectCheckboxes()));
    }

    function syncPanelUI() {
        if (!els.slider) return;
        els.slider.value = speedConfig.speed;
        els.input.value = speedConfig.speed.toFixed(2);
        els.cbSi.checked = speedConfig.cbSetIntervalChecked;
        els.cbSt.checked = speedConfig.cbSetTimeoutChecked;
        els.cbPn.checked = speedConfig.cbPerformanceNowChecked;
        els.cbDn.checked = speedConfig.cbDateNowChecked;
        els.cbRaf.checked = speedConfig.cbRequestAnimationFrameChecked;
        els.cbVid.checked = speedConfig.cbVideoSpeedChecked;
    }

    // ===== Autoclicker =====
    let autoclickerTimer = null;
    let clickCount = 0;
    let pointIndex = 0;
    let currentMousePos = { x: 0, y: 0 };
    let pickingPosition = false;
    let capturingHotkey = false;
    let markerEls = [];

    document.addEventListener('mousemove', (e) => {
        currentMousePos = { x: e.clientX, y: e.clientY };
    }, true);

    // Desce para dentro de iframes same-origin para encontrar o alvo real
    function resolveTarget(x, y) {
        let doc = document;
        let el = doc.elementFromPoint(x, y);
        let depth = 0;
        while (el && el.tagName === 'IFRAME' && depth < 5) {
            let innerDoc;
            try { innerDoc = el.contentDocument; } catch (err) { innerDoc = null; }
            if (!innerDoc) break;
            const rect = el.getBoundingClientRect();
            x = x - rect.left;
            y = y - rect.top;
            doc = innerDoc;
            el = doc.elementFromPoint(x, y);
            depth++;
        }
        return el ? { el, x, y } : null;
    }

    function dispatchClickSequence(el, x, y, type) {
        const win = el.ownerDocument.defaultView || window;
        const common = { bubbles: true, cancelable: true, view: win, clientX: x, clientY: y, button: 0, buttons: 1 };
        const pointerExtra = { pointerId: 1, pointerType: 'mouse', isPrimary: true };

        try { el.dispatchEvent(new PointerEvent('pointermove', Object.assign({}, pointerExtra, common))); } catch (e) {}
        el.dispatchEvent(new MouseEvent('mousemove', common));
        try { el.dispatchEvent(new PointerEvent('pointerdown', Object.assign({}, pointerExtra, common))); } catch (e) {}
        el.dispatchEvent(new MouseEvent('mousedown', common));

        if (type === 'mousedown') return;

        try { el.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, pointerExtra, common))); } catch (e) {}
        el.dispatchEvent(new MouseEvent('mouseup', common));
        el.dispatchEvent(new MouseEvent('click', common));

        if (type === 'double') {
            el.dispatchEvent(new MouseEvent('dblclick', Object.assign({}, common, { detail: 2 })));
        }
    }

    function simulateClick(x, y) {
        const target = resolveTarget(x, y);
        if (!target) return false;
        if (panelEl && panelEl.contains(target.el)) return false;
        dispatchClickSequence(target.el, target.x, target.y, autoclickerConfig.clickType);
        return true;
    }

    function nextClickPos() {
        if (autoclickerConfig.mode === 'points' && autoclickerConfig.points.length > 0) {
            const pos = autoclickerConfig.points[pointIndex % autoclickerConfig.points.length];
            pointIndex++;
            return pos;
        }
        return currentMousePos;
    }

    function computeIntervalMs() {
        const min = Math.max(parseInt(els.acMin.value) || 0, 0);
        const sec = clamp(parseInt(els.acSec.value) || 0, 0, 59);
        const ms = clamp(parseInt(els.acMs.value) || 0, 0, 999);
        return Math.max(min * 60000 + sec * 1000 + ms, 10);
    }

    function updateAcStatusUI(running) {
        els.acStatus.textContent = running ? '● Ativo' : '● Parado';
        els.acStatus.className = 'ac-status' + (running ? ' running' : '');
        els.acToggleBtn.textContent = running ? 'Parar' : 'Iniciar';
        els.acToggleBtn.className = 'ac-toggle-btn' + (running ? ' running' : '');
    }

    function startAutoclicker() {
        if (autoclickerTimer) return;
        pointIndex = 0;
        const interval = computeIntervalMs();
        autoclickerTimer = nativeSetInterval(() => {
            const pos = nextClickPos();
            if (simulateClick(pos.x, pos.y)) {
                clickCount++;
                els.acCounter.textContent = clickCount + ' cliques';
            }
        }, interval);
        updateAcStatusUI(true);
    }

    function stopAutoclicker() {
        if (autoclickerTimer) {
            nativeClearInterval(autoclickerTimer);
            autoclickerTimer = null;
        }
        updateAcStatusUI(false);
    }

    function toggleAutoclicker() {
        if (autoclickerTimer) stopAutoclicker(); else startAutoclicker();
    }

    // --- Marcadores visuais dos pontos ---
    function showMarkers(durationMs) {
        hideMarkers();
        autoclickerConfig.points.forEach((p, i) => {
            const m = document.createElement('div');
            m.className = 'hush-point-marker';
            m.style.left = p.x + 'px';
            m.style.top = p.y + 'px';
            m.textContent = (i + 1);
            document.body.appendChild(m);
            markerEls.push(m);
        });
        if (durationMs) setTimeout(hideMarkers, durationMs);
    }
    function hideMarkers() {
        markerEls.forEach(m => m.remove());
        markerEls = [];
    }

    document.addEventListener('mousedown', (e) => {
        if (!pickingPosition) return;
        if (panelEl && panelEl.contains(e.target)) return;
        e.preventDefault();
        e.stopPropagation();
        pickingPosition = false;
        document.body.style.cursor = '';
        els.acAddPointBtn.textContent = '➕ Adicionar ponto';
        autoclickerConfig.points.push({ x: e.clientX, y: e.clientY });
        saveAutoclickerConfig(autoclickerConfig);
        els.acPointsDisplay.textContent = pointsLabel();
        showMarkers(1500);
    }, true);

    document.addEventListener('keydown', (e) => {
        if (capturingHotkey) {
            e.preventDefault();
            e.stopPropagation();
            if (e.key !== 'Escape') {
                autoclickerConfig.hotkey = e.code;
                saveAutoclickerConfig(autoclickerConfig);
                els.acHotkeyDisplay.textContent = autoclickerConfig.hotkey;
            }
            capturingHotkey = false;
            els.acHotkeyBtn.textContent = 'Definir tecla';
            return;
        }
        const activeTag = document.activeElement && document.activeElement.tagName;
        if (activeTag === 'INPUT' || activeTag === 'TEXTAREA') return;

        // Hotkey do autoclicker
        if (e.code === autoclickerConfig.hotkey) {
            e.preventDefault();
            if (autoclickerConfig.trigger === 'hold') {
                if (!e.repeat) startAutoclicker();
            } else {
                if (!e.repeat) toggleAutoclicker();
            }
            return;
        }

        // Atalhos de preset: Shift+1 a Shift+6
        if (e.shiftKey && /^Digit[1-6]$/.test(e.code)) {
            const idx = parseInt(e.code.slice(5)) - 1;
            if (PRESETS[idx] !== undefined) {
                e.preventDefault();
                applySpeed(PRESETS[idx]);
            }
        }
    }, true);

    document.addEventListener('keyup', (e) => {
        if (autoclickerConfig.trigger === 'hold' && e.code === autoclickerConfig.hotkey) {
            stopAutoclicker();
        }
    }, true);

    function initPanelWhenReady() {
        if (!document.body) {
            // fallback robusto: tenta até o body existir, mesmo que a página
            // reescreva o documento e "coma" o evento DOMContentLoaded
            nativeSetInterval(function tryInit() {
                if (document.body && !document.getElementById('hush-control-panel')) {
                    try { doInitPanel(); } catch (e) { console.error('[HUSH+] erro a criar painel:', e); }
                }
            }, 300);
            return;
        }
        try { doInitPanel(); } catch (e) { console.error('[HUSH+] erro a criar painel:', e); }
    }

    // Watchdog: se a página apagar/reescrever o body e levar o painel junto, recria-o
    nativeSetInterval(() => {
        if (document.body && panelEl && !document.body.contains(panelEl)) {
            try { doInitPanel(); } catch (e) { console.error('[HUSH+] erro a recriar painel:', e); }
        }
    }, 1000);

    function doInitPanel() {
        const existing = document.getElementById('hush-control-panel');
        if (existing) existing.remove();

        panelEl = buildPanel();
        const panel = panelEl;
        els = {
            slider: document.getElementById('hush-speed-slider'),
            input: document.getElementById('hush-speed-input'),
            cbSi: document.getElementById('cb-si'),
            cbSt: document.getElementById('cb-st'),
            cbPn: document.getElementById('cb-pn'),
            cbDn: document.getElementById('cb-dn'),
            cbRaf: document.getElementById('cb-raf'),
            cbVid: document.getElementById('cb-vid'),
            acStatus: document.getElementById('hush-ac-status'),
            acCounter: document.getElementById('hush-ac-counter'),
            acHotkeyDisplay: document.getElementById('hush-ac-hotkey-display'),
            acMin: document.getElementById('hush-ac-min'),
            acSec: document.getElementById('hush-ac-sec'),
            acMs: document.getElementById('hush-ac-ms'),
            acClickType: document.getElementById('hush-ac-clicktype'),
            acTrigger: document.getElementById('hush-ac-trigger'),
            acModeCursor: document.getElementById('hush-ac-mode-cursor'),
            acModePoints: document.getElementById('hush-ac-mode-points'),
            acPointsRow: document.getElementById('hush-ac-points-row'),
            acAddPointBtn: document.getElementById('hush-ac-add-point'),
            acClearPointsBtn: document.getElementById('hush-ac-clear-points'),
            acShowPointsBtn: document.getElementById('hush-ac-show-points'),
            acPointsDisplay: document.getElementById('hush-ac-points-display'),
            acToggleBtn: document.getElementById('hush-ac-toggle'),
            acHotkeyBtn: document.getElementById('hush-ac-hotkey-btn'),
        };

        // --- Speed ---
        els.slider.addEventListener('input', () => applySpeed(els.slider.value));
        els.input.addEventListener('input', () => {
            const v = parseFloat(els.input.value);
            if (!isNaN(v)) {
                const clamped = clamp(v, SPEED_MIN, SPEED_MAX);
                els.slider.value = clamped;
                updateConfig(Object.assign({ speed: clamped }, collectCheckboxes()));
            }
        });
        els.input.addEventListener('blur', () => applySpeed(els.input.value));
        els.input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') { applySpeed(els.input.value); els.input.blur(); }
        });
        panel.querySelectorAll('.preset-group button').forEach(btn => {
            btn.addEventListener('click', () => applySpeed(btn.dataset.speed));
        });
        document.getElementById('hush-reset').addEventListener('click', () => applySpeed(1));
        [els.cbSi, els.cbSt, els.cbPn, els.cbDn, els.cbRaf, els.cbVid].forEach(cb => {
            cb.addEventListener('change', () => updateConfig(collectCheckboxes()));
        });

        // --- Autoclicker ---
        [els.acMin, els.acSec, els.acMs].forEach(inp => {
            inp.addEventListener('change', () => {
                autoclickerConfig.intervalMin = Math.max(parseInt(els.acMin.value) || 0, 0);
                autoclickerConfig.intervalSec = clamp(parseInt(els.acSec.value) || 0, 0, 59);
                autoclickerConfig.intervalMs = clamp(parseInt(els.acMs.value) || 0, 0, 999);
                els.acMin.value = autoclickerConfig.intervalMin;
                els.acSec.value = autoclickerConfig.intervalSec;
                els.acMs.value = autoclickerConfig.intervalMs;
                saveAutoclickerConfig(autoclickerConfig);
                if (autoclickerTimer) { stopAutoclicker(); startAutoclicker(); }
            });
        });

        els.acClickType.addEventListener('change', () => {
            autoclickerConfig.clickType = els.acClickType.value;
            saveAutoclickerConfig(autoclickerConfig);
        });

        els.acTrigger.addEventListener('change', () => {
            autoclickerConfig.trigger = els.acTrigger.value;
            saveAutoclickerConfig(autoclickerConfig);
            stopAutoclicker();
        });

        function updateModeUI() {
            autoclickerConfig.mode = els.acModePoints.checked ? 'points' : 'cursor';
            saveAutoclickerConfig(autoclickerConfig);
            els.acPointsRow.style.display = autoclickerConfig.mode === 'points' ? 'flex' : 'none';
        }
        els.acModeCursor.addEventListener('change', updateModeUI);
        els.acModePoints.addEventListener('change', updateModeUI);

        els.acAddPointBtn.addEventListener('click', () => {
            pickingPosition = true;
            els.acAddPointBtn.textContent = 'Clica no ecrã...';
            document.body.style.cursor = 'crosshair';
        });

        els.acClearPointsBtn.addEventListener('click', () => {
            autoclickerConfig.points = [];
            saveAutoclickerConfig(autoclickerConfig);
            els.acPointsDisplay.textContent = pointsLabel();
            hideMarkers();
        });

        els.acShowPointsBtn.addEventListener('click', () => showMarkers(2500));

        els.acToggleBtn.addEventListener('click', toggleAutoclicker);

        els.acHotkeyBtn.addEventListener('click', () => {
            capturingHotkey = true;
            els.acHotkeyBtn.textContent = 'Pressiona uma tecla...';
        });

        // --- Painel: opacidade, export/import ---
        document.getElementById('hush-opacity').addEventListener('input', (e) => {
            panelOpacity = parseFloat(e.target.value);
            panel.style.setProperty('--hush-opacity', panelOpacity);
            GM_setValue(OPACITY_KEY, panelOpacity);
        });

        document.getElementById('hush-export').addEventListener('click', () => {
            const payload = JSON.stringify({ speed: speedConfig, autoclicker: autoclickerConfig }, null, 2);
            try {
                GM_setClipboard(payload);
                alert('Configuração copiada para o clipboard!');
            } catch (e) {
                prompt('Copia a configuração:', payload);
            }
        });

        document.getElementById('hush-import').addEventListener('click', () => {
            const raw = prompt('Cola aqui a configuração exportada:');
            if (!raw) return;
            try {
                const parsed = JSON.parse(raw);
                if (parsed.speed) updateConfig(Object.assign({}, defaultConfig, parsed.speed));
                if (parsed.autoclicker) {
                    autoclickerConfig = Object.assign({}, defaultAcConfig, parsed.autoclicker);
                    saveAutoclickerConfig(autoclickerConfig);
                }
                alert('Configuração importada! A página vai recarregar.');
                location.reload();
            } catch (e) {
                alert('JSON inválido.');
            }
        });

        // --- Colapsar / expandir ---
        const toggleBtn = document.getElementById('hush-toggle');
        const body = document.getElementById('hush-body');
        let collapsed = GM_getValue(COLLAPSED_KEY, false);
        if (collapsed) { body.style.display = 'none'; toggleBtn.textContent = '▢'; }
        toggleBtn.addEventListener('click', () => {
            collapsed = !collapsed;
            body.style.display = collapsed ? 'none' : 'block';
            toggleBtn.textContent = collapsed ? '▢' : '▁';
            GM_setValue(COLLAPSED_KEY, collapsed);
        });

        // --- Arrastar ---
        let isDragging = false, offsetX, offsetY;
        const dragHandle = document.getElementById('hush-drag-handle');
        dragHandle.addEventListener('mousedown', (e) => {
            isDragging = true;
            offsetX = e.clientX - panel.offsetLeft;
            offsetY = e.clientY - panel.offsetTop;
            panel.style.cursor = 'grabbing';
        });
        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            panel.style.left = (e.clientX - offsetX) + 'px';
            panel.style.top = (e.clientY - offsetY) + 'px';
            panel.style.right = 'auto';
        });
        document.addEventListener('mouseup', () => {
            if (isDragging) GM_setValue(POS_KEY, { x: panel.offsetLeft, y: panel.offsetTop });
            isDragging = false;
            panel.style.cursor = 'move';
        });
    }

    initPanelWhenReady();

})();