Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions idleon-clicker.user.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ==UserScript==
// @name IdleOn Clicker
// @namespace nativerobot
// @version 3.5
// @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)
Expand Down Expand Up @@ -36,8 +36,23 @@

// ---------- state ----------
let on = false, timer = null, capturing = false;
let lastX = 0, lastY = 0;
document.addEventListener('mousemove', e => { lastX = e.clientX; lastY = e.clientY; }, true);
// 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');
Expand Down Expand Up @@ -120,7 +135,8 @@
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' ? '(follows cursor)'
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)';
Expand Down Expand Up @@ -175,8 +191,15 @@

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 (cfg.mode !== 'fixed' || hasTarget()) {
if (!blind && (cfg.mode !== 'fixed' || hasTarget())) {
const [tx, ty] = cfg.mode === 'fixed' ? fixedPoint() : [lastX, lastY];
clickAt(tx, ty);
}
Expand Down
233 changes: 223 additions & 10 deletions idleon-suite.user.js

Large diffs are not rendered by default.

116 changes: 114 additions & 2 deletions tools/suite/00-head.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@
// ---------- which helpers are on ----------
const SUITE_KEY = 'idleon_suite';
const ALL_ON = { clicker: true, hoops: true, fishing: true, darts: true };
const suite = Object.assign({ collapsed: false, hidden: false },
// layout: 'free' keeps the dragged-anywhere behaviour every version until now
// had, and stays the default so an upgrade moves nobody's panels. 'left' and
// 'top' dock them into one column or one row.
// solo: opening a helper closes the other helpers. Only meaningful docked,
// where they share a column; see the collapse handler.
// follow: opt in to letting the active minigame open its own helper.
const suite = Object.assign({ collapsed: false, hidden: false,
layout: 'free', solo: true, follow: false },
JSON.parse(localStorage.getItem(SUITE_KEY) || '{}'));
suite.enabled = Object.assign({}, ALL_ON, suite.enabled);
const saveSuite = () => localStorage.setItem(SUITE_KEY, JSON.stringify(suite));
Expand Down Expand Up @@ -130,6 +137,87 @@
return img;
}

// ---------- docked layouts ----------
// Five panels is a lot of furniture to arrange by hand every session, and
// only one helper is ever useful at a time — you are in exactly one minigame.
// Docking stacks them against an edge in a fixed order and takes over their
// positions; the saved px/py are left untouched so switching back to 'free'
// restores exactly where things were.
const docks = []; // { def, ui }, sorted by def.dockOrder
const DOCK_EDGE = 10, DOCK_GAP = 8;
let relayoutPending = false;

function relayout() {
if (suite.layout === 'free') {
for (const d of docks) d.ui.place();
return;
}
const vert = suite.layout === 'left';
// ?? not ||: the hub is dockOrder 0, which || would treat as missing and
// sort to the bottom of its own dock.
const list = docks.slice().sort((a, b) => (a.def.dockOrder ?? 99) - (b.def.dockOrder ?? 99));
// A run wraps rather than running off the edge. Five panels do not fit
// across a half-width window, and with several expanded they do not fit
// down a short one either — and a panel past the edge is the exact trap the
// clamping in place() exists to avoid: unreachable, and unreachable means
// undraggable, so there is no way back to it.
//
// `run` is the thickness of the current row (or column): the tallest panel
// in a row, the widest in a column, which is what the next one has to clear.
// The first panel of a run never wraps — if one panel is bigger than the
// whole viewport there is nowhere better for it, and wrapping on it would
// spin.
let x = DOCK_EDGE, y = DOCK_EDGE, run = 0;
for (const { ui } of list) {
if (ui.cfg.hidden) continue; // hidden panels are a nub, not a slot
const p = ui.panel;
p.style.right = 'auto';
// Measured before placing: the width is pinned by style.width so the
// height does not depend on where it ends up, and the wrap has to be
// decided before the position is written.
const r = p.getBoundingClientRect();
if (vert) {
if (y > DOCK_EDGE && y + r.height > window.innerHeight - DOCK_EDGE) {
x += run + DOCK_GAP; y = DOCK_EDGE; run = 0;
}
} else if (x > DOCK_EDGE && x + r.width > window.innerWidth - DOCK_EDGE) {
y += run + DOCK_GAP; x = DOCK_EDGE; run = 0;
}
p.style.left = x + 'px';
p.style.top = y + 'px';
if (vert) { y += r.height + DOCK_GAP; run = Math.max(run, r.width); }
else { x += r.width + DOCK_GAP; run = Math.max(run, r.height); }
}
}

// The hub owns the layout controls, but a drag out of a dock has to change
// the layout from inside makePanel. This is the seam between the two.
let onLayoutChange = () => {};
// Solo has to be an invariant, not just something the collapse button does.
// Arriving in a dock with four helpers already open gives a column that needs
// two of them to fit — which is the exact thing the dock is for avoiding. So
// entering a docked layout closes all but the first open helper.
function enforceSolo() {
if (!suite.solo || suite.layout === 'free') return;
let kept = false;
const list = docks.slice().sort((a, b) => (a.def.dockOrder ?? 99) - (b.def.dockOrder ?? 99));
for (const d of list) {
if (!d.def.helper || d.ui.cfg.collapsed) continue;
if (!kept) { kept = true; continue; } // the first open one stays open
d.ui.cfg.collapsed = true; d.ui.save(); d.ui.chrome();
}
}
function syncLayout() { enforceSolo(); relayout(); onLayoutChange(); }

// Collapsing a panel changes every panel below it, so the re-stack is
// coalesced to one pass per frame rather than run per panel per change.
function relayoutSoon() {
if (relayoutPending) return;
relayoutPending = true;
requestAnimationFrame(() => { relayoutPending = false; relayout(); });
}
window.addEventListener('resize', relayoutSoon);

// ---------- panel chrome ----------
// Every panel is the same furniture around a different body: a title bar
// that drags, a roll-up toggle, a hide toggle, and a nub that brings a
Expand Down Expand Up @@ -245,12 +333,17 @@
minBtn.textContent = cfg.collapsed ? '+' : '–';
panel.style.display = cfg.hidden ? 'none' : '';
nub.style.display = cfg.hidden ? '' : 'none';
relayoutSoon(); // heights and occupancy just changed
}

