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 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-03 17:16:10 +02:00
1 parent b44fcd8465
commit 44419d9ce1
3 files changed
+60 -2

No files matched your search

+18
View File
@@ -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
+1 -1
View File
@@ -179,6 +179,6 @@
</div>
</div>
</div>
<script type="module" src="main.js?v=106"></script>
<script type="module" src="main.js?v=107"></script>
</body>
</html>
+41 -1
View File
@@ -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; } };