Hide the racket, and light the beam all the way to its edge
The mg racket is gone, not moved. 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. The laser's far edge was pinned to the middle of the ad strip two commits ago and nothing appeared to change, twice. The geometry was always right -- each wedge layer faded to alpha 0 along its length, so the beam went invisible before it reached its own far corners and the VISIBLE end sat well short of the pinned one. The far stop is lit now, so the two are the same line. Verified by recomputing the projection: far corners at y 113.8, the ads' middle, against 110.2 unpinned. It also falls off with distance now, brightest at the strings and thinning down the court -- but never to zero, which is the trap above. Fixes a third instance of the empties-under-the-iterator bug: a rocket blast can bank the last kill of a game, and the duel or chapter boss that follows clears pellets[] mid-loop. shots, pellets, extras and the body loop all carry the guard. Also records that two consecutive render() calls do not produce the same frame -- render mutates camera smoothing as a side effect -- so frame-diffing to find where something was drawn measures the camera drift instead. It claimed the beam reached 16 px above where its own geometry could go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e88fd5bb9
commit
da9cca2f23
4 files changed
+48
-24
No files matched your search
+28
-1
@@ -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
|
`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.
|
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
|
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 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
|
**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
|
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`
|
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
|
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.
|
`window.requestAnimationFrame = () => 0` and step with `T.step(1/60)`. `T.press('Space')` = tap.
|
||||||
Clear the intro first: `document.getElementById('intro')?.remove()`.
|
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
|
- **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
|
`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:
|
stays stale through a reload while every other module is fresh. Read the src off the page:
|
||||||
|
|||||||
+1
-1
@@ -179,6 +179,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js?v=102"></script>
|
<script type="module" src="main.js?v=103"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -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 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);
|
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
|
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
|
// It must NOT fade to nothing along its length. The far corners are pinned to the ads' middle,
|
||||||
// stopping at a hard edge
|
// 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,
|
const g = ctx.createLinearGradient((c1.x + c2.x) / 2, (c1.y + c2.y) / 2,
|
||||||
(c3.x + c4.x) / 2, (c3.y + c4.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, `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(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]},0)`);
|
g.addColorStop(1, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},${L.a * 0.32 * alpha})`);
|
||||||
ctx.fillStyle = g;
|
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();
|
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 = 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);
|
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
|
// 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
|
// 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();
|
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.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
|
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
|
// bomb bloom: a warm blood-orange wash, not a white-out. At 0.85 white it blinded you on a phone
|
||||||
|
|||||||
@@ -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
|
for (let i = pellets.length - 1; i >= 0; i--) { // gun rounds flat and fast; lobbed ones ride an arc
|
||||||
const pe = pellets[i];
|
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;
|
pe.t -= dt; pe.x += pe.vx * dt; pe.z += pe.vz * dt;
|
||||||
let hit = false, landed = false;
|
let hit = false, landed = false;
|
||||||
if (pe.arc) { // a thrown round: it falls, and landing is a hit
|
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
|
for (let i = extras.length - 1; i >= 0; i--) { // triple-serve side balls: full ballistic clones
|
||||||
const e = extras[i];
|
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.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;
|
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; }
|
if (e.y <= 0 && e.vy < 0) { e.y = 0; e.vy = -e.vy * TUNE.bounceY; e.vx *= TUNE.bounceXZ; e.vz *= TUNE.bounceXZ; }
|
||||||
|
|||||||
Reference in new issue
Block a user