Time speed control + autoclicker panel for HTML5 games (idle/incremental) — per-site profiles, multi-point clicking, hotkeys
// ==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();
})();