Brighter stands, a serve hint that shows the gesture, and a boy who walks
- The crowd sat in the dark, worst of all in splatter mode where you spend the game. Both crowd frames get an identical gamma lift (means 30/14/39 -> 42/22/56) and the rot tint drops 0.5 -> 0.26. The curve MUST stay identical across the two frames or the clap swap flashes brightness; _sheet_crowd1.png keeps the pre-lift original. - The splatter hint led with the tap when pulling is the whole gesture. It now reads PULL BACK TO AIM - RELEASE TO FIRE, and after 1.5s of sitting in the serve stance untouched a ghost thumb slides back while chevrons grow up-court on a loop. Pull back, shoot forward is an inverse relationship that a line of text cannot carry on its own. - The mop boy has three sheets instead of two: the push cycle now plays ONLY while he is stopped and working, a new carry sheet walks him upright with the mop shouldered, and the run sheet stays for the tennis-mode ball fetch. He was mopping thin air the entire way to the stain. Sheet note: frames 2 and 3 of the carry sheet merged (a mop handle bridging the gap), so it is cut at the true column gaps plus the thinnest column inside the merged pair, then common-scaled — same technique as the brute stride sheet. 26-game soak clean. Serve returns 133/150 = 88.7% +/-2.6%, 30 human-tapped points = 50 hits / 0 whiffs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No files matched your search
@@ -230,6 +230,20 @@ Oliver rejected the procedural fakes on sight. The rule this round: **animation
|
||||
expects; it's now ~62. Rack text is positioned proportionally (`g.h * 0.33` / `0.72`) so the tile
|
||||
height stays a free knob.
|
||||
|
||||
## Fifth feedback round (2026-07-27)
|
||||
- **Crowd brightness**: both crowd sheets got an identical gamma-0.8 lift (means 30/14/39 →
|
||||
42/22/56) and the splatter tint dropped 0.5 → 0.26. **If you ever touch one crowd frame you must
|
||||
apply the exact same curve to the other**, or the clap swap starts flashing brightness.
|
||||
`assets/_sheet_crowd1.png` is the pre-lift original.
|
||||
- **The splatter serve hint led with the tap** ("TAP TO SERVE · PULL BACK TO AIM") when pulling is
|
||||
the entire gesture. Now "PULL BACK TO AIM · RELEASE TO FIRE", plus `drawPullHint()`: after
|
||||
`TUNE.pullHintAfter` seconds of sitting in the serve stance untouched (`G.idleHintT`), a ghost
|
||||
thumb slides back while chevrons grow up-court, on a loop. It teaches the inverse relationship
|
||||
the text can't.
|
||||
- **The mop boy has three sheets now**, not two: `x_boymop` is the push cycle used ONLY while
|
||||
stopped and working, `x_boycarry` is him walking upright with the mop shouldered, `x_boyrun` is
|
||||
the tennis-mode fetch. He was mopping the air the whole way to the stain.
|
||||
|
||||
## Gotchas learned the hard way
|
||||
- Browser-pane verification: python http.server sends no cache headers, so edited ES
|
||||
modules often load STALE even after location.reload(). Reliable fix: preview_stop +
|
||||
|
||||
|
After Width: | Height: | Size: 474 KiB |
|
After Width: | Height: | Size: 806 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 806 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 611 KiB |
@@ -58,6 +58,7 @@ export const TUNE = {
|
||||
// mean something: at 0.45 the cone was ±10 m at the far baseline — the whole
|
||||
// court — so every downward pull picked the same body and the angle felt inert.
|
||||
// 0.10 gives ±2.9 m out there: you have to actually point at a lane.
|
||||
pullHintAfter: 1.5, // s of doing nothing in the serve stance before the pull gesture demos itself
|
||||
aimTie: 0.12, // how much worse-aimed a NEARER body may be and still win the lane (0..1 of the
|
||||
// cone half-width). 0 = pure best-aligned wins; 1 = the old "nearest in the
|
||||
// lane always wins", which is what made the magnet grab the wrong shambler.
|
||||
|
||||
@@ -798,6 +798,8 @@ export function update(dt) {
|
||||
crowd.cheer = Math.max(0, crowd.cheer - dt);
|
||||
G.idleT += dt;
|
||||
const w = BOUNCE_IDLE.tempo;
|
||||
// idle in the serve stance and not touching anything: after a beat, show the pull gesture
|
||||
if (G.phase === 'pretoss' && !G.aim) G.idleHintT += dt; else G.idleHintT = 0;
|
||||
if (G.phase === 'pretoss' && Math.sin(G.idleT * w) * Math.sin((G.idleT - dt) * w) < 0) pock(0.12, 420); // ball bounce
|
||||
if (G.phase === 'toss') { const sv = score.server === 'P' ? P : O; sv.pose = 'serve'; sv.poseT = 0; }
|
||||
|
||||
|
||||
@@ -60,6 +60,6 @@
|
||||
<p class="start">TAP TO START</p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="main.js?v=34"></script>
|
||||
<script type="module" src="main.js?v=35"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -76,7 +76,8 @@ const SPR = {};
|
||||
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_swing1','p_swing2',
|
||||
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2',
|
||||
'x_ump','x_boyrun1','x_boyrun2','x_boyrun3','x_boyrun4','x_crowd','x_crowd2','x_ads',
|
||||
'x_boymop1','x_boymop2','x_boymop3','x_boymop4','x_howto', // the mopping boy, and the slingshot tutorial panel
|
||||
'x_boymop1','x_boymop2','x_boymop3','x_boymop4', // the push cycle, used only while he is stopped and working
|
||||
'x_boycarry1','x_boycarry2','x_boycarry3','x_boycarry4','x_howto', // walking with the mop shouldered, and the slingshot tutorial panel
|
||||
'z_gw2','z_gw3','z_gw4','z_gwb','z_gw2b','z_gw3b','z_gw4b', // the brute comes apart in stages (4/5 hp = z_gw .. 1/5 = z_gw4), each with a 2-frame lurch (…b)
|
||||
'z_a1','z_a2','z_a3','z_a4','z_b1','z_b2','z_b3','z_b4','z_c1','z_c2',
|
||||
'z_f1','z_f2','z_f3','z_f4','z_g1','z_g2','z_gw','z_s1','z_s2','z_s3',
|
||||
@@ -143,11 +144,15 @@ function drawStatic(name, x, z, hMeters, bob = 0) {
|
||||
function drawBoy(b) {
|
||||
// mopping is not sprinting: its own sheet and a slower push cycle. Falls back to the run
|
||||
// frames until the mop PNGs have loaded.
|
||||
// Three sheets, because a boy WALKING to the next stain is not a boy mopping: x_boymop is the
|
||||
// push cycle (used only while he's stopped and working), x_boycarry is him walking upright with
|
||||
// the mop shouldered, x_boyrun is the plain tennis-mode ball fetch.
|
||||
const mop = b.mop && SPR.x_boymop1;
|
||||
const sheet = !mop ? 'x_boyrun' : b.mopT > 0 ? 'x_boymop' : SPR.x_boycarry1 ? 'x_boycarry' : 'x_boymop';
|
||||
// Frames step off DISTANCE COVERED, not elapsed time: a time-driven cycle made him moonwalk
|
||||
// whenever his speed and the frame rate disagreed. Now the stride owns the ground he covers.
|
||||
const stride = TUNE.boyStride * (mop ? 1 : 2.1); // a sprint eats more ground per frame than a push
|
||||
const img = SPR[(mop ? 'x_boymop' : 'x_boyrun') + (Math.floor((b.dist || 0) / stride) % 4 + 1)]; if (!img) return;
|
||||
const img = SPR[sheet + (Math.floor((b.dist || 0) / stride) % 4 + 1)]; if (!img) return;
|
||||
const p = proj(b.x, 0, b.z);
|
||||
// scale off the frame's OWN pixel height, not a fixed BOY_H: the sheets are common-scaled per
|
||||
// sheet, so a deep mopping lean is genuinely shorter and must not be stretched back upright
|
||||
@@ -379,6 +384,40 @@ function drawCard(title, sub1, sub2, color) {
|
||||
if (sub2) ctx.fillText(sub2, W / 2, H / 2 + 12);
|
||||
}
|
||||
|
||||
// Sit in the serve stance doing nothing and the gesture demonstrates itself: a ghost thumb slides
|
||||
// BACK from the player while chevrons grow FORWARD up the court, then it releases and repeats.
|
||||
// Pulling is the whole game in splatter mode, and a line of text was carrying that alone.
|
||||
function drawPullHint() {
|
||||
const cyc = 1.9, t = (G.idleHintT - TUNE.pullHintAfter) % cyc;
|
||||
const draw = Math.min(1, t / 1.15); // the pull itself, then a beat held at full
|
||||
const gone = t > 1.45; // released — the thumb lifts off
|
||||
const anchor = proj(P.x, 0, P.z + 0.6);
|
||||
const ax = Math.round(anchor.x), ay = Math.round(Math.min(anchor.y + 6, METER_Y() - 26));
|
||||
const len = 30 * draw;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = gone ? 0 : 0.85;
|
||||
// the dotted track the thumb travels, straight down-screen = "pull back"
|
||||
ctx.fillStyle = UI.dim;
|
||||
for (let d = 6; d < 34; d += 6) ctx.fillRect(ax - 1, ay + d, 2, 2);
|
||||
// chevrons growing UP-court as the pull grows — the inverse relationship is the whole lesson
|
||||
ctx.fillStyle = UI.warm;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const u = (i + 1) / 4;
|
||||
if (draw < u * 0.9) continue;
|
||||
const cy = ay - 8 - i * 7, s = 4;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(ax, cy - s); ctx.lineTo(ax + s, cy + s * 0.7); ctx.lineTo(ax - s, cy + s * 0.7);
|
||||
ctx.fill();
|
||||
}
|
||||
// the ghost thumb
|
||||
ctx.globalAlpha = gone ? 0 : 0.9;
|
||||
ctx.fillStyle = UI.cream;
|
||||
ctx.beginPath(); ctx.arc(ax, ay + len, 5, 0, 7); ctx.fill();
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.45)';
|
||||
ctx.beginPath(); ctx.arc(ax, ay + len, 2, 0, 7); ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// The modal: drawn LAST, over everything, and the world behind it is frozen (gameplay.update
|
||||
// early-returns). Unlike drawCard this is a wall — nothing moves until it is tapped away.
|
||||
function drawModal(m) {
|
||||
@@ -455,7 +494,9 @@ export function render() {
|
||||
for (let x = 0; x < W; x += tw) ctx.drawImage(ads, x, standsBottom - adH, tw, adH);
|
||||
} else { ctx.fillStyle = pal.board; ctx.fillRect(0, standsBottom - adH, W, adH); }
|
||||
// the rot dims the stadium: one tint over crowd + boards, fading home during duels
|
||||
if (pt > 0) { ctx.fillStyle = `rgba(12,8,20,${0.5 * pt})`; ctx.fillRect(0, 0, W, standsBottom); }
|
||||
// the rot dims the stadium — but 0.5 on top of an already-dark crowd sheet buried the stands in
|
||||
// splatter mode, where you spend most of the game. 0.26 still reads as "the lights went wrong".
|
||||
if (pt > 0) { ctx.fillStyle = `rgba(12,8,20,${0.26 * pt})`; ctx.fillRect(0, 0, W, standsBottom); }
|
||||
|
||||
// ground + court (Super Tennis lawn); everything outside the doubles court is OUT
|
||||
const dw = COURT.dblW / 2;
|
||||
@@ -680,10 +721,12 @@ export function render() {
|
||||
? (G.weapon === 4 ? (touch ? 'TAP TO UNLOAD THE GUN' : 'SPACE UNLOADS THE GUN')
|
||||
: G.aim ? 'PULL BACK: FURTHER = HARDER'
|
||||
: G.weapon > 0 ? `SERVE FIRES: ${sel} · ${switchHint}`
|
||||
: touch ? 'TAP TO SERVE · PULL BACK TO AIM' : 'SPACE SERVES · ARROWS AIM')
|
||||
: touch ? 'PULL BACK TO AIM · RELEASE TO FIRE' : 'DRAG BACK TO AIM · SPACE SERVES')
|
||||
: G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
|
||||
: (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE');
|
||||
ctx.fillText(txt, W / 2, METER_Y()); // same slot the draw meter uses
|
||||
// and after a beat of inaction, show the gesture instead of only naming it
|
||||
if (G.mode === 'splatter' && !G.interlude && G.weapon !== 4 && G.idleHintT > TUNE.pullHintAfter) drawPullHint();
|
||||
}
|
||||
if (G.flash) {
|
||||
ctx.font = F(12); ctx.fillStyle = '#ffffff';
|
||||
|
||||
@@ -46,6 +46,8 @@ export const G = {
|
||||
afterDuel: null, // the game beat the scheduled duel interrupted ('changeover' | 'game'), resumed when it resolves
|
||||
pendingGame: false, // a game just ended — flash 'GAME N' when the next one arms
|
||||
aim: null, // live two-stage serve aim {fx, fy, mag} while the thumb is down (splatter pretoss)
|
||||
idleHintT: 0, // s the player has sat in pretoss without touching anything — after a beat the
|
||||
// pull gesture demonstrates itself (render.drawPullHint)
|
||||
meleeT: 0, // s of racket-swish cooldown left
|
||||
interlude: false, // sudden-death duel vs the racket zombie: falsy, or {introT} while announcing
|
||||
duelLerp: 0, // 0..1 — how far the court has returned to tennis colors for the duel
|
||||
|
||||