// drag
let dx = 0, dy = 0, drag = false;
$('#hd').addEventListener('mousedown', e => {
if (e.target.id === 'min') return;
// Dragging out of a dock means you want it somewhere else, so the dock
// gets out of the way rather than snapping the panel back and looking
// broken. "Reset panel layout" puts it back.
if (suite.layout !== 'free') { suite.layout = 'free'; saveSuite(); syncLayout(); }
drag = true;
const r = panel.getBoundingClientRect();
dx = e.clientX - r.left; dy = e.clientY - r.top;
Expand Down Expand Up @@ -278,6 +371,7 @@
ui.save(); place(); chrome();
},
dot: $('#dot'), runBtn: $('#run'), stEl: $('#st'), nub, minBtn, body,
place, // so a dock can hand positions back on the way out
save: () => {}, // replaced by the module, which owns its store
// 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
Expand All @@ -296,10 +390,28 @@
for (const [tg, ty, fn, cap] of bound) tg.removeEventListener(ty, fn, cap);
roots.delete(root);
host.remove();
const i = docks.findIndex(d => d.ui === ui);
if (i >= 0) docks.splice(i, 1);
relayoutSoon();
}
};

minBtn.addEventListener('click', () => { cfg.collapsed = !cfg.collapsed; ui.save(); chrome(); });
docks.push({ def, ui });
minBtn.addEventListener('click', () => {
cfg.collapsed = !cfg.collapsed;
ui.save();
// Solo closes the other HELPERS when you open one — not the clicker,
// which is useful alongside any of them, and not the suite panel. Only
// while docked: in the free layout the panels are wherever you put them
// and collapsing one you never touched would just look like a bug.
if (!cfg.collapsed && suite.solo && suite.layout !== 'free' && def.helper) {
for (const d of docks) {
if (d.ui === ui || !d.def.helper || d.ui.cfg.collapsed) continue;
d.ui.cfg.collapsed = true; d.ui.save(); d.ui.chrome();
}
}
chrome();
});
nub.addEventListener('click', () => { cfg.hidden = false; ui.save(); chrome(); });
return ui;
}
Expand Down
17 changes: 14 additions & 3 deletions tools/suite/01-clicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
z: 2147483646,
theme: { dot: '#4ade80', ac: '#2563eb', stop: '#dc2626' },
slot: { top: 12, right: 12, width: 210, nub: 24 },
dockOrder: 1,
overlay: false,
hotkeys: { F8: 'toggle', F9: 'panic', F10: 'hide' },
keyHint: 'F8',
Expand All @@ -38,13 +39,23 @@
xyEl = $('#xy'), setBtn = $('#set');

let on = false, timer = null, capturing = false;
let lastX = 0, lastY = 0;
ui.on(document, 'mousemove', e => { lastX = e.clientX; lastY = e.clientY; }, true);
// lastX/lastY only move while the pointer is over THIS window, so in a
// second window they go stale on the way out and are 0,0 before it has
// ever arrived. See the standalone clicker for the whole story; ptrIn is
// what says whether the coordinates mean anything.
let lastX = 0, lastY = 0, ptrIn = false, wasBlind = false;
ui.on(document, 'mousemove', e => {
lastX = e.clientX; lastY = e.clientY; ptrIn = true;
}, true);
// A null relatedTarget is the pointer leaving the document altogether;
// leaving for a panel names that element instead and does not count.
ui.on(document, 'mouseout', e => { if (!e.relatedTarget) ptrIn = false; }, true);

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' ? '(follows cursor)'
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)';
Expand Down
5 changes: 4 additions & 1 deletion tools/suite/02-hoops-post.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

return { loop, sync, toggle };
// For the suite's auto-open: the platform is found every frame the court is up.
// Reusing the loop's own state rather than testing the screen again --
// a second detector here would be one more thing to drift.
return { loop, sync, toggle, active: () => plat != null };
}
};
1 change: 1 addition & 0 deletions tools/suite/02-hoops-pre.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
z: 2147483645,
theme: { dot: '#f87171', ac: '#dc2626' },
slot: { top: 12, left: 220, width: 228, nub: 42 },
dockOrder: 2, helper: true,
overlay: true,
hotkeys: { F7: 'toggle', F6: 'hide' },
keyHint: 'F7',
Expand Down
5 changes: 4 additions & 1 deletion tools/suite/03-fish-post.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

return { loop, sync, toggle };
// For the suite's auto-open: the lane goes null the moment the fishing spot is off screen.
// Reusing the loop's own state rather than testing the screen again --
// a second detector here would be one more thing to drift.
return { loop, sync, toggle, active: () => lane != null };
}
};
1 change: 1 addition & 0 deletions tools/suite/03-fish-pre.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
z: 2147483644,
theme: { dot: '#38bdf8', ac: '#0284c7' },
slot: { top: 12, left: 460, width: 214, nub: 60 },
dockOrder: 3, helper: true,
overlay: true,
hotkeys: { F4: 'toggle', F3: 'hide' },
keyHint: 'F4',
Expand Down
5 changes: 4 additions & 1 deletion tools/suite/04-darts-post.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

return { loop, sync, toggle };
// For the suite's auto-open: the board is nulled by the wall gate and after 900ms stale.
// Reusing the loop's own state rather than testing the screen again --
// a second detector here would be one more thing to drift.
return { loop, sync, toggle, active: () => board != null };
}
};
1 change: 1 addition & 0 deletions tools/suite/04-darts-pre.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
z: 2147483643,
theme: { dot: '#fbbf24', ac: '#d97706' },
slot: { top: 12, left: 686, width: 216, nub: 78 },
dockOrder: 4, helper: true,
overlay: true,
hotkeys: { F2: 'toggle', F1: 'hide' },
keyHint: 'F2',
Expand Down
Loading
Loading