// ==UserScript==
// @name IdleOn Clicker
// @namespace nativerobot
// @version 3.6
// @downloadURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-clicker.user.js
// @updateURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-clicker.user.js
// @description Stealthy in-page autoclicker panel for Legends of IdleOn (browser)
// @match https://www.legendsofidleon.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ---------- persistence ----------
const KEY = 'ac_cfg';
const cfg = Object.assign({
ivMin: 600, // ms — lower bound of click interval
ivMax: 1200, // ms — upper bound; each click picks uniformly in [min, max]
jitterPx: 2, // +/- position jitter in px (0 = pixel-perfect)
mode: 'cursor', // 'cursor' | 'fixed'
fx: 0, fy: 0, // fixed target, viewport px (legacy / no-canvas fallback)
fu: null, fv: null,// fixed target as a fraction of the game canvas rect
collapsed: false, // rolled up to just the title bar
hidden: false,
px: null, py: null // dragged panel position, viewport px
}, JSON.parse(localStorage.getItem(KEY) || '{}'));
// migrate old base+jitter config -> min/max range
if (cfg.ivMin === undefined && cfg.interval !== undefined) {
const j = cfg.jitterMs || 0;
cfg.ivMin = Math.max(20, cfg.interval - j);
cfg.ivMax = cfg.interval + j;
}
delete cfg.interval; delete cfg.jitterMs;
const save = () => localStorage.setItem(KEY, JSON.stringify(cfg));
// ---------- state ----------
let on = false, timer = null, capturing = false;
// lastX/lastY only move while the pointer is over THIS window. In a second
// window — side by side, or simply not the one being pointed at — they go
// stale the moment it leaves, and they are 0,0 before it has ever arrived.
// Cursor mode would then click the top-left corner of the game forever,
// silently, which is not a harmless no-op: a click on bare ground is a walk
// command, so the character strolls off. ptrIn is what says whether the
// coordinates mean anything.
let lastX = 0, lastY = 0, ptrIn = false, wasBlind = false;
document.addEventListener('mousemove', e => {
lastX = e.clientX; lastY = e.clientY; ptrIn = true;
}, true);
// mouseout with a null relatedTarget is the pointer leaving the document
// altogether; leaving for the panel, or for any other element, names that
// element instead and does not count.
document.addEventListener('mouseout', e => {
if (!e.relatedTarget) ptrIn = false;
}, true);
// ---------- stealth UI host (closed shadow DOM, hidden from page JS) ----------
const host = document.createElement('div');
host.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483647';
const root = host.attachShadow({ mode: 'closed' });
const mount = () => document.documentElement.appendChild(host);
mount();
root.innerHTML = `
`;
const $ = s => root.querySelector(s);
const dot = $('#dot'), runBtn = $('#run'), ivMinEl = $('#ivmin'), ivMaxEl = $('#ivmax'),
jpEl = $('#jp'), xyEl = $('#xy'), setBtn = $('#set'), panel = $('#p'),
nub = $('#nub'), body = $('.body'), minBtn = $('#min');
// ---------- remembered panel position ----------
// Where the panel was dragged to is kept in the same config as everything
// else, so it comes back there on the next load instead of jumping to the
// corner it was built in. Clamped on the way in: a position saved on a wider
// window would otherwise put the panel off-screen, where the only way back is
// clearing localStorage.
if (cfg.px != null && cfg.py != null) {
const w = panel.offsetWidth || 220, h = 40;
panel.style.right = 'auto';
panel.style.left = Math.max(0, Math.min(cfg.px, window.innerWidth - w)) + 'px';
panel.style.top = Math.max(0, Math.min(cfg.py, window.innerHeight - h)) + 'px';
}
// ---------- UI sync ----------
function sync() {
ivMinEl.value = cfg.ivMin; ivMaxEl.value = cfg.ivMax; jpEl.value = cfg.jitterPx;
root.querySelectorAll('.seg button').forEach(b => b.classList.toggle('sel', b.dataset.m === cfg.mode));
xyEl.textContent = cfg.mode !== 'fixed'
? (ptrIn ? '(follows cursor)' : 'cursor is in another window')
: hasTarget() ? fixedPoint().map(Math.round).join(', ') : 'not set';
dot.classList.toggle('on', on);
runBtn.textContent = on ? 'Stop (F8)' : 'Start (F8)';
runBtn.className = 'btn ' + (on ? 'stop' : 'go');
setBtn.textContent = capturing ? 'Click a spot…' : 'Set Position';
body.style.display = cfg.collapsed ? 'none' : '';
minBtn.textContent = cfg.collapsed ? '+' : '–';
panel.style.display = cfg.hidden ? 'none' : '';
nub.style.display = cfg.hidden ? '' : 'none';
}
// ---------- target resolution ----------
// Largest canvas on the page is the game; anything smaller is a UI element.
function gameCanvas() {
let best = null, area = 0;
for (const c of document.querySelectorAll('canvas')) {
const a = c.clientWidth * c.clientHeight;
if (a > area) { area = a; best = c; }
}
return area > 160000 ? best : null; // ignore tiny/UI canvases
}
// A fixed target is stored as a fraction of the game canvas, not as viewport
// pixels, so it survives a resize, zoom or fullscreen toggle. The canvas is
// scaled and letterboxed, so the same screen pixel lands on a different spot
// in the world once its size changes — and a click on bare ground is a walk
// command, which is how a drifted target sends the character strolling off.
const hasTarget = () => cfg.fu != null || cfg.fx || cfg.fy;
function fixedPoint() {
const cv = gameCanvas();
if (!cv || cfg.fu == null) return [cfg.fx, cfg.fy]; // pre-3.3 config
const r = cv.getBoundingClientRect();
return [r.left + cfg.fu * r.width, r.top + cfg.fv * r.height];
}
// ---------- clicking ----------
const rand = j => j ? (Math.random() * 2 - 1) * j : 0;
function clickAt(x, y) {
x = Math.round(x + rand(cfg.jitterPx));
y = Math.round(y + rand(cfg.jitterPx));
const el = document.elementFromPoint(x, y) || gameCanvas();
if (!el) return;
for (const type of ['mousemove', 'mousedown', 'mouseup', 'click']) {
el.dispatchEvent(new MouseEvent(type, {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y, button: 0, buttons: type === 'mousedown' ? 1 : 0
}));
}
}
function tick() {
if (!on) return;
// Cursor mode with the pointer in another window has nothing to aim at, so
// it holds rather than clicking a stale coordinate. The timer keeps running
// and it resumes by itself when the pointer comes back. Announced, because
// the failure is otherwise invisible: the clicker looks like it is running
// and the game just never responds.
const blind = cfg.mode !== 'fixed' && !ptrIn;
if (blind !== wasBlind) { wasBlind = blind; sync(); }
// Resolved every tick: the canvas rect can change under a running clicker.
if (!blind && (cfg.mode !== 'fixed' || hasTarget())) {
const [tx, ty] = cfg.mode === 'fixed' ? fixedPoint() : [lastX, lastY];
clickAt(tx, ty);
}
const lo = Math.min(cfg.ivMin, cfg.ivMax), hi = Math.max(cfg.ivMin, cfg.ivMax);
timer = setTimeout(tick, Math.max(20, lo + Math.random() * (hi - lo)));
}
function start() { if (!on) { on = true; sync(); tick(); } }
function stop() { on = false; clearTimeout(timer); sync(); }
function toggle(){ on ? stop() : start(); }
// ---------- position capture ----------
function armCapture() {
capturing = true; sync();
const grab = e => {
e.preventDefault(); e.stopPropagation();
cfg.fx = e.clientX; cfg.fy = e.clientY;
const cv = gameCanvas(), r = cv && cv.getBoundingClientRect();
cfg.fu = r ? (e.clientX - r.left) / r.width : null;
cfg.fv = r ? (e.clientY - r.top) / r.height : null;
cfg.mode = 'fixed';
capturing = false; save(); sync();
window.removeEventListener('mousedown', grab, true);
};
window.addEventListener('mousedown', grab, true);
}
// ---------- wiring ----------
runBtn.onclick = toggle;
setBtn.onclick = () => capturing ? null : armCapture();
ivMinEl.onchange = e => { cfg.ivMin = Math.max(20, +e.target.value); save(); };
ivMaxEl.onchange = e => { cfg.ivMax = Math.max(20, +e.target.value); save(); };
jpEl.onchange = e => { cfg.jitterPx = Math.max(0, +e.target.value); save(); };
root.querySelectorAll('.seg button').forEach(b => b.onclick = () => { cfg.mode = b.dataset.m; save(); sync(); });
// Roll the panel up rather than hiding it outright — the title bar stays on
// screen so it can always be clicked open again.
minBtn.onclick = () => { cfg.collapsed = !cfg.collapsed; save(); sync(); };
nub.onclick = () => { cfg.hidden = false; save(); sync(); };
// drag
(() => {
let dx, dy, drag = false;
$('#hd').addEventListener('mousedown', e => {
if (e.target.id === 'min') return;
drag = true; const r = panel.getBoundingClientRect();
dx = e.clientX - r.left; dy = e.clientY - r.top;
panel.style.right = 'auto';
});
window.addEventListener('mousemove', e => {
if (!drag) return;
panel.style.left = (e.clientX - dx) + 'px';
panel.style.top = (e.clientY - dy) + 'px';
});
window.addEventListener('mouseup', () => {
if (!drag) return;
drag = false;
const r = panel.getBoundingClientRect();
cfg.px = Math.round(r.left); cfg.py = Math.round(r.top);
save();
});
})();
// Keep every control out of the tab order and drop focus as soon as it is
// released, so a Space or Enter aimed at the game can't re-fire whichever
// button was touched last.
root.querySelectorAll('button').forEach(el => {
el.setAttribute('tabindex', '-1');
el.addEventListener('mouseup', () => el.blur());
});
root.querySelectorAll('input').forEach(el => el.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === 'Escape') el.blur();
}));
// hotkeys (capture phase). These fire even while a number input holds focus:
// a function key is never typed into a field, and the game canvas swallows
// the mousedown that would otherwise blur it — so a field left focused used
// to strand F8/F9/F10 with no way back except the mouse. Whatever is focused
// is blurred on the way through, which also commits a half-typed value.
window.addEventListener('keydown', e => {
if (e.key !== 'F8' && e.key !== 'F9' && e.key !== 'F10') return;
e.preventDefault();
if (root.activeElement) root.activeElement.blur();
if (e.key === 'F8') toggle();
if (e.key === 'F9') stop(); // panic off
if (e.key === 'F10') { cfg.hidden = !cfg.hidden; save(); sync(); }
}, true);
sync();
})();