HTML5 Universal Speed Hack PLUS (Userscript Edition)

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Advertisement:

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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();

})();