Ten small flourishes, all borrowed machinery
Kills stutter the world a touch more per streak; a massacre borrows the boss's slow-mo; a boss death gets the transition's zoom punch. PERFECT contact flashes a spark ring. The sidestep and every real bounce kick up court-tinted dust. The net cord ripples where the ball clips it. The landing ring contracts on time-to-touchdown. Hot rally balls drag three fading ghosts. And every 8-15 seconds the floodlights stutter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d847c2e4a8
commit
8ab7ac0c25
4 files changed
+98
-10
No files matched your search
@@ -924,11 +924,11 @@ function drawAimMark(a, u0, flying) {
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function drawRing(x, z, color) {
|
||||
function drawRing(x, z, color, k = 1) { // k: radius scale — the landing ring contracts on time-to-touchdown
|
||||
const p = proj(x, 0, z);
|
||||
// integer-snapped so the moving ring steps whole pixels instead of shimmering
|
||||
const cx = Math.round(p.x), cy = Math.round(p.y);
|
||||
const rx = Math.max(2, Math.round(p.s * 0.3)), ry = Math.max(1, Math.round(p.s * 0.12));
|
||||
const rx = Math.max(2, Math.round(p.s * 0.3 * k)), ry = Math.max(1, Math.round(p.s * 0.12 * k));
|
||||
ctx.strokeStyle = color; ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, 7); ctx.stroke();
|
||||
}
|
||||
@@ -1300,6 +1300,8 @@ 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 —
|
||||
@@ -1330,6 +1332,17 @@ export function render() {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// gore decals: what remains of the visiting team (flat ground layer, then far-side mounds)
|
||||
for (const d of gore) {
|
||||
@@ -1344,7 +1357,9 @@ export function render() {
|
||||
// landing rings: white = incoming ball, yellow/red = your shot (red lands out)
|
||||
if (G.phase === 'rally' && ball.active && ball.bounces === 0 && ball.lastHit === 'O') {
|
||||
const l = predictLanding();
|
||||
if (l) drawRing(l.x, l.z, 'rgba(255,255,255,0.85)');
|
||||
// the ring CONTRACTS toward touchdown (predictLanding's closed-form t), so the static marker
|
||||
// becomes a timing cue: wide while the ball is high, snapping to its true size as it arrives
|
||||
if (l) drawRing(l.x, l.z, 'rgba(255,255,255,0.85)', Math.min(2.2, 0.55 + l.t * 1.5));
|
||||
}
|
||||
if (G.outMarker) drawRing(G.outMarker.x, G.outMarker.z, G.outMarker.out ? UI.hot : UI.warm);
|
||||
// SLINGSHOT AIM: the shot's actual flight, drawn as a band through the air.
|
||||
@@ -1391,7 +1406,18 @@ export function render() {
|
||||
ctx.fillStyle = 'rgba(20,26,34,0.72)';
|
||||
ctx.beginPath(); ctx.moveTo(nl.x, nl.y); ctx.lineTo(nr.x, nr.y); ctx.lineTo(nrt.x, nrt.y); ctx.lineTo(nlt.x, nlt.y); ctx.fill();
|
||||
ctx.strokeStyle = '#eeeeee'; ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(nlt.x, nlt.y); ctx.lineTo(nrt.x, nrt.y); ctx.stroke();
|
||||
ctx.beginPath();
|
||||
const rip = G.netRipple;
|
||||
if (rip) { // net cord: a damped travelling wave centred where the ball clipped the tape (0.4 s)
|
||||
const px = proj(rip.x, COURT.netH, COURT.netZ).x;
|
||||
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);
|
||||
i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
|
||||
}
|
||||
} else { ctx.moveTo(nlt.x, nlt.y); ctx.lineTo(nrt.x, nrt.y); }
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = '#333'; ctx.fillRect(nl.x - 1, nlt.y, 2, nl.y - nlt.y); ctx.fillRect(nr.x - 1, nrt.y, 2, nr.y - nrt.y);
|
||||
drawPiles(true); // near-side piles: after the net (they're closer), before the near actors
|
||||
if (G.boy && G.boy.z > COURT.netZ) drawBoy(G.boy);
|
||||
@@ -1431,6 +1457,22 @@ export function render() {
|
||||
const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * (mine ? bounceApex() : BOUNCE_IDLE.oApex);
|
||||
drawBall(bx, by, sv.z);
|
||||
}
|
||||
// hot-ball ghost streak: a rally ball above 0.8 pow drags three fading ghosts behind it. Serves
|
||||
// already have the erasing arc band; this gives the volley its own read. Render-owned visual
|
||||
// state (like the crowd dots) — gameplay never sees it.
|
||||
const hot = ball.active && (ball.pow || 0) > 0.8 && G.phase === 'rally' && !ball.collected;
|
||||
if (hot) { ballHist.push({ x: ball.x, y: ball.y, z: ball.z }); if (ballHist.length > 7) ballHist.shift(); }
|
||||
else if (ballHist.length) ballHist.length = 0;
|
||||
if (hot) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const h = ballHist[ballHist.length - 2 - i * 2]; if (!h) break;
|
||||
const p = proj(h.x, h.y, h.z);
|
||||
ctx.globalAlpha = 0.26 - i * 0.08;
|
||||
ctx.fillStyle = UI.ball;
|
||||
ctx.beginPath(); ctx.arc(p.x, p.y, Math.max(1, p.s * 0.09 * (0.8 - i * 0.18)), 0, 7); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
// the match ball (and any triple-serve side balls in flight)
|
||||
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb, ball.flame);
|
||||
for (const e of extras) drawBall(e.x, e.y, e.z);
|
||||
@@ -1516,6 +1558,20 @@ 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;
|
||||
}
|
||||
// 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
|
||||
|
||||
Reference in new issue
Block a user