From b504cb3565885f85d3dfef886c9ad418b17c0321 Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Thu, 6 Aug 2026 09:58:11 +0200 Subject: [PATCH] Three verdicts from the phone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PERFECT spark ring is out — the hitstop, the click and the praise word already carry the moment. The net-cord wobble drops to ~1 px. And the floodlight stutter is gone entirely: on the iPhone a random dark dip over the stands reads as the crowd flickering black — a rendering bug, not ambience. Co-Authored-By: Claude Fable 5 --- gameplay.js | 10 +++------- render.js | 34 ++++++---------------------------- state.js | 1 - 3 files changed, 9 insertions(+), 36 deletions(-) diff --git a/gameplay.js b/gameplay.js index 4910b9c..606d4d1 100644 --- a/gameplay.js +++ b/gameplay.js @@ -168,8 +168,7 @@ function doHit(ch, quality, aim) { G.aiReactT = 0; // CPU hasn't read the shot yet } sfx(ch.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded racket strike, per side - if (quality > 0.999) { G.hitstop = 0.05; praise(); pock(0.5, 1600); // the rally hit: a real 45 ms window (perfectMs) - if (ch.side === 'P') G.spark = { x: ball.x, y: ball.y, z: ball.z, t: 0.3 }; } // ...and the contact flashes a ring + if (quality > 0.999) { G.hitstop = 0.05; praise(); pock(0.5, 1600); } // the rally hit: a real 45 ms window (perfectMs) ch.pose = 'swing'; ch.poseT = 0; } @@ -331,8 +330,7 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) { if (sv.side === 'P' && G.mode === 'tennis') G.outMarker = { x: boxX, z: tzs, out: !inServeBox(boxX, tzs), t: 1.3 }; G.phase = 'rally'; sfx(sv.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded serve strike, per side sv.pose = 'serve'; sv.poseT = 0; - if (perfect) { praise(); G.hitstop = 0.05; - if (sv.side === 'P') G.spark = { x: ball.x, y: ball.y, z: ball.z, t: 0.3 }; } // the PERFECT ring, at the strike + if (perfect) { praise(); G.hitstop = 0.05; } } // ---------------------------------------------------------------- scoring ---- @@ -1314,8 +1312,7 @@ export function update(dt) { if (was < EATEN_SUMMARY && G.eatenT >= EATEN_SUMMARY) { zombieDuck(true); menuDuck(false); } crowd.cheer = Math.max(0, crowd.cheer - dt); // the stand sits back down over the death too — // a cheer live at the kill used to clap forever - if (G.spark && (G.spark.t -= dt) <= 0) G.spark = null; // one-shot effects finish - if (G.netRipple && (G.netRipple.t -= dt) <= 0) G.netRipple = null; // rather than freezing mid-air + if (G.netRipple && (G.netRipple.t -= dt) <= 0) G.netRipple = null; // settles rather than freezing if (G.shake && (G.shake.t -= dt) <= 0) G.shake = null; if (G.boomFlash > 0) G.boomFlash -= dt; updateZombies(dt); @@ -1328,7 +1325,6 @@ export function update(dt) { else { G.card.t -= dt; if (G.card.t <= 0) G.card = null; } } if (G.shake && (G.shake.t -= dt) <= 0) G.shake = null; - if (G.spark && (G.spark.t -= dt) <= 0) G.spark = null; // the PERFECT ring burns out if (G.netRipple && (G.netRipple.t -= dt) <= 0) G.netRipple = null; // the tape settles if (G.zoom && (G.zoom.t += dt) >= G.zoom.T) G.zoom = null; // scripted punch-in (render applies it) if (G.bossBurst && (G.bossBurst.t -= dt) <= 0) G.bossBurst = null; diff --git a/render.js b/render.js index e334588..be76ebd 100644 --- a/render.js +++ b/render.js @@ -1301,7 +1301,6 @@ function drawModal(m) { // ------------------------------------------------------------------ frame ---- const ballHist = []; // hot-ball ghost trail positions (render-owned, see the streak block in render) -let flickAt = 14; // idleT of the next floodlight stutter (splatter ambience, see render) const AD_H = 14; // px tall the courtside ad strip is drawn — also the ceiling for centred HUD text const FLASH_Y = 68; // the ONE slot every free-floating message uses. Clear of the scoreboard block // (6 + 42 + 15 = 63 at its tallest), which the old 60 was not in portrait — @@ -1331,18 +1330,9 @@ export function render() { // stands, ads, rot tint, court, lines and the run-off, as one cached blit (see paintBackdrop); // the clap ripple is overlaid live inside. The ad strip sits flush on the court's back edge (the // out-area quad reaches z=-6), which keeps the boards clear of the lawn in BOTH orientations. - const standsBottom = drawBackdrop(pal, pt); - // floodlight stutter (splatter only): every 8-15 s the stadium lights dip for a tenth of a second - // — "the lights went wrong", made ambient. An overlay over the stands, so it never invalidates - // the backdrop cache; skipped during the duel, where the lights come back to tennis. - if (G.mode === 'splatter' && !G.interlude) { - if (flickAt > G.idleT + 16) flickAt = G.idleT + 8 + Math.random() * 7; // a fresh run pulled it back in - if (G.idleT > flickAt + 0.12) flickAt = G.idleT + 8 + Math.random() * 7; - else if (G.idleT >= flickAt) { - ctx.fillStyle = `rgba(4,2,10,${(0.3 * Math.abs(Math.sin((G.idleT - flickAt) / 0.12 * Math.PI * 2))).toFixed(3)})`; - ctx.fillRect(0, 0, W, standsBottom); - } - } + drawBackdrop(pal, pt); + // (a random floodlight stutter darkened the stands here for one build — on the phone it read as + // the crowd area flickering black, i.e. as a rendering bug, not as ambience. Gone.) // gore decals: what remains of the visiting team (flat ground layer, then far-side mounds) for (const d of gore) { @@ -1413,7 +1403,7 @@ export function render() { for (let i = 0; i <= 24; i++) { const x = nlt.x + (nrt.x - nlt.x) * i / 24; const y = nlt.y + (nrt.y - nlt.y) * i / 24 - + Math.sin((x - px) * 0.3 + rip.t * 34) * 3 * (rip.t / 0.4) * Math.exp(-Math.abs(x - px) * 0.05); + + Math.sin((x - px) * 0.3 + rip.t * 34) * 1.1 * (rip.t / 0.4) * Math.exp(-Math.abs(x - px) * 0.05); // ~1 px at the clip — a wobble, not a wave i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); } } else { ctx.moveTo(nlt.x, nlt.y); ctx.lineTo(nrt.x, nrt.y); } @@ -1558,20 +1548,8 @@ 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.spark) { // PERFECT contact: a one-shot expanding ring + four spark pixels at the strike. - // Light, which the procedural rule explicitly exempts — same clause as the barrel's glow. - const s = G.spark, k = 1 - s.t / 0.3, p = proj(s.x, s.y, s.z); - const r = Math.max(2, p.s * (0.06 + 0.4 * k)); - ctx.strokeStyle = `rgba(255,255,255,${(0.9 * (1 - k)).toFixed(3)})`; - ctx.lineWidth = 1.5; - ctx.beginPath(); ctx.ellipse(p.x, p.y, r, r * 0.75, 0, 0, 7); ctx.stroke(); - ctx.fillStyle = `rgba(255,250,220,${(1 - k).toFixed(3)})`; - for (let i = 0; i < 4; i++) { // sparks fly the diagonals, riding the ring's edge outward - const a = i * 1.5708 + 0.7854; - ctx.fillRect(Math.round(p.x + Math.cos(a) * r * 1.35), Math.round(p.y + Math.sin(a) * r), 1.5, 1.5); - } - ctx.lineWidth = 1; - } + // (a PERFECT-contact spark ring lived here for one build — Oliver's verdict: too much. The + // hitstop, the click and the praise word already carry the moment.) // 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 diff --git a/state.js b/state.js index a3e7270..95451a3 100644 --- a/state.js +++ b/state.js @@ -116,7 +116,6 @@ export const G = { modalQ: [], // popups waiting behind the open one — see showModal seen: {}, // one-shot markers so a first-time-only modal fires once per run zoom: null, // {x, z, k, t, T} — a scripted punch-in on a world point (the transition's turn, a boss death) - spark: null, // {x, y, z, t} — PERFECT contact: a one-shot ring + sparks at the racket face netRipple: null, // {x, t} — the ball clipped the tape; the net's top line wobbles there briefly best: null, // best run {games, sets}, loaded from localStorage when you get eaten newBest: false, // this run beat the stored best — the eaten summary celebrates it