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; } };