From 05b485ea2962c31d92a6ec3ea65f62bdbbb9433a Mon Sep 17 00:00:00 2001 From: averagenative Date: Tue, 1 Sep 2026 12:57:39 -0400 Subject: [PATCH] Ring the squid and the urchins, and stop the aim fit flattening Four fixes, all measured off the live game canvas over the DevTools protocol rather than off a screen recording. That distinction mattered: a recording of a session with the overlay on feeds the helper's own marks back into its detection, and the capture pipeline shifts the very saturations this is about. Both sent the first diagnosis the wrong way. isSquid: the saturation floor came off the game's legend icon and was far too high for the sprite in the lane. The live sprite is three flat tones -- body rgb(237,204,240) at s.15, shading rgb(147,89,161) at s.45, outline rgb(49,24,60) at v.24 -- and s > .22 kept only the middle one. Those 63 pixels are scattered singles through the body they outline, so nothing connected: no component reached even a 21px floor against the 63px the count screen wanted, and the squid was never ringed at all. At s > .12 it comes back as one 173px, 22x24 blob, and a whole-canvas sweep at s > .10 turned up no new component anywhere. isHazard: 315..360 does not wrap, and hsvAt returns hue 0 for pure red. The urchin's two strongest tones sit at exactly hue 0, so not one of the sprite's 456 pixels could ever match and AVOID never appeared. Now uses inH, the same wrapping helper isBobber has always used. refitAim: below eight samples the fit dropped to a straight line, sound when the seed was itself a line and wrong once v6 made the seed a curve measured over 19 casts. It fires after three casts, and a live config held aim2 = 0 from six samples spanning only p .25-.625 -- a line that reads +2.2% of the lane at half power and -6.9% at full, so far targets demand more power than they need and you release early to land. The fallback now pins the curvature to the seed and fits only slope and offset; on those same six samples that beats the line on its own data, 1.34% mean residual against 1.52%, without the collapsed top end. A migration restores the seed for configs already holding aim2 = 0. Blob search: the box stopped exactly at the lane ends, slicing a sprite in half as it swam past. Not the count floor -- at 99% of the lane the squid still had 84px against a floor of 63, but three surviving columns are not 1.2% of the canvas wide, so the width screen killed it. Padding by half a sprite holds the ring to the end of the water. Also stop ringing AVOID on a hazard with a catch sitting on it. Landing there catches the fish, which is why the aim marker already lets the catch colour outrank the hazard colour; the ring argued with itself. --- idleon-fishing.user.js | 95 ++++++++++++++++++++++++++++++++++++++---- idleon-suite.user.js | 95 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 172 insertions(+), 18 deletions(-) diff --git a/idleon-fishing.user.js b/idleon-fishing.user.js index 41aa454..a21b044 100644 --- a/idleon-fishing.user.js +++ b/idleon-fishing.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name IdleOn Fishing Helper // @namespace nativerobot -// @version 2.1 +// @version 2.2 // @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 @@ -70,6 +70,11 @@ cfg.aim2 = 0.3095; cfg.aim1 = 0.5631; cfg.aim0 = 0.0420; delete cfg.aimA; delete cfg.aimB; } + // A zero curvature can only have come from the straight-line fallback that + // refitAim used to drop to below eight samples — the seed has never been a + // line under calVer 6. Put the seed curve back; the samples themselves are + // still good, and the first cast landed from here refits them properly. + if (!cfg.aim2) { cfg.aim2 = 0.3095; cfg.aim1 = 0.5631; cfg.aim0 = 0.0420; } let saveAt = 0; const save = () => localStorage.setItem(KEY, JSON.stringify(cfg)); const saveSoon = () => { const t = performance.now(); if (t - saveAt > 1000) { saveAt = t; save(); } }; @@ -239,7 +244,17 @@ // Hazard hue and brightness vary by fishing spot: the first one measured sat // at hue 320-340 / v.46-.54, another at hue ~350 / v up to .85. Too narrow a // window and the AVOID rings simply never appear. - const isHazard = (h, s, v) => h > 315 && h <= 360 && s > 0.28 && v > 0.3; + // + // The window also has to WRAP, and it never did. Written as 315..360 it + // stops dead at the top of the circle, and hsvAt returns hue 0 — not 360 — + // for pure red. A colour census of the Blunder Hills lane read off the live + // canvas found the urchin's two strongest tones sitting at exactly hue 0: + // rgb(240,66,66) s.73 v.94, 96px of outline, and rgb(125,93,93) s.26 v.49, + // 96px of body. Both were outside 315..360, so not one of the sprite's 456 + // pixels could ever match and the AVOID ring never appeared at that spot at + // all. inH wraps the same way isBobber's 345..15 does; +5 is enough to cover + // hue 0 without letting the window drift towards the sand's orange. + const isHazard = (h, s, v) => inH(h, 315, 5) && s > 0.28 && v > 0.3; // Higher-tier catches unlocked by landing streaks (eel +2, squid +3, whale +5), // measured off the game's legend sprites. The squid's purple sits at 255-315, // just clear of the hazard window, which starts at 315. Only the whale's @@ -248,8 +263,25 @@ // The lane's own shadow edge reaches hue ~225 at s .6-.7, which a window // starting at 216 rang as a whale; the whale body is hue ~230 at s ~.4, so // both the hue floor and a saturation ceiling keep the shadow out. + // + // The squid's saturation floor came off that legend icon and was far too high + // for the sprite swimming in the lane. Read off the live canvas, the sprite is + // three flat tones and only ONE of them cleared s > .22: + // + // rgb(237,204,240) h295 s.150 v.94 138px body — the bulk of it + // rgb(147,89,161) h288 s.447 v.63 63px mid shading + // rgb(49,24,60) h282 s.600 v.24 77px outline (below the v floor) + // + // Keeping only the 63 shading pixels left them scattered as single dots + // through the body they outline, so nothing connected: no component reached + // even a 21px floor, let alone the 63px the count screen wanted, and the + // squid was never ringed once. At s > .12 the sprite comes back as a single + // 173px, 22x24 blob. The floor is set .03 below the body's exact .15 rather + // than snug against it because that .15 is a palette entry, and a sprite + // drawn at another scale blends its edges. A whole-canvas sweep at s > .10 + // turned up no new component anywhere, so there is room below to spend. 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.22 && v > 0.35; + 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; const SPECIES = [ { name: 'FISH', pts: 1, color: '#4ade80', test: isFish }, @@ -522,9 +554,27 @@ // A parabola needs its samples spread out to be worth fitting: six casts all // at half power pin the middle and let the ends fly anywhere, which is a // worse predictor than the seed they replaced. So the quadratic is only - // accepted with enough samples over a wide enough range of the gauge, and a - // straight line is fitted below that — still better than nothing, and it - // cannot bend the wrong way. + // accepted with enough samples over a wide enough range of the gauge. + // + // Below that the fit used to drop to a straight line, on the reasoning that a + // line is "still better than nothing and cannot bend the wrong way". That was + // true when the seed was itself a line, and became wrong the moment v6 made + // the seed a curve measured over 19 casts and two spots: the fallback was no + // longer replacing nothing, it was replacing the best number in the file. In + // practice it fired almost immediately — three casts is enough — and a live + // config caught in the act held aim2 = 0, aim1 = .8456 from six samples that + // spanned only p .25 to .625. Against the seed that line reads +2.2% of the + // lane at half power and -6.9% at full, so the further the target the more + // power it demands, and you have to release early to land anything. That is + // exactly the complaint v6 was supposed to have fixed. + // + // So the fallback keeps the curvature and fits only what the samples can + // honestly see: the slope and the offset. Both spots measured for v6 fell on + // the same curve, which makes SEED_C2 the game's law rather than one lane's + // quirk, while slope and offset absorb the things that do move — chiefly how + // wide findLane measured this particular lane. Two free parameters need far + // fewer samples than three, and the result cannot bend the wrong way either. + const SEED_C2 = 0.3095; // must move with cfg.aim2's default and migration function refitAim() { const S = cfg.samples; if (S.length < 3) return; @@ -543,8 +593,13 @@ if (sol) [c2, c1, c0] = sol; } if (!c2) { + // Curvature pinned, slope and offset least-squared over l - SEED_C2*p^2. + c2 = SEED_C2; let n = 0, sx = 0, sy = 0, sxx = 0, sxy = 0; - for (const [p, l] of S) { n++; sx += p; sy += l; sxx += p * p; sxy += p * l; } + for (const [p, l] of S) { + const y = l - c2 * p * p; + n++; sx += p; sy += y; sxx += p * p; sxy += p * y; + } const den = n * sxx - sx * sx; if (Math.abs(den) < 1e-6) return; c1 = (n * sxy - sx * sy) / den; @@ -662,7 +717,21 @@ let fish = [], haz = [], landed = null; if (S) { const toCss = o => ({ x: o.x / S.w * W, y: (S.sy + o.y) / S.cvH * H, w: o.w / S.w * W, h: o.h / S.cvH * H, n: o.n }); - const sx0 = Math.round(laneX0 / W * S.w), sx1 = Math.round(laneX1 / W * S.w); + // Search a little PAST both ends of the lane. A catch drifting to the end + // of the water keeps swimming until its centre is level with the last + // pixel of the bar, and half its sprite then hangs over the edge — but + // the search box used to stop exactly at laneX1, so the blob was sliced + // down the middle as it went. Watched live at Blunder Hills, a squid held + // 173px and a solid ring out to 96% of the lane, then fell to 84px and + // lost the ring at 99% purely to the slicing: the count floor was never + // the problem, the width screen was, because three surviving columns are + // not 1.2% of the canvas wide. Half a sprite is the margin that fixes it + // — that squid measured 22px against a 296px lane, so 5% is half a sprite + // with a little room. Nothing downstream gets looser: startX still throws + // out the sand mound off the left end, and onLane still throws out the + // surfboard rack off the right. + const pad = Math.round(laneW * 0.05 / W * S.w); + const sx0 = Math.round(laneX0 / W * S.w) - pad, sx1 = Math.round(laneX1 / W * S.w) + pad; const px = S.w / W; // native px per CSS px // The strip is tall enough that scenery pokes into it: the surfboard // rack at the spot's right edge matches both the hazard pink and the @@ -704,7 +773,15 @@ const p = invAim((f.x - laneX0) / laneW); drawLaneMark(f.x, f.y, f.color, `${f.name} +${f.pts}`, p !== null ? ((p * 100) | 0) + '%' : null); } - for (const z of haz) drawLaneMark(z.x, z.y, '#f87171', 'AVOID'); + // A hazard with a catch sitting on it is not a hazard. Land there and the + // catch is what you get — which is why the aim marker below already lets + // the catch colour outrank the hazard colour. Ringing it AVOID as well + // put a red ring and a species ring on the same pixel, arguing with each + // 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)) + drawLaneMark(z.x, z.y, '#f87171', 'AVOID'); } // ---- power meter ---- diff --git a/idleon-suite.user.js b/idleon-suite.user.js index 3bd40ad..5b1ee9c 100644 --- a/idleon-suite.user.js +++ b/idleon-suite.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name IdleOn Helper Suite // @namespace nativerobot -// @version 1.3 +// @version 1.5 // @downloadURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-suite.user.js // @updateURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-suite.user.js // @description All-in-one: autoclicker + Hoops, Fishing and Darts minigame helpers for Legends of IdleOn, each one individually switchable @@ -1381,6 +1381,11 @@ cfg.aim2 = 0.3095; cfg.aim1 = 0.5631; cfg.aim0 = 0.0420; delete cfg.aimA; delete cfg.aimB; } + // A zero curvature can only have come from the straight-line fallback that + // refitAim used to drop to below eight samples — the seed has never been a + // line under calVer 6. Put the seed curve back; the samples themselves are + // still good, and the first cast landed from here refits them properly. + if (!cfg.aim2) { cfg.aim2 = 0.3095; cfg.aim1 = 0.5631; cfg.aim0 = 0.0420; } }); const FISHING = { @@ -1473,7 +1478,17 @@ // Hazard hue and brightness vary by fishing spot: the first one measured sat // at hue 320-340 / v.46-.54, another at hue ~350 / v up to .85. Too narrow a // window and the AVOID rings simply never appear. - const isHazard = (h, s, v) => h > 315 && h <= 360 && s > 0.28 && v > 0.3; + // + // The window also has to WRAP, and it never did. Written as 315..360 it + // stops dead at the top of the circle, and hsvAt returns hue 0 — not 360 — + // for pure red. A colour census of the Blunder Hills lane read off the live + // canvas found the urchin's two strongest tones sitting at exactly hue 0: + // rgb(240,66,66) s.73 v.94, 96px of outline, and rgb(125,93,93) s.26 v.49, + // 96px of body. Both were outside 315..360, so not one of the sprite's 456 + // pixels could ever match and the AVOID ring never appeared at that spot at + // all. inH wraps the same way isBobber's 345..15 does; +5 is enough to cover + // hue 0 without letting the window drift towards the sand's orange. + const isHazard = (h, s, v) => inH(h, 315, 5) && s > 0.28 && v > 0.3; // Higher-tier catches unlocked by landing streaks (eel +2, squid +3, whale +5), // measured off the game's legend sprites. The squid's purple sits at 255-315, // just clear of the hazard window, which starts at 315. Only the whale's @@ -1482,8 +1497,25 @@ // The lane's own shadow edge reaches hue ~225 at s .6-.7, which a window // starting at 216 rang as a whale; the whale body is hue ~230 at s ~.4, so // both the hue floor and a saturation ceiling keep the shadow out. + // + // The squid's saturation floor came off that legend icon and was far too high + // for the sprite swimming in the lane. Read off the live canvas, the sprite is + // three flat tones and only ONE of them cleared s > .22: + // + // rgb(237,204,240) h295 s.150 v.94 138px body — the bulk of it + // rgb(147,89,161) h288 s.447 v.63 63px mid shading + // rgb(49,24,60) h282 s.600 v.24 77px outline (below the v floor) + // + // Keeping only the 63 shading pixels left them scattered as single dots + // through the body they outline, so nothing connected: no component reached + // even a 21px floor, let alone the 63px the count screen wanted, and the + // squid was never ringed once. At s > .12 the sprite comes back as a single + // 173px, 22x24 blob. The floor is set .03 below the body's exact .15 rather + // than snug against it because that .15 is a palette entry, and a sprite + // drawn at another scale blends its edges. A whole-canvas sweep at s > .10 + // turned up no new component anywhere, so there is room below to spend. 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.22 && v > 0.35; + 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; const SPECIES = [ { name: 'FISH', pts: 1, color: '#4ade80', test: isFish }, @@ -1756,9 +1788,27 @@ // A parabola needs its samples spread out to be worth fitting: six casts all // at half power pin the middle and let the ends fly anywhere, which is a // worse predictor than the seed they replaced. So the quadratic is only - // accepted with enough samples over a wide enough range of the gauge, and a - // straight line is fitted below that — still better than nothing, and it - // cannot bend the wrong way. + // accepted with enough samples over a wide enough range of the gauge. + // + // Below that the fit used to drop to a straight line, on the reasoning that a + // line is "still better than nothing and cannot bend the wrong way". That was + // true when the seed was itself a line, and became wrong the moment v6 made + // the seed a curve measured over 19 casts and two spots: the fallback was no + // longer replacing nothing, it was replacing the best number in the file. In + // practice it fired almost immediately — three casts is enough — and a live + // config caught in the act held aim2 = 0, aim1 = .8456 from six samples that + // spanned only p .25 to .625. Against the seed that line reads +2.2% of the + // lane at half power and -6.9% at full, so the further the target the more + // power it demands, and you have to release early to land anything. That is + // exactly the complaint v6 was supposed to have fixed. + // + // So the fallback keeps the curvature and fits only what the samples can + // honestly see: the slope and the offset. Both spots measured for v6 fell on + // the same curve, which makes SEED_C2 the game's law rather than one lane's + // quirk, while slope and offset absorb the things that do move — chiefly how + // wide findLane measured this particular lane. Two free parameters need far + // fewer samples than three, and the result cannot bend the wrong way either. + const SEED_C2 = 0.3095; // must move with cfg.aim2's default and migration function refitAim() { const S = cfg.samples; if (S.length < 3) return; @@ -1777,8 +1827,13 @@ if (sol) [c2, c1, c0] = sol; } if (!c2) { + // Curvature pinned, slope and offset least-squared over l - SEED_C2*p^2. + c2 = SEED_C2; let n = 0, sx = 0, sy = 0, sxx = 0, sxy = 0; - for (const [p, l] of S) { n++; sx += p; sy += l; sxx += p * p; sxy += p * l; } + for (const [p, l] of S) { + const y = l - c2 * p * p; + n++; sx += p; sy += y; sxx += p * p; sxy += p * y; + } const den = n * sxx - sx * sx; if (Math.abs(den) < 1e-6) return; c1 = (n * sxy - sx * sy) / den; @@ -1895,7 +1950,21 @@ let fish = [], haz = [], landed = null; if (S) { const toCss = o => ({ x: o.x / S.w * W, y: (S.sy + o.y) / S.cvH * H, w: o.w / S.w * W, h: o.h / S.cvH * H, n: o.n }); - const sx0 = Math.round(laneX0 / W * S.w), sx1 = Math.round(laneX1 / W * S.w); + // Search a little PAST both ends of the lane. A catch drifting to the end + // of the water keeps swimming until its centre is level with the last + // pixel of the bar, and half its sprite then hangs over the edge — but + // the search box used to stop exactly at laneX1, so the blob was sliced + // down the middle as it went. Watched live at Blunder Hills, a squid held + // 173px and a solid ring out to 96% of the lane, then fell to 84px and + // lost the ring at 99% purely to the slicing: the count floor was never + // the problem, the width screen was, because three surviving columns are + // not 1.2% of the canvas wide. Half a sprite is the margin that fixes it + // — that squid measured 22px against a 296px lane, so 5% is half a sprite + // with a little room. Nothing downstream gets looser: startX still throws + // out the sand mound off the left end, and onLane still throws out the + // surfboard rack off the right. + const pad = Math.round(laneW * 0.05 / W * S.w); + const sx0 = Math.round(laneX0 / W * S.w) - pad, sx1 = Math.round(laneX1 / W * S.w) + pad; const px = S.w / W; // native px per CSS px // The strip is tall enough that scenery pokes into it: the surfboard // rack at the spot's right edge matches both the hazard pink and the @@ -1937,7 +2006,15 @@ const p = invAim((f.x - laneX0) / laneW); drawLaneMark(f.x, f.y, f.color, `${f.name} +${f.pts}`, p !== null ? ((p * 100) | 0) + '%' : null); } - for (const z of haz) drawLaneMark(z.x, z.y, '#f87171', 'AVOID'); + // A hazard with a catch sitting on it is not a hazard. Land there and the + // catch is what you get — which is why the aim marker below already lets + // the catch colour outrank the hazard colour. Ringing it AVOID as well + // put a red ring and a species ring on the same pixel, arguing with each + // 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)) + drawLaneMark(z.x, z.y, '#f87171', 'AVOID'); } // ---- power meter ----