Skip to content
67 changes: 60 additions & 7 deletions idleon-darts.user.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ==UserScript==
// @name IdleOn Darts Helper
// @namespace nativerobot
// @version 1.14
// @version 1.15
// @downloadURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-darts.user.js
// @updateURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-darts.user.js
// @description Draws the predicted dart path and where it lands on the board, wind included, for the Throwy Darts minigame
Expand Down Expand Up @@ -84,10 +84,11 @@
// belongs to a later aim. That method cannot measure this and should not be
// used to re-tune landN. Compare against the tracked flight instead.
landN: 0, // landing correction / height
// v6: magenta is NO LONGER gated. The colour was never a kind of wind, it is
// a strength tier — the game picks the arrow sprite as
// v6: magenta is NO LONGER gated, and v7 added red. The colour was never a
// kind of wind, it is a strength tier — the game picks the arrow sprite as
// mag < 10 ? DartWind0 : mag < 18 ? DartWind1 : DartWind2
// so cyan is simply every wind under 10 mph and magenta is 10-17. Every
// so cyan is every wind under 10 mph, magenta 10-17, red 18 and up. Red was
// not matched at all until v7 and read as 'none'; see windPx. Every
// cyan logged here came in at 4/6/8/9 mph and every magenta at 10/11/13,
// which is that boundary exactly. Gating magenta therefore threw away the
// STRONGEST winds, modelling a 13 mph crosswind as still air.
Expand Down Expand Up @@ -416,15 +417,62 @@
// Do not "calibrate" windK against this until the offset is anchored.
// S is the native-resolution crop from grabWind, so the whole image IS the
// window -- no sub-window arithmetic here any more.
// The three arrow sprites, and the one that used to be invisible.
//
// DartWind0 cyan hue 185-209 v .91-1.00 under 10 mph
// DartWind1 magenta hue 275-293 v 1.00 10-17 mph
// DartWind2 red hue 3- 36 v 1.00 18 mph and up
//
// Only the first two were ever matched, so an 18+ mph wind read as 'none' and
// was modelled as still air -- the strongest winds in the game, treated as no
// wind at all. Exactly the same shape of bug as the magenta gate.
//
// Red needs care the other two do not. It shares the HUD's own colours: the
// brown panel behind it is hue 0-32 saturation .30-.75, and the amber text and
// trim beside it run hue 33-44 -- so the arrow overlaps its background in BOTH
// hue and saturation. Hue cannot separate them at all: the arrow's hue is
// quantised, 73.5% of it below 36.3 and the remainder exactly at 36.3, right
// inside the amber.
//
// Brightness helps -- the arrow is v=1.00 throughout and the brown never gets
// past .72 -- but it is not enough on its own, because the amber reaches .96.
// What actually separates an arrow from HUD text is that an arrow is a solid
// blob; see the density gate in readWind.
//
// One asymmetry to know about: every darts recording reports 'none', which
// makes them a free test that red is not seen where it should not be. None of
// them contains an 18+ mph wind, so that red IS seen when it should be stays
// unverified until one turns up.
const windPx = (h, s, v) =>
s > 0.35 && v > 0.6 && (
(h > 165 && h < 215) || // cyan
(h > 270 && h < 335) || // magenta
(h < 45 && v > 0.85) // red, 18 mph and up
);

function readWind(S) {
if (!S) return { key: 'none', deg: 0 };
let pts = [];
for (let y = 0; y < S.h; y++)
for (let x = 0; x < S.w; x++) {
const [h, s, v] = px(S, x, y);
if (s > 0.35 && v > 0.6 && ((h > 165 && h < 215) || (h > 270 && h < 335))) pts.push({ x, y, h });
if (windPx(h, s, v)) pts.push({ x, y, h });
}
if (pts.length < 8) return { key: 'none', deg: 0 };
// An arrow is a BLOB, not a scattering. Requiring merely 8 pixels was
// enough while only cyan and magenta were matched -- neither colour appears
// in the HUD -- but red shares the HUD's own palette, and a handful of
// amber text pixels would otherwise be read as a wind.
//
// Density is what separates them, and it does not care about colour at all:
// the arrow sprites fill 4-8% of this window (480, 518 and 258 px of a
// window that is 0.12W x 0.10H), while the amber scatter that was being
// picked up ran 22-32 px, under half a percent. 2% sits in the gap with
// room on both sides.
//
// This replaces a v threshold that was being tuned against whichever frame
// was last looked at -- .85 let 70 false frames through, .97 still let 22
// through -- which is fitting a constant to noise rather than measuring.
if (pts.length < 0.02 * S.w * S.h) return { key: 'none', deg: 0 };
// The window catches a few matching pixels hard against its left edge that
// are not part of the arrow at all -- seen as a stray column many pixels
// clear of the glyph in a captured mask. They are far enough out to drag
Expand All @@ -449,7 +497,12 @@
let ux = Math.cos(th), uy = Math.sin(th);
if (ux < 0) { ux = -ux; uy = -uy; }
const hue = pts.reduce((p, c) => p + c.h, 0) / n;
return { key: hue < 240 ? 'cyan' : 'magenta', deg: Math.atan2(-uy, ux) * 180 / Math.PI };
// Staged, not a single split: red sits at ~20, which a `hue < 240` test
// would have called cyan. predict() no longer cares which name it gets --
// every detected wind is trusted since v6 -- but the status line says it
// and the probe records it, so it should be the truth.
const key = hue < 45 ? 'red' : hue < 240 ? 'cyan' : 'magenta';
return { key, deg: Math.atan2(-uy, ux) * 180 / Math.PI };
}


Expand Down
70 changes: 64 additions & 6 deletions idleon-fishing.user.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ==UserScript==
// @name IdleOn Fishing Helper
// @namespace nativerobot
// @version 2.4
// @version 2.5
// @downloadURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-fishing.user.js
// @updateURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-fishing.user.js
// @description Draws where your cast will land, plus fish and hazard markers, for the IdleOn fishing minigame
Expand Down Expand Up @@ -314,12 +314,30 @@
const isEel = (h, s, v) => h > 30 && h < 55 && s > 0.35 && v > 0.55;
const isSquid = (h, s, v) => h > 255 && h <= 315 && s > 0.12 && v > 0.35;
const isWhale = (h, s, v) => h > 228 && h < 258 && s > 0.22 && s < 0.6 && v > 0.3;
// How close the bobber has to land, per species, as a fraction of the lane.
// The game's catch test is
// |fishX - bobberX| < 6 + SIZE[type]
// with SIZE = [6,6,9,10,12,13,17,17] in lane units and the 6 being the
// bobber's own half-width. Points identify the type: 1pt is type 2, 2pt is
// type 3, 3pt is type 4 and 5pt is type 6, so the tolerances come out at
// 15, 16, 18 and 23 lane units. The pufferfish is type 5, size 13, so 19.
//
// The lane is about 299.5 of those units across, and two independent routes
// agree on it: inverting the measured aim curve puts the lane ends at game x
// 11 and 311, and the game seeds fish between 40 and 295 with the bobber
// landing between 24 and 285 — all inside that span. Dividing by it turns a
// tolerance into a fraction of whatever the lane measures on screen, so this
// survives any window size, which raw pixels would not.
const LANE_UNITS = 299.5;
const tol = u => u / LANE_UNITS;

const SPECIES = [
{ name: 'FISH', pts: 1, color: '#4ade80', test: isFish },
{ name: 'EEL', pts: 2, color: '#facc15', test: isEel },
{ name: 'SQUID', pts: 3, color: '#e879f9', test: isSquid },
{ name: 'WHALE', pts: 5, color: '#60a5fa', test: isWhale },
{ name: 'FISH', pts: 1, color: '#4ade80', test: isFish, catchN: tol(15) },
{ name: 'EEL', pts: 2, color: '#facc15', test: isEel, catchN: tol(16) },
{ name: 'SQUID', pts: 3, color: '#e879f9', test: isSquid, catchN: tol(18) },
{ name: 'WHALE', pts: 5, color: '#60a5fa', test: isWhale, catchN: tol(23) },
];
const HAZARD_N = tol(19); // pufferfish, type 5, size 13

// ---------- the lane ----------
// The fishing lane is a long flat blue bar. Its longest horizontal run is both
Expand Down Expand Up @@ -916,6 +934,20 @@
// Left of each catch, the power that would land the cast on it — the
// number to release the gauge at. Recomputed every frame, so once the
// fish start moving (later in a run) the label tracks them.
// The catch WINDOW, not just the spot: a bar as wide as the tolerance the
// game actually allows, so a near miss is visibly near rather than a
// mystery. A whale is half again as forgiving as a fish, which is not
// something the sprite sizes make obvious.
octx.save();
octx.lineWidth = 3; octx.globalAlpha = 0.45;
octx.shadowColor = 'rgba(0,0,0,.6)'; octx.shadowBlur = 2;
for (const f of fish) {
const r = (f.catchN || 0) * laneW;
if (r <= 0) continue;
octx.strokeStyle = f.color;
octx.beginPath(); octx.moveTo(f.x - r, f.y); octx.lineTo(f.x + r, f.y); octx.stroke();
}
octx.restore();
for (const f of fish) {
const p = invAim((f.x - laneX0) / laneW);
drawLaneMark(f.x, f.y, f.color, `${f.name} +${f.pts}`, p !== null ? ((p * 100) | 0) + '%' : null);
Expand All @@ -927,8 +959,17 @@
// other over a spot you actually want to hit. Hazards only cost you when
// you land on a bare one, or miss everything; same W*0.02 as the marker.
for (const z of haz)
if (!fish.some(f => Math.abs(f.x - z.x) < W * 0.02))
if (!fish.some(f => Math.abs(f.x - z.x) < W * 0.02)) {
// Same treatment for the pufferfish: its window is how far away you
// have to stay, and at 19 lane units it is wider than every catch
// except the whale.
const r = HAZARD_N * laneW;
octx.save();
octx.strokeStyle = '#f87171'; octx.lineWidth = 3; octx.globalAlpha = 0.45;
octx.beginPath(); octx.moveTo(z.x - r, z.y); octx.lineTo(z.x + r, z.y); octx.stroke();
octx.restore();
drawLaneMark(z.x, z.y, '#f87171', 'AVOID');
}
}

// ---- power meter ----
Expand All @@ -951,7 +992,24 @@
const p = invAim((f.x - laneX0) / laneW);
if (p === null) continue;
const tx = m.x * kx, ty = (m.bot - p * (m.bot - m.top)) * ky;
// A BAND, not a tick: the ends of the catch window mapped back through
// the aim curve give the range of gauge fills that still land on this
// fish. That is the release slack, and it is what you are actually
// aiming at — a tick says where perfect is and nothing about how much
// room there is around it. The curve is not linear, so the band is not
// symmetric about the tick, and it tightens the further out the fish is.
const r = (f.catchN || 0) * laneW;
const pLo = invAim((f.x - r - laneX0) / laneW);
const pHi = invAim((f.x + r - laneX0) / laneW);
octx.strokeStyle = f.color;
if (pLo !== null && pHi !== null) {
const yLo = (m.bot - pLo * (m.bot - m.top)) * ky;
const yHi = (m.bot - pHi * (m.bot - m.top)) * ky;
octx.save();
octx.globalAlpha = 0.35; octx.lineWidth = 6;
octx.beginPath(); octx.moveTo(tx - 2, yLo); octx.lineTo(tx - 2, yHi); octx.stroke();
octx.restore();
}
octx.beginPath(); octx.moveTo(tx - 12, ty); octx.lineTo(tx + 8, ty); octx.stroke();
}
octx.restore();
Expand Down
Loading
Loading