diff --git a/HANDOFF.md b/HANDOFF.md index 98d80bc..a081050 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -385,10 +385,31 @@ them missed would hand out free ammo. The soak asserts `gunId` is never set with `mx || mz || assist`, and finger-steering zeroes `mx`/`mz` — so a player sliding left and right under auto-fire still counted as standing still and never ran. `mgMoving` is in that expression now. -**The oval on his back was the mg racket.** `drawMgRacket` put its head at `(P.x, 0.95, P.z - 1.35)`, +**The mg racket is GONE.** It was a procedural racket drawn over a sprite that already holds one: +straight up-court it projected onto the player's back as a floating oval, and offset to the side it +just stuck out. Oliver: "can we please just hide it?" The pellets streaming up-court are the +feedback. `drawMgRacket` is deleted, not commented out. + +**(Previously) the oval on his back was the mg racket.** `drawMgRacket` put its head at `(P.x, 0.95, P.z - 1.35)`, which at this camera projects almost exactly onto the player sprite. Offset by `P.facing` it reads as a racket held out beside him. +**A third array had the empties-under-the-iterator bug.** A rocket blast can bank the last kill of a +game, and the beat that follows (a duel, a chapter boss) does `pellets.length = 0` — so the pellet +loop's next index read undefined and the frame threw. `shots`, `pellets`, `extras` and the body loop +all carry the same `if (!x) continue` guard now. **Any array iterated in `updateZombies` that can +contain something whose death advances the game needs it.** + +**A gradient that ends at alpha 0 hides the thing you are aligning.** The laser's far corners were +pinned correctly from the first attempt, but each wedge layer faded to fully transparent along its +length — so the beam became invisible before it reached the pinned edge, the VISIBLE end sat well +short of it, and moving the geometry appeared to do nothing twice in a row. The far stop is lit +(0.62 of the layer's alpha) now, so the visible edge and the pinned edge are the same line. + +**The beam falls off with distance** — brightest at the strings, thinning down the court, because +that is what a beam travelling away from you does. The far stop is 0.32 of the layer's alpha: enough +depth to read, never zero, so the visible end stays the pinned end. + **The laser's far edge is pinned in SCREEN space.** Running out to `beamLen` on the ground landed it on the ads' TOP line; it should die at their middle. `c3.y = c4.y = proj(0,0,-6).y - AD_H / 2` — the x's keep their perspective, the y is pinned. That holds in both orientations and leaves `beamLen` @@ -420,6 +441,12 @@ Drive the game deterministically via `window.T` with `javascript_tool`. The Brow context reports `document.visibilityState === 'hidden'`, so RAF is paused; halt it anyway with `window.requestAnimationFrame = () => 0` and step with `T.step(1/60)`. `T.press('Space')` = tap. Clear the intro first: `document.getElementById('intro')?.remove()`. +- **Two consecutive `render()` calls do NOT produce the same frame**, so "draw it, change one thing, + draw again, diff the pixels" silently measures nothing. `render()` mutates camera state as a side + effect (`aimCam += ((G.aim ? 1 : 0) - aimCam) * 0.15`), so the whole scene shifts slightly between + grabs and the diff lights up everywhere — it reported a laser beam extending 16 px above where its + own geometry could possibly reach. To check WHERE something lands, recompute the projection + directly (`T.proj(...)`) and compare numbers; keep pixel-diffing for "did anything at all change". - **The module-refresh list MUST include the VERSIONED entry point.** `index.html` loads `main.js?v=N`, and `fetch('main.js', {cache:'reload'})` does not bust that URL — so main.js alone stays stale through a reload while every other module is fresh. Read the src off the page: diff --git a/index.html b/index.html index 260ac12..eef9520 100644 --- a/index.html +++ b/index.html @@ -179,6 +179,6 @@ - + diff --git a/render.js b/render.js index 4e9d83f..a4958bb 100644 --- a/render.js +++ b/render.js @@ -619,13 +619,19 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, alpha, from) { const w0 = wAt(u0) * L.k, w1 = wAt(1) * L.k; const c1 = corner(u0, -w0), c2 = corner(u0, w0), c3 = corner(1, w1), c4 = corner(1, -w1); c3.y = c4.y = farY; // ...the x's keep their perspective, the y is pinned - // ...and it fades ALONG its length too, so the far end dissolves into the ads instead of - // stopping at a hard edge + // It must NOT fade to nothing along its length. The far corners are pinned to the ads' middle, + // but a gradient ending at alpha 0 made the beam INVISIBLE for its last stretch — so what you saw + // ended well short of where it was drawn, and no amount of moving the geometry changed it. The + // far end stays lit, so the visible edge and the pinned edge are the same line. const g = ctx.createLinearGradient((c1.x + c2.x) / 2, (c1.y + c2.y) / 2, (c3.x + c4.x) / 2, (c3.y + c4.y) / 2); + // DEPTH: brightest at the strings, thinning away down the court, because that is what a beam + // travelling away from you does. It must NOT reach zero though — a stop at alpha 0 made the beam + // invisible before its far edge, so the visible end sat short of the pinned one and moving the + // geometry appeared to change nothing at all. g.addColorStop(0, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},${L.a * alpha})`); - g.addColorStop(0.55, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},${L.a * 0.75 * alpha})`); - g.addColorStop(1, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},0)`); + g.addColorStop(0.45, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},${L.a * 0.58 * alpha})`); + g.addColorStop(1, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},${L.a * 0.32 * alpha})`); ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(c1.x, c1.y); ctx.lineTo(c2.x, c2.y); ctx.lineTo(c3.x, c3.y); ctx.lineTo(c4.x, c4.y); ctx.fill(); } @@ -809,23 +815,6 @@ function drawFallbackChar(ch, p, h) { // shown only until sprites load ctx.fillStyle = shorts; ctx.fillRect(x - 2.4 * u, y - 6 * u, 4.8 * u, 3 * u); ctx.fillStyle = skin; ctx.fillRect(x - 1.8 * u, y - 3 * u, 1.4 * u, 3 * u); ctx.fillRect(x + 0.4 * u, y - 3 * u, 1.4 * u, 3 * u); } -function drawMgRacket() { // horde: the player braces the racket forward and up-court, machine-gun style - // HELD OUT TO ONE SIDE. Straight up-court (P.x, P.z - 1.35) projects to almost exactly where the - // player sprite already is, so the racket head landed as an oval painted on his back — Oliver saw - // it every time. Offset by facing, it reads as a racket held out beside him. - const f = P.facing || 1; - const hip = proj(P.x + 0.34 * f, 1.15, P.z); - const tip = proj(P.x + 0.86 * f, 1.02, P.z - 1.25); // pointing up-court, where the pellets stream out - ctx.strokeStyle = '#d8d8d8'; ctx.lineWidth = 2; - ctx.beginPath(); ctx.moveTo(hip.x, hip.y); ctx.lineTo(tip.x, tip.y); ctx.stroke(); - ctx.strokeStyle = '#b07030'; ctx.lineWidth = 1.5; // the head, aimed forward - ctx.beginPath(); ctx.ellipse(tip.x, tip.y, Math.max(2, tip.s * 0.14), Math.max(3, tip.s * 0.22), 0, 0, 7); ctx.stroke(); - const cool = G.horde ? G.horde.mgT : G.mgCool; // horde or GUN-tin pickup — same racket - const rate = G.horde ? G.horde.mgRate || TUNE.mgRate : TUNE.mgRate; - if (cool > 1 / rate - 0.03) { // muzzle flash right after a shot - ctx.fillStyle = '#fff0a0'; ctx.beginPath(); ctx.arc(tip.x, tip.y, Math.max(2, tip.s * 0.12), 0, 7); ctx.fill(); - } -} // the courtside weapon rack along the bottom: PLAIN + the four tins. Tap a tile (or 1–5) to // pick which one your next serve throws; empty tins dim out. Geometry is shared with input @@ -1260,7 +1249,10 @@ export function render() { ctx.beginPath(); ctx.ellipse(p.x, p.y, p.s * 0.62 * k, p.s * 0.22 * k, 0, 0, 7); ctx.stroke(); } if (G.mode !== 'eaten') drawChar(P); // ...the dying man is drawn with the crowd instead (drawDying) - if ((G.horde || G.mgT > 0) && G.mode === 'splatter') drawMgRacket(); // rambo racket over the player + // The braced "rambo racket" is NOT drawn any more. It was a procedural racket laid over a sprite + // that already holds one: straight up-court it projected onto his back as a floating oval, and + // offset to the side it just stuck out. The pellets streaming up-court are the feedback; the man + // holding his own racket is the picture. ctx.setTransform(RES, 0, 0, RES, 0, 0); // un-shake: the HUD and overlays stay rock steady // bomb bloom: a warm blood-orange wash, not a white-out. At 0.85 white it blinded you on a phone diff --git a/zombies.js b/zombies.js index 5948c1a..283ab04 100644 --- a/zombies.js +++ b/zombies.js @@ -961,6 +961,10 @@ export function updateZombies(dt) { for (let i = pellets.length - 1; i >= 0; i--) { // gun rounds flat and fast; lobbed ones ride an arc const pe = pellets[i]; + // A rocket blast can bank the last kill of a game, and the beat that follows (a duel, a chapter + // boss) empties this array — so every remaining index reads undefined and the frame throws. Same + // guard, and the same reason, as the one in the shots loop and the body loop. + if (!pe) continue; pe.t -= dt; pe.x += pe.vx * dt; pe.z += pe.vz * dt; let hit = false, landed = false; if (pe.arc) { // a thrown round: it falls, and landing is a hit @@ -1062,6 +1066,7 @@ export function updateZombies(dt) { for (let i = extras.length - 1; i >= 0; i--) { // triple-serve side balls: full ballistic clones const e = extras[i]; + if (!e) continue; // same as pellets/shots: a kill here can start a beat that empties the array e.t -= dt; e.vy -= (e.g || TUNE.gravity) * dt; // e.g: a clone flies under the SAME topspin as the ball it came from e.x += e.vx * dt; e.y += e.vy * dt; e.z += e.vz * dt; if (e.y <= 0 && e.vy < 0) { e.y = 0; e.vy = -e.vy * TUNE.bounceY; e.vx *= TUNE.bounceXZ; e.vz *= TUNE.bounceXZ; }