From 44419d9ce1231acf1228068b306328e4c8c37e7e Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Mon, 3 Aug 2026 17:16:10 +0200 Subject: [PATCH] Pose the laser scene and hold it still, so it can be poked at T.laserScene() (or #laser on the URL) drops into splatter with six bodies of different types at known depths, the laser armed, a pull held, and the world paused. T.pause(true) freezes the WORLD but keeps drawing, which is the whole point: a value changed in devtools shows on the next frame while the scene stands still. The old recipe halted rAF, which freezes the picture too and is no use for judging how something looks. Co-Authored-By: Claude Opus 5 --- HANDOFF.md | 18 ++++++++++++++++++ index.html | 2 +- main.js | 42 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index c4fb54a..775e378 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -482,6 +482,24 @@ nothing for it to cut exactly where the crowd stands. are AIMING rather than only on the shot. Measured on desktop: 0.27 ms/frame with 20 bodies against a 16.7 ms budget. +## T.laserScene() — a posed, PAUSED scene for looking at a shot + +`T.laserScene()` in the console (or load the page with `#laser`) drops you into splatter with six +bodies of different types at known depths, the laser armed, a full pull held, and the world PAUSED. + +**`T.pause(true)` freezes the world but keeps DRAWING**, which is the point: change `TUNE.beamY` or a +colour in devtools and the next frame shows it, with the scene standing still. The old test recipe +halted rAF instead (`requestAnimationFrame = () => 0`), which freezes the picture too and is useless +for judging how something looks. `T.pause(false)` runs again; `T.render()` redraws by hand. + +It sets `TUNE.zMax = 0` so nothing wanders into the shot — that persists after unpausing, so reload +before playing on. + +The knobs worth turning there: `beamY` (the height the plane holds, and the line it cuts bodies at), +`beamOriginY`, `beamRise`, `beamW0`/`beamW1` (half-width at the racket and at the far end), `beamLen` +(the KILL range — the drawn length is solved separately against the ad strip), `beamT`, `dmgBeam`. +The cone's colours are the gradient stops in `drawBeamWedge`; the cut line is `CUT_COL`. + ## TEST MODE — triple-tap the scoreboard A sandbox for trying new content without playing twenty games to reach it. Three taps on the diff --git a/index.html b/index.html index 8dd8279..167531a 100644 --- a/index.html +++ b/index.html @@ -179,6 +179,6 @@ - + diff --git a/main.js b/main.js index 68229b4..a7e7565 100644 --- a/main.js +++ b/main.js @@ -13,9 +13,14 @@ import { cv } from './dom.js'; import './input.js'; let last = performance.now(); +// T.pause(true) freezes the WORLD but keeps drawing, so a value changed in devtools shows up on the +// next frame without the scene moving on underneath you. Halting rAF instead (the old test recipe) +// freezes the picture too, which is useless for looking at a shot. +let paused = false; function frame(now) { requestAnimationFrame(frame); // schedule FIRST: one bad frame must never kill the loop (a thrown error froze the game for good) if (TUNE.fps && now - last < 1000 / TUNE.fps - 2) return; // frame cap (battery): skip RAF ticks until it's our beat + if (paused) { last = now; render(); return; } let dt = Math.min(0.04, (now - last) / 1000); last = now; // A chapter boss dies in slow motion. The clock that ENDS it runs at real speed, so the effect is // always the same length in seconds however far the world has been slowed. @@ -194,9 +199,44 @@ cv.addEventListener('pointerdown', e => { if (umpTaps.length >= 3) { umpTaps = []; forceZombies(); } }); + +// ---- A POSED SCENE for looking at the laser. `T.laserScene()` in the console, or load the page +// with #laser. It drops you into splatter with a spread of bodies at known depths, the laser armed +// and a pull held, renders it, and PAUSES the world so devtools edits (TUNE.beamY, beamW1, the +// colours in drawBeamWedge) can be judged against a still picture. +function laserScene() { + const intro = document.getElementById('intro'); + if (intro) intro.hidden = true; + paused = false; + if (G.mode === 'tennis') forceZombies(); + for (let i = 0; i < 600; i++) update(1 / 60); // ride the transition out into splatter + G.modal = null; G.horde = null; G.interlude = false; G.changeover = 0; G.clearing = false; + G.mgT = 0; G.gunId = null; G.beam = null; G.bossFight = 0; G.hp = G.hpMax = TUNE.playerHp; + zombies.length = 0; pellets.length = 0; parts.length = 0; gore.length = 0; piles.length = 0; + score.server = 'P'; setupPoint(); + P.x = 0; + TUNE.zMax = 0; // no new spawns wandering into the shot + // one of everything, at four depths, straddling the lane so the cut lands at different heights + for (const [x, z, v] of [[0, 20, 'a'], [0.75, 16, 'b'], [-0.85, 12, 'k'], + [0.85, 8.5, 'g'], [-0.75, 5.5, 'h'], [0, 3, 'e']]) { + spawnZombie(x, z, v); + zombies[zombies.length - 1].v = 0; // hold still: this is a photograph + } + G.rack.length = 0; G.temp = null; + G.weapon = grantWeapon('laser'); + G.modal = null; + G.aim = { fx: 0, fy: -1, mag: 100 }; // a full pull straight up-court (fy is NEGATIVE) + update(1 / 60); P.x = 0; + render(); + paused = true; + return 'laser scene posed and PAUSED. T.pause(false) to run again, T.render() to redraw by hand.'; +} +// a plain timer, not the load event: this module runs as a module, so `load` may already have gone +if (location.hash === '#laser') setTimeout(laserScene, 600); + requestAnimationFrame(frame); // exposed for in-browser sanity checks and simulated playtests -window.T = { TUNE, ball, P, O, score, G, zombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, spawnBarrel, grantWeapon, toggleTestMode, piles, gore, parts, pellets, extras, shots, explode, hurtPlayer, killPlayer, splatPoint, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies, deliverTin, selectWeapon, showModal, dismissModal, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight, +window.T = { TUNE, ball, P, O, score, G, zombies, laserScene, pause: v => { paused = v !== false; }, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, spawnBarrel, grantWeapon, toggleTestMode, piles, gore, parts, pellets, extras, shots, explode, hurtPlayer, killPlayer, splatPoint, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies, deliverTin, selectWeapon, showModal, dismissModal, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight, step: dt => update(dt), render, press: keyPressed, get phase() { return G.phase; }, set phase(v) { G.phase = v; } };