diff --git a/README.md b/README.md index 843bb64..e1020cf 100644 --- a/README.md +++ b/README.md @@ -128,14 +128,40 @@ The arc starts noticeably **above your head** — that's correct, not a bug. The ### Calibration -The shot is described by three numbers, all fractions of canvas size: the curvature of the arc, and where it crosses platform height going up and coming down. They are seeded from five measured shots, so the arc draws immediately — the status shows `(default)` until your own first shot replaces them outright (not blended, since the default isn't your game). Later shots refine by averaging, and fits outside the measured spread are rejected rather than averaged in. +The shot is described by three numbers, all fractions of canvas size: the curvature of the arc, and where it crosses platform height going up and coming down. They are seeded from measured shots, so the arc draws immediately — the status shows `(default)` until your own first shot replaces them outright (not blended, since the default isn't your game). Later shots refine by averaging, and fits outside the measured spread are rejected rather than averaged in. Each shot contributes **once**, from the median of the fits taken across its whole flight, committed when the ball is gone. An earlier build folded in every frame of every flight; at a 0.25 weight applied thirty-odd times in a row that is not an average, so each shot ended up sitting on its own last fit no matter what came before it. -That made it possible to read the **per-shot spread** off a recording taken while resetting between shots: `arc` ranged 1.71–3.01 and `range` 48–63%. That is a ±30% swing on curvature from shots taken the same way, which is far wider than the ±3% the seeded defaults were measured at. Either the shot genuinely varies or the single-shot fit is noisy; until that is settled, treat the preview as an aim guide rather than a guarantee. Averaging across shots is what the current weighting is for — **resetting between shots defeats it**. +That made it possible to read the **per-shot spread** off a recording, and it was alarming: `arc` ranged 1.71–3.01, a ±30% swing on curvature from shots taken the same way. It was an open question whether the shot genuinely varied or the single-shot fit was noisy. + +It is the fit. Reading 15 flights out of the running game over the DevTools protocol and fitting `x(t)` and `y(t)` separately — which needs no release instant, and cannot degenerate the way fitting `y` as a function of `x` does — gives a horizontal release speed of **536–541 px/s across every well-tracked flight**. The shot is the same shot to half a percent, every time. Nothing about it varies. + +The spread was two things, and neither is the shot: + +- **Short tracks.** `fitXY` returns a curve from only 40px of horizontal spread, and curvature error goes as 1/spread², so a fit over a short arc is a guess wearing a number. The three wildest calibrations in the sample — 2.941, 1.865, 2.716 against a true 2.23 — were the three shortest tracks, spans of 200, 143 and 140px. +- **Backboard and rim-lip bounces**, both of which are ordinary ways to score. They send the ball back over `x` it has already crossed, and since the curve is fitted as `y` of `x`, that is not a hard fit but an impossible one — two `y` for one `x`. This is where `arc` reached 55. + +Learning now waits for a track spanning 20% of the canvas and stops at the first bounce, which takes the spread of the committed curvature from **47% to 4%**. The 40px floor stays inside `fitXY` so the live arc still draws early in a flight; it is only *learning* that waits. Averaging across shots is what the weighting is for — **resetting between shots defeats it**. Predicting each measured shot from only the *other* shots (leave-one-out) lands within 48px, mean error 3px. +**The anchor is the biggest error left, and it is not yet understood.** `arc`, `range` and the upward crossing are meant to describe *the shot*, so anchoring them to the platform should make them invariant to where the platform is. Measured across two independent runs read off the live game, they are not: + +| | corr(platY, upward crossing) | corr(platY, range) | +|---|---|---| +| 8 flights | −0.79 | +0.71 | +| 5 flights | −0.86 | +0.77 | + +Curvature barely moves, which is the tell — it is set by `g / 2·vx²`, and neither of those cares how high the platform is, while the other two are where the arc meets platform *height*. + +It looks like the lever, and it is not. Modelling the upward crossing as linear in platform height predicts it 43% better out of sample (leave-one-out mean error 51.6px → 29.6px), and `range` barely moves. But the number that decides a make is the height of the arc where it passes the **rim**, and there the same comparison is 54.7px → 53.0px: three percent. The errors in the three parameters are correlated and largely cancel by the time the curve reaches the rim. Measured and left unshipped on that basis — an unexplained empirical correction fitted on 11 flights from one player has to earn more than 3%. + +So the ~55px of arc height at the rim is the real accuracy ceiling today, and it is per-shot noise in the two crossings rather than anything to do with the anchor. Averaging across shots is what removes it, which is what the commit weighting is for. Three explanations have been measured and none survived — a stale anchor (a Δt sweep over −200…+400ms shows no minimum), the detector picking a different row as the platform moves (width was 177px in 2565 of 2566 frames), and the ball inheriting the platform's velocity (correlates worse than height, though the two are confounded on an oscillating platform). What remains is that the shot may not be fixed relative to the platform at all. Settling it needs the release instant, which nothing currently measures. + +Two further things are known but *not* the problem. The anchor's `y` snaps to a 5.5px grid, because `findPlatform` runs on the downscaled readback — that contributes about 4% of the observed spread, so it is not worth chasing. And the platform has never once been observed to move horizontally, so the horizontal half of the anchor is entirely untested; it first matters when the platform starts looping later in a run. + +Of the three numbers, `range` is the trustworthy one — it is the part of the arc the tracked points actually cover, and two independently measured seeds agree on it to 2%. The upward crossing is the weakest in any seed: it sits behind the point tracking begins, so no shot ever observes it directly and every estimate of it is an extrapolation. Its spread across shots is a quarter of its own value. + Calibration is stored in `localStorage` (`hoops_cfg`) relative to canvas size, so it survives resizing the window. **Reset calibration** returns to the measured default. Calibration learned by an older build is discarded automatically via a version stamp — early builds tracked the wrong sprites and learned wrong numbers, and a wrong throw is worse than none. ### Controls diff --git a/idleon-hoops.user.js b/idleon-hoops.user.js index c937854..6da9cdb 100644 --- a/idleon-hoops.user.js +++ b/idleon-hoops.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name IdleOn Hoops Helper // @namespace nativerobot -// @version 1.8 +// @version 1.9 // @downloadURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-hoops.user.js // @updateURL https://raw.githubusercontent.com/averagenative/idleon-userscripts/main/idleon-hoops.user.js // @description Dotted-line shot preview + live ball arc for the Swishy Hoops minigame in Legends of IdleOn @@ -54,29 +54,105 @@ debug: false, // outline every detected blob // Calibration is stored as fractions of canvas size so it survives resizing // the window — the game scales its physics with the viewport. - calVer: 5, // bump to throw away calibration learned by an older build + calVer: 6, // bump to throw away calibration learned by an older build // The shot is a fixed parabola anchored to the PLATFORM, not to the ball in // your hands. Written as y = platY + A*(u - uL)*(u - R) where u is distance // right of the platform centre: A is curvature, uL and R are where the path - // crosses platform height going up and coming down. - // Measured across three recordings and five shots; leave-one-out prediction - // of the landing point is within 48px. Anchoring to the held ball instead - // was ~100px out, because that anchor goes stale while the character jumps - // and the platform keeps moving under them. - shotA: 2.103, // curvature x canvas width - shotL: -0.179, // upward crossing, fraction of width left of the platform - shotR: 0.557, // landing range, fraction of width right of the platform + // crosses platform height going up and coming down. Anchoring to the held + // ball instead was ~100px out, because that anchor goes stale while the + // character jumps and the platform keeps moving under them. + // + // "Fixed" is now measured rather than assumed. The README used to record an + // open question -- per-shot arc ranging 1.71-3.01, "either the shot + // genuinely varies or the single-shot fit is noisy". It is the fit. Fitting + // x(t) and y(t) separately across 15 live flights (which needs no release + // instant, and cannot degenerate the way y-as-a-function-of-x does) gives a + // release velocity of 536-541 px/s horizontally across every well-tracked + // flight -- the same shot to half a percent. Nothing about it varies. + // + // These three are the medians of 8 flights that passed the span and bounce + // screens, read live off the running game rather than off a recording: + // + // shotA 2.233 sd .034 range 2.195..2.288 old seed 2.103, 6% low + // shotL -0.119 sd .030 range -.158..-.083 old seed -0.179, 33% off + // shotR 0.547 sd .031 range .510.. .588 old seed 0.557, agrees + // + // shotR is the one the old five-shot seed already had right, and it is also + // the one the tracked points actually cover. shotL is the weakest of the + // three in both seeds: it is where the arc crossed platform height on the + // way UP, which is behind the point tracking starts, so no shot ever + // observes it directly and every estimate of it is an extrapolation. Its + // own spread across shots is a quarter of its value. Treat a disagreement + // there as unsettled rather than as this seed being right. + // + // KNOWN, UNEXPLAINED, and the biggest error left. These two are supposed to + // describe the SHOT, so anchoring to the platform should make them + // invariant to where the platform happens to be. They are not. Across two + // independent runs read off the live game: + // + // corr(platY, shotL) corr(platY, shotR) + // 8 flights -0.79 +0.71 + // 5 flights -0.86 +0.77 + // + // Curvature barely moves, which is the tell: shotA is set by g/2vx^2 and + // neither of those cares how high the platform is, while shotL and shotR + // are where the arc meets platform HEIGHT. + // + // It looks like the lever and it is not. Modelling shotL as linear in + // platform height predicts it 43% better out of sample -- leave-one-out + // mean error 51.6px falls to 29.6px -- and shotR barely moves, 40.8 to + // 37.7px. But the number that decides a make is the height of the arc where + // it passes the RIM, and there the same comparison is: + // + // constant, as shipped mean 54.7px worst 117.5px + // linear in platform height mean 53.0px worst 113.9px + // + // Three percent. The errors in A, L and R are correlated and largely cancel + // by the time the curve reaches the rim, so a correction that plainly + // improves two of the three parameters buys almost nothing where it counts. + // Measured, not argued, and left unshipped on that basis: an unexplained + // empirical correction fitted on 11 flights from one player has to earn + // more than 3% before it goes in. + // + // The ~55px of arc height at the rim is therefore the real accuracy ceiling + // today, and it is per-shot noise in L and R rather than anything to do + // with the anchor. Averaging across shots is what actually removes it, + // which is what the commit weighting is for. + // + // Three explanations have been measured and none survived: + // - Stale anchor. flightPlat is sampled a frame or two after release and + // the platform is moving (26 of 32 releases, ~135px/s, biased upward). + // But recomputing L and R against the platform at release+dt over + // -200..+400ms gives no minimum -- the spread falls monotonically and + // is still falling at -200ms, which is before the ball left. + // - The detector picking a different row of the platform as it moves. + // The detected width was 177px in 2565 of 2566 logged frames. + // - The ball inheriting the platform's velocity. platV correlates worse + // than platY (-0.68 vs -0.86 for L), though on an oscillating platform + // the two are confounded and 5 flights cannot separate them. + // + // What is left is that the shot may simply not be fixed relative to the + // platform -- if the character's jump reaches a height that is not purely + // platform-relative, the arc meets platform height further out when the + // platform sits lower, which is the observed sign. Settling it needs the + // release instant, which nothing currently measures. + shotA: 2.233, // curvature x canvas width + shotL: -0.119, // upward crossing, fraction of width left of the platform + shotR: 0.547, // landing range, fraction of width right of the platform calSeeded: true, 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) || '{}')); - // Calibration from an older build was learned per frame rather than per shot, - // so whatever is stored is one arbitrary mid-flight fit — wrong rather than - // merely stale. Drop it outright. - if (cfg.calVer !== 5) { - cfg.calVer = 5; cfg.calSeeded = true; - cfg.shotA = 2.103; cfg.shotL = -0.179; cfg.shotR = 0.557; + // Calibration learned before calVer 6 banked a fit from every frame of every + // flight that produced a plausible-looking parabola, including flights barely + // tracked at all and flights that came off the backboard. Measured over 15 + // live flights the committed curvature ranged 1.865-2.941 around a true + // 2.23 — a live config caught mid-session held 2.486. That is not stale, it + // is contaminated, and averaging more shots into it does not wash it out. + if (cfg.calVer !== 6) { + cfg.calVer = 6; cfg.calSeeded = true; + cfg.shotA = 2.233; cfg.shotL = -0.119; cfg.shotR = 0.547; } delete cfg.grav; delete cfg.launch; delete cfg.launchN; delete cfg.gravN; const save = () => localStorage.setItem(KEY, JSON.stringify(cfg)); @@ -806,6 +882,19 @@ const dir = Math.sign(pN.x - p0.x) || 1; made = drawCurve(x => f.a * x * x + f.b * x + f.c, fly.x, dir, W, H, 'live'); + // Has this shot come off the backboard or the far lip of the rim? Both + // are ordinary ways to score, and both send the ball back over x it has + // already crossed. A projectile's x is monotonic, so any retreat from + // the furthest point reached is a bounce and nothing else. The margin + // is a whole 1% of the width because the tracker's own x jitter measured + // 3-7px rms, and one noisy frame must not read as a bounce. + if (!fly.dir0 && Math.abs(pN.x - p0.x) > 4) fly.dir0 = Math.sign(pN.x - p0.x); + if (fly.dir0) { + const reach = pN.x * fly.dir0; + if (fly.reach === undefined || reach > fly.reach) fly.reach = reach; + else if (fly.reach - reach > W * 0.01) fly.bounced = true; + } + // Learn the shot in platform-relative terms: curvature, plus where the // path crosses platform height going up and coming down. Those three // are the same for every shot regardless of when tracking began. @@ -817,11 +906,45 @@ const uL = (Math.min(r1, r2) - flightPlat.x) * dir; const uR = (Math.max(r1, r2) - flightPlat.x) * dir; const An = A * W, Ln = uL / W, Rn = uR / W; - // Measured spread across recordings: A*W 2.05-2.18, R/W .54-.59. - // Anything outside this is a corrupted fit, not a different shot. - // Banked, not applied: the shot is only folded in once it is over - // and the whole flight can be reduced to one median fit. - if (An > 1.5 && An < 3.2 && Rn > 0.40 && Rn < 0.75 && Ln > -0.45 && Ln < 0.05) + const xs = fly.pts.map(q => q.x); + const span = Math.max(...xs) - Math.min(...xs); + // Two screens on top of the plausibility window, both measured off + // 15 live flights read out of the running game over the DevTools + // protocol. The window alone is not enough: it asks whether the + // fitted parabola looks sane, never whether the points under it + // were a single projectile, and the worst offenders sail through. + // + // SPAN. fitXY needs only 40px of x to return a curve, and it will, + // but curvature error goes as 1/spread^2, so a fit over a short arc + // is a guess wearing a number. The three wildest calibrations in + // the sample -- A of 2.941, 1.865, 2.716 against a true 2.23 -- came + // from the three shortest tracks, spans of 200, 143 and 140px. 40px + // stays as fitXY's floor because the live arc should still draw + // early in a flight; it is only LEARNING that waits for real spread. + // + // BOUNCE. y is fitted as a function of x, so a ball returning over + // its own x is not a hard fit, it is an impossible one -- two y for + // one x. That is where A reached 55. + // + // Sweeping the span gate over those flights (commits kept, spread + // of the committed A): + // + // none (shipped) 15 commits 47.4% 1.865-2.941 + // 0.10 W 11 commits 19.3% + // 0.15 W 10 commits 8.2% + bounce cut 4.3% + // 0.20 W 10 commits 7.7% + bounce cut 4.2% + // 0.25 W 10 commits 7.2% + bounce cut 4.0% + // 0.30 W 10 commits 5.1% + // 0.35 W 7 commits 2.0% + // 0.40 W 2 commits 0.9% + // + // Commits hold flat from .15 to .30 and fall off a cliff above it, + // so .20 sits in the middle of the plateau rather than on an edge: + // tightening or loosening it by a quarter changes nothing much. + // Together they take the spread from 47.4% to 4.2% for two commits + // out of ten -- and a commit is cheap, the calibration averages. + if (!fly.bounced && span >= W * 0.20 && + An > 1.5 && An < 3.2 && Rn > 0.40 && Rn < 0.75 && Ln > -0.45 && Ln < 0.05) calSamples.push({ A: An, L: Ln, R: Rn }); } } @@ -892,7 +1015,7 @@ const toggle = () => { cfg.on = !cfg.on; if (!cfg.on) tracks = []; save(); sync(); }; tap(runBtn, toggle); - tap($('#cal'), () => { cfg.shotA = 2.103; cfg.shotL = -0.179; cfg.shotR = 0.557; cfg.calSeeded = true; calSamples = []; save(); }); + tap($('#cal'), () => { cfg.shotA = 2.233; cfg.shotL = -0.119; cfg.shotR = 0.547; cfg.calSeeded = true; calSamples = []; save(); }); $('#span').onchange = e => { cfg.span = Math.max(200, +e.target.value); save(); }; $('#hue').onchange = e => { cfg.hue = (+e.target.value % 360 + 360) % 360; save(); }; $('#huew').onchange = e => { cfg.hueW = Math.min(90, Math.max(1, +e.target.value)); save(); }; diff --git a/idleon-suite.user.js b/idleon-suite.user.js index f0c2fcc..a7a2ba2 100644 --- a/idleon-suite.user.js +++ b/idleon-suite.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name IdleOn Helper Suite // @namespace nativerobot -// @version 1.6 +// @version 1.9 // @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 @@ -585,26 +585,102 @@ debug: false, // outline every detected blob // Calibration is stored as fractions of canvas size so it survives resizing // the window — the game scales its physics with the viewport. - calVer: 5, // bump to throw away calibration learned by an older build + calVer: 6, // bump to throw away calibration learned by an older build // The shot is a fixed parabola anchored to the PLATFORM, not to the ball in // your hands. Written as y = platY + A*(u - uL)*(u - R) where u is distance // right of the platform centre: A is curvature, uL and R are where the path - // crosses platform height going up and coming down. - // Measured across three recordings and five shots; leave-one-out prediction - // of the landing point is within 48px. Anchoring to the held ball instead - // was ~100px out, because that anchor goes stale while the character jumps - // and the platform keeps moving under them. - shotA: 2.103, // curvature x canvas width - shotL: -0.179, // upward crossing, fraction of width left of the platform - shotR: 0.557, // landing range, fraction of width right of the platform + // crosses platform height going up and coming down. Anchoring to the held + // ball instead was ~100px out, because that anchor goes stale while the + // character jumps and the platform keeps moving under them. + // + // "Fixed" is now measured rather than assumed. The README used to record an + // open question -- per-shot arc ranging 1.71-3.01, "either the shot + // genuinely varies or the single-shot fit is noisy". It is the fit. Fitting + // x(t) and y(t) separately across 15 live flights (which needs no release + // instant, and cannot degenerate the way y-as-a-function-of-x does) gives a + // release velocity of 536-541 px/s horizontally across every well-tracked + // flight -- the same shot to half a percent. Nothing about it varies. + // + // These three are the medians of 8 flights that passed the span and bounce + // screens, read live off the running game rather than off a recording: + // + // shotA 2.233 sd .034 range 2.195..2.288 old seed 2.103, 6% low + // shotL -0.119 sd .030 range -.158..-.083 old seed -0.179, 33% off + // shotR 0.547 sd .031 range .510.. .588 old seed 0.557, agrees + // + // shotR is the one the old five-shot seed already had right, and it is also + // the one the tracked points actually cover. shotL is the weakest of the + // three in both seeds: it is where the arc crossed platform height on the + // way UP, which is behind the point tracking starts, so no shot ever + // observes it directly and every estimate of it is an extrapolation. Its + // own spread across shots is a quarter of its value. Treat a disagreement + // there as unsettled rather than as this seed being right. + // + // KNOWN, UNEXPLAINED, and the biggest error left. These two are supposed to + // describe the SHOT, so anchoring to the platform should make them + // invariant to where the platform happens to be. They are not. Across two + // independent runs read off the live game: + // + // corr(platY, shotL) corr(platY, shotR) + // 8 flights -0.79 +0.71 + // 5 flights -0.86 +0.77 + // + // Curvature barely moves, which is the tell: shotA is set by g/2vx^2 and + // neither of those cares how high the platform is, while shotL and shotR + // are where the arc meets platform HEIGHT. + // + // It looks like the lever and it is not. Modelling shotL as linear in + // platform height predicts it 43% better out of sample -- leave-one-out + // mean error 51.6px falls to 29.6px -- and shotR barely moves, 40.8 to + // 37.7px. But the number that decides a make is the height of the arc where + // it passes the RIM, and there the same comparison is: + // + // constant, as shipped mean 54.7px worst 117.5px + // linear in platform height mean 53.0px worst 113.9px + // + // Three percent. The errors in A, L and R are correlated and largely cancel + // by the time the curve reaches the rim, so a correction that plainly + // improves two of the three parameters buys almost nothing where it counts. + // Measured, not argued, and left unshipped on that basis: an unexplained + // empirical correction fitted on 11 flights from one player has to earn + // more than 3% before it goes in. + // + // The ~55px of arc height at the rim is therefore the real accuracy ceiling + // today, and it is per-shot noise in L and R rather than anything to do + // with the anchor. Averaging across shots is what actually removes it, + // which is what the commit weighting is for. + // + // Three explanations have been measured and none survived: + // - Stale anchor. flightPlat is sampled a frame or two after release and + // the platform is moving (26 of 32 releases, ~135px/s, biased upward). + // But recomputing L and R against the platform at release+dt over + // -200..+400ms gives no minimum -- the spread falls monotonically and + // is still falling at -200ms, which is before the ball left. + // - The detector picking a different row of the platform as it moves. + // The detected width was 177px in 2565 of 2566 logged frames. + // - The ball inheriting the platform's velocity. platV correlates worse + // than platY (-0.68 vs -0.86 for L), though on an oscillating platform + // the two are confounded and 5 flights cannot separate them. + // + // What is left is that the shot may simply not be fixed relative to the + // platform -- if the character's jump reaches a height that is not purely + // platform-relative, the arc meets platform height further out when the + // platform sits lower, which is the observed sign. Settling it needs the + // release instant, which nothing currently measures. + shotA: 2.233, // curvature x canvas width + shotL: -0.119, // upward crossing, fraction of width left of the platform + shotR: 0.547, // landing range, fraction of width right of the platform calSeeded: true, }, cfg => { - // Calibration from an older build was learned per frame rather than per shot, - // so whatever is stored is one arbitrary mid-flight fit — wrong rather than - // merely stale. Drop it outright. - if (cfg.calVer !== 5) { - cfg.calVer = 5; cfg.calSeeded = true; - cfg.shotA = 2.103; cfg.shotL = -0.179; cfg.shotR = 0.557; + // Calibration learned before calVer 6 banked a fit from every frame of every + // flight that produced a plausible-looking parabola, including flights barely + // tracked at all and flights that came off the backboard. Measured over 15 + // live flights the committed curvature ranged 1.865-2.941 around a true + // 2.23 — a live config caught mid-session held 2.486. That is not stale, it + // is contaminated, and averaging more shots into it does not wash it out. + if (cfg.calVer !== 6) { + cfg.calVer = 6; cfg.calSeeded = true; + cfg.shotA = 2.233; cfg.shotL = -0.119; cfg.shotR = 0.547; } delete cfg.grav; delete cfg.launch; delete cfg.launchN; delete cfg.gravN; }); @@ -1231,6 +1307,19 @@ const dir = Math.sign(pN.x - p0.x) || 1; made = drawCurve(x => f.a * x * x + f.b * x + f.c, fly.x, dir, W, H, 'live'); + // Has this shot come off the backboard or the far lip of the rim? Both + // are ordinary ways to score, and both send the ball back over x it has + // already crossed. A projectile's x is monotonic, so any retreat from + // the furthest point reached is a bounce and nothing else. The margin + // is a whole 1% of the width because the tracker's own x jitter measured + // 3-7px rms, and one noisy frame must not read as a bounce. + if (!fly.dir0 && Math.abs(pN.x - p0.x) > 4) fly.dir0 = Math.sign(pN.x - p0.x); + if (fly.dir0) { + const reach = pN.x * fly.dir0; + if (fly.reach === undefined || reach > fly.reach) fly.reach = reach; + else if (fly.reach - reach > W * 0.01) fly.bounced = true; + } + // Learn the shot in platform-relative terms: curvature, plus where the // path crosses platform height going up and coming down. Those three // are the same for every shot regardless of when tracking began. @@ -1242,11 +1331,45 @@ const uL = (Math.min(r1, r2) - flightPlat.x) * dir; const uR = (Math.max(r1, r2) - flightPlat.x) * dir; const An = A * W, Ln = uL / W, Rn = uR / W; - // Measured spread across recordings: A*W 2.05-2.18, R/W .54-.59. - // Anything outside this is a corrupted fit, not a different shot. - // Banked, not applied: the shot is only folded in once it is over - // and the whole flight can be reduced to one median fit. - if (An > 1.5 && An < 3.2 && Rn > 0.40 && Rn < 0.75 && Ln > -0.45 && Ln < 0.05) + const xs = fly.pts.map(q => q.x); + const span = Math.max(...xs) - Math.min(...xs); + // Two screens on top of the plausibility window, both measured off + // 15 live flights read out of the running game over the DevTools + // protocol. The window alone is not enough: it asks whether the + // fitted parabola looks sane, never whether the points under it + // were a single projectile, and the worst offenders sail through. + // + // SPAN. fitXY needs only 40px of x to return a curve, and it will, + // but curvature error goes as 1/spread^2, so a fit over a short arc + // is a guess wearing a number. The three wildest calibrations in + // the sample -- A of 2.941, 1.865, 2.716 against a true 2.23 -- came + // from the three shortest tracks, spans of 200, 143 and 140px. 40px + // stays as fitXY's floor because the live arc should still draw + // early in a flight; it is only LEARNING that waits for real spread. + // + // BOUNCE. y is fitted as a function of x, so a ball returning over + // its own x is not a hard fit, it is an impossible one -- two y for + // one x. That is where A reached 55. + // + // Sweeping the span gate over those flights (commits kept, spread + // of the committed A): + // + // none (shipped) 15 commits 47.4% 1.865-2.941 + // 0.10 W 11 commits 19.3% + // 0.15 W 10 commits 8.2% + bounce cut 4.3% + // 0.20 W 10 commits 7.7% + bounce cut 4.2% + // 0.25 W 10 commits 7.2% + bounce cut 4.0% + // 0.30 W 10 commits 5.1% + // 0.35 W 7 commits 2.0% + // 0.40 W 2 commits 0.9% + // + // Commits hold flat from .15 to .30 and fall off a cliff above it, + // so .20 sits in the middle of the plateau rather than on an edge: + // tightening or loosening it by a quarter changes nothing much. + // Together they take the spread from 47.4% to 4.2% for two commits + // out of ten -- and a commit is cheap, the calibration averages. + if (!fly.bounced && span >= W * 0.20 && + An > 1.5 && An < 3.2 && Rn > 0.40 && Rn < 0.75 && Ln > -0.45 && Ln < 0.05) calSamples.push({ A: An, L: Ln, R: Rn }); } } @@ -1316,7 +1439,7 @@ const toggle = () => { cfg.on = !cfg.on; if (!cfg.on) tracks = []; save(); sync(); }; tap(runBtn, toggle); - tap($('#cal'), () => { cfg.shotA = 2.103; cfg.shotL = -0.179; cfg.shotR = 0.557; cfg.calSeeded = true; calSamples = []; save(); }); + tap($('#cal'), () => { cfg.shotA = 2.233; cfg.shotL = -0.119; cfg.shotR = 0.547; cfg.calSeeded = true; calSamples = []; save(); }); $('#span').onchange = e => { cfg.span = Math.max(200, +e.target.value); save(); }; $('#hue').onchange = e => { cfg.hue = (+e.target.value % 360 + 360) % 360; save(); }; $('#huew').onchange = e => { cfg.hueW = Math.min(90, Math.max(1, +e.target.value)); save(); };