Shape the cone by hand, and give the playground its knobs back
Oliver's tuned numbers become the defaults: beamY 0.95, beamLen 23, beamW0 0.08, beamW1 3.1, beamOriginLift 0.44, beamBorder 0.18. Three knobs earned their keep on the way there. beamLen is the DRAWN length now, not only the kill range. The drawn length was solved every frame so the far end landed on the middle of the ad strip, which made this a control that visibly did nothing. One meaning beats one clever behaviour -- and it pairs with beamY, since a lower plane meets the ads sooner. beamOriginLift replaces beamOriginY: the apex is a lift ABOVE the plane rather than an absolute height, so lowering beamY brings the near end down with it instead of leaving it hanging at chest height with the beam sloping away. beamBorder exposes the edge width, which was hardcoded. And the playground is back, deliberately NOT paused. The first version froze the world so the picture would hold still, and then nothing redrew, so every value changed did nothing -- a still frame you cannot change is worth nothing. The bodies are held still individually and spawning is off instead. Also: a boolean living in TUNE has to be tested with >= 0.5. The panel steps every key by 0.02, so one nudge set beamBand to 0.02 -- still truthy -- and the renderer switched variants under him mid-tune. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac1d5fbccd
commit
99774ab4be
6 files changed
+125
-62
No files matched your search
+24
@@ -443,6 +443,9 @@ that holds that height all the way out instead of sloping to the ground — that
|
||||
makes the rest possible. Sloping, it lay flat on the grass across the whole far half, so there was
|
||||
nothing for it to cut exactly where the crowd stands.
|
||||
|
||||
- **`beamLen` is the DRAWN length now, not just the kill range.** It used to be solved every frame so
|
||||
the far end landed on the middle of the ad strip, which made it a knob that visibly did nothing.
|
||||
One meaning beats one clever behaviour.
|
||||
- **`TUNE.beamBand` is a live A/B between two looks**, flippable from the T panel on the phone.
|
||||
**1 (default) = the BAND**: drawn the way the ball's aim trajectory is drawn — a path down the lane
|
||||
whose width comes from each point's OWN projection scale, so it narrows into the distance like
|
||||
@@ -494,6 +497,27 @@ 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() — the tuning playground
|
||||
|
||||
`T.laserScene()` in the console (or `#laser` on the URL) poses six bodies of different types at known
|
||||
depths with the laser armed and a pull held. `T.laserFire()` shoots it; `T.laserScene()` re-poses.
|
||||
|
||||
**It does NOT pause, and that is the point.** The first version froze the world so the picture would
|
||||
hold still — and then every value changed in the T panel did nothing visible, because nothing
|
||||
redrew. Oliver: "tuning the parameters does not change the view (because it's paused?)". The bodies
|
||||
are held still individually (`v = 0`) and spawning is off instead, so the picture is stable while the
|
||||
render loop keeps running. **Never pose a tuning scene by halting rAF or pausing** — a still frame
|
||||
you cannot change is worth nothing.
|
||||
|
||||
**A boolean living in TUNE must be tested with `>= 0.5`.** The panel renders every TUNE key as a
|
||||
number input stepping by 0.02, so one nudge on `beamBand` set it to 0.02 — still truthy — and the
|
||||
renderer silently switched variants under him mid-tune.
|
||||
|
||||
The laser's shape was tuned this way and these are Oliver's numbers: `beamY` 0.95, `beamLen` 23,
|
||||
`beamW0` 0.08, `beamW1` 3.1, `beamOriginLift` 0.44, `beamBorder` 0.18.
|
||||
**`beamY` and `beamLen` interact** — a lower plane meets the ad strip at a shorter distance, so
|
||||
dropping one wants the other shortened.
|
||||
|
||||
## T.pause(true) — freeze the world, keep drawing
|
||||
|
||||
`T.pause(true)` stops `update()` but keeps `render()` running, so a value changed in devtools shows
|
||||
|
||||
@@ -240,27 +240,36 @@ export const TUNE = {
|
||||
testSpawnEvery: 0.9,// s between spawns: they keep coming
|
||||
testLevel: 20, // the roster level pickVariant pretends to be at, so every body type can turn up
|
||||
// ---- THE LASER's wedge: aim and release like a serve, and a plane of light widens down the lane
|
||||
beamLen: 34, // m it reaches. Was 26, which stopped at the far baseline and read as the beam
|
||||
// being CUT OFF in mid-court; the player stands at z~24, so this carries it past
|
||||
// the baseline and up into the courtside ads, which is where light should stop.
|
||||
beamY: 1.15, // m — the height the plane HOLDS down the court, and the line it cuts bodies at.
|
||||
beamLen: 23, // m it reaches — the DRAWN length as well as the kill length. It used to be only
|
||||
// the kill: the drawn length was solved each frame so the far end landed on the
|
||||
// middle of the ad strip, which meant turning this knob did nothing you could
|
||||
// see. One meaning now. Tuned WITH beamY: a lower plane reaches the ads sooner,
|
||||
// so dropping one wants the other shortened. Oliver's numbers, 2026-08-03.
|
||||
beamY: 0.95, // m — the height the plane HOLDS down the court, and the line it cuts bodies at.
|
||||
// It used to slope to the ground by about 20 m out, which meant that across the
|
||||
// whole far half it lay flat on the grass: nothing to cut, and the split would
|
||||
// have been invisible exactly where the crowd stands. Chest on a shambler, belly
|
||||
// on a brute, thigh on the roller man — so a bigger body visibly takes it lower.
|
||||
beamOriginY: 1.25,// m — it leaves the PLAYER at chest height, not the racket head. Emitted from
|
||||
// the racket (2.2 m) the apex hung in the air above him and read as detached;
|
||||
// out of his chest, level with the plane it becomes, it reads as HIS shot.
|
||||
beamOriginLift: 0.44, // m the apex sits ABOVE beamY. It is a lift, not an absolute height, so
|
||||
// lowering beamY brings the near end down with it — set as an absolute, dropping
|
||||
// the plane left the apex hanging at chest height with the beam sloping away
|
||||
// beneath it. It leaves the PLAYER, not the racket head: emitted from the racket
|
||||
// (2.2 m) the apex floated above him and read as detached.
|
||||
beamRise: 0.14, // fraction of the length it takes to settle from the origin to that height
|
||||
beamW0: 0.06, // m half-width at the racket — a POINT. At 0.35 it left the racket as a 0.7 m
|
||||
// bar hanging in the air rather than as something emitted from the strings.
|
||||
beamW1: 5.0, // ...and at the far end. It is a CONE: the spread is the point of it
|
||||
beamW0: 0.08, // m half-width at the racket — very nearly a point
|
||||
beamW1: 3.1, // ...and at the far end. It IS a cone — but a gentle one. At 5.0 the world width
|
||||
// grew so much faster than the projection shrank that on screen it opened out
|
||||
// while the court receded, and the eye read that as a flat shape stuck to the
|
||||
// glass. Growing slowly, the two roughly cancel and it lies in the world.
|
||||
// A/B, live from the T panel on the phone. 1 = draw the laser the way the BALL's aim band is
|
||||
// drawn: a path down the lane whose width comes from each point's OWN projection scale, so it
|
||||
// tapers into the distance and sits in the world. 0 = the solid widening cone.
|
||||
// The cone is a real projection, but it GROWS in world width (0.06 m -> beamW1), so on screen it
|
||||
// opens out while everything else recedes — which is what reads as a flat sticker.
|
||||
beamBand: 1,
|
||||
beamBorder: 0.18, // px of the two outer edge lines — a hairline. At 1 they were the heaviest
|
||||
// thing on the court and boxed the cone in.
|
||||
beamBand: 0, // 0 now: the CONE won, once it stopped ballooning (see beamW1). >= 0.5 is ON —
|
||||
// the panel steps by 0.02 and a nudge to 0.02 used to flip the renderer silently
|
||||
beamBandW: 0.30, // world half-width of that band — CONSTANT, which is what makes it taper
|
||||
beamT: 0.42, // s the wedge stays drawn
|
||||
dmgBeam: 8, // what it takes off a BOSS. An ordinary body is not damaged by the beam, it is
|
||||
|
||||
+1
-1
@@ -179,6 +179,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="main.js?v=109"></script>
|
||||
<script type="module" src="main.js?v=110"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -200,9 +200,48 @@ cv.addEventListener('pointerdown', e => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ---- A POSED SCENE for tuning a shot: `T.laserScene()`, or load with #laser. Six bodies of
|
||||
// different types at known depths, the laser armed, a pull held.
|
||||
// IT DOES NOT PAUSE, deliberately. The first version froze the world so the picture would hold
|
||||
// still — and then every TUNE value you changed did nothing visible, because nothing redrew. The
|
||||
// bodies are held still individually (v = 0) and spawning is off instead, so the picture is stable
|
||||
// while the render loop keeps running and an edit shows on the very next frame.
|
||||
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; // nothing new wanders into the shot
|
||||
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; // held still: the scene 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)
|
||||
return 'posed and LIVE — change TUNE (T panel or console) and it redraws. T.laserFire() to shoot it.';
|
||||
}
|
||||
// fire the posed shot, then pose it again so you can keep tuning
|
||||
function laserFire() {
|
||||
G.aim = null; score.server = 'P'; setupPoint();
|
||||
keyPressed('Space');
|
||||
return 'fired — T.laserScene() to re-pose';
|
||||
}
|
||||
// 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, 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,
|
||||
window.T = { TUNE, ball, P, O, score, G, zombies, laserScene, laserFire, 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; } };
|
||||
@@ -601,57 +601,48 @@ function drawShotBand(a, u0, flying) {
|
||||
function drawBeamWedge(dirX, dirZ, ox, oy, oz, from) {
|
||||
const d = Math.hypot(dirX, dirZ) || 1, ux = dirX / d, uz = dirZ / d;
|
||||
const px = -uz, pz = ux; // the lane's perpendicular, on the ground
|
||||
// How far it runs is solved, not tuned: walk out along the lane until the plane's own projection
|
||||
// reaches the middle of the ad strip. Solving it (rather than pinning the corners afterwards)
|
||||
// keeps the drawn triangle a TRUE projection of the plane, which is what lets the per-body cut
|
||||
// line below agree with the edge you can see.
|
||||
const target = proj(0, 0, -6).y - AD_H / 2;
|
||||
let lo = 4, hi = 70;
|
||||
for (let k = 0; k < 18; k++) {
|
||||
const m = (lo + hi) / 2;
|
||||
if (proj(ox + ux * m, TUNE.beamY, oz + uz * m).y > target) lo = m; else hi = m;
|
||||
}
|
||||
const L = (lo + hi) / 2;
|
||||
// The DRAWN length is TUNE.beamLen, straight. It used to be solved here each frame so the far end
|
||||
// landed on the middle of the ad strip — which made beamLen a knob that visibly did nothing, since
|
||||
// it only governed the kill. One meaning is worth more than one clever behaviour.
|
||||
const L = TUNE.beamLen;
|
||||
if (from >= 1) return L;
|
||||
// it leaves the racket high and settles to the cutting height within the first stretch
|
||||
const yAt = u => oy + (TUNE.beamY - oy) * Math.min(1, u / TUNE.beamRise);
|
||||
const wAt = u => TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * u;
|
||||
const corner = (u, w) => proj(ox + ux * (u * L) + px * w, yAt(u), oz + uz * (u * L) + pz * w);
|
||||
if (TUNE.beamBand) { drawBeamBand(ux, uz, ox, oy, oz, L, from); return L; }
|
||||
// >= 0.5, not just truthy: the T panel steps numbers by 0.02, so one nudge on this flag set it to
|
||||
// 0.02 — still truthy, and the renderer silently switched under Oliver mid-tune. A flag living in
|
||||
// a table of numbers has to be robust to being treated as a number.
|
||||
if (TUNE.beamBand >= 0.5) { drawBeamBand(ux, uz, ox, oy, oz, L, from); return L; }
|
||||
const u0 = Math.max(0, from), w0 = wAt(u0), w1 = wAt(1);
|
||||
// TWO gradients, because the reference has both: a HORIZONTAL one across the width (near-white
|
||||
// down the centre line, falling to blue at the edges) and a DEPTH one along the length. Canvas has
|
||||
// no 2D gradient, so the wedge is filled in a handful of slices along its length, each with its
|
||||
// own across-gradient at that slice's depth alpha. Enough slices and the seams disappear; too few
|
||||
// and it bands, which is the thing the old nested-wedge version got wrong.
|
||||
const SEGS = 8;
|
||||
for (let i = 0; i < SEGS; i++) {
|
||||
const ua = u0 + (1 - u0) * (i / SEGS), ub = u0 + (1 - u0) * ((i + 1) / SEGS);
|
||||
const um = (ua + ub) / 2;
|
||||
const wa = wAt(ua), wb = wAt(ub), wm = wAt(um);
|
||||
const a1 = corner(ua, -wa), a2 = corner(ua, wa), b2 = corner(ub, wb), b1 = corner(ub, -wb);
|
||||
const el = corner(um, -wm), er = corner(um, wm); // the across axis, at this slice's width
|
||||
// FULLY OPAQUE. Depth is carried by BRIGHTNESS, not by alpha — the plane is solid colour, dark
|
||||
// blue at the edges and near-white down the core, dimming as it runs away from you. Nothing under
|
||||
// it shows through, so "above the plane" and "below the plane" stop being a matter of degree:
|
||||
// the part above is simply there and the part below is simply gone behind it.
|
||||
const f = 1 - 0.5 * um; // depth: full at the strings, half at the ads
|
||||
const mix = (c, m) => `rgb(${Math.round(c[0] * m)},${Math.round(c[1] * m)},${Math.round(c[2] * m)})`;
|
||||
const g = ctx.createLinearGradient(el.x, el.y, er.x, er.y);
|
||||
g.addColorStop(0, mix(BEAM_EDGE, f * 0.8)); // edge: deep blue
|
||||
g.addColorStop(0.30, mix(BEAM_MID, f));
|
||||
g.addColorStop(0.5, mix(BEAM_CORE, f)); // core: the hot line down the middle
|
||||
g.addColorStop(0.70, mix(BEAM_MID, f));
|
||||
g.addColorStop(1, mix(BEAM_EDGE, f * 0.8));
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath(); ctx.moveTo(a1.x, a1.y); ctx.lineTo(a2.x, a2.y); ctx.lineTo(b2.x, b2.y); ctx.lineTo(b1.x, b1.y);
|
||||
ctx.closePath(); ctx.fill();
|
||||
// THE SHADOW ON THE LAWN, borrowed from the ball's aim band and from the band variant — this is
|
||||
// most of why those read as lying in the world. A bright shape hanging in the air is ambiguous
|
||||
// about where it actually is until something pins it to the ground.
|
||||
for (let i = 0; i <= 14; i++) {
|
||||
const u = u0 + (1 - u0) * (i / 14), w = wAt(u);
|
||||
const p = proj(ox + ux * (u * L), 0, oz + uz * (u * L));
|
||||
ctx.fillStyle = `rgba(0,0,0,${0.16 * (1 - u * 0.55)})`;
|
||||
ctx.beginPath(); ctx.ellipse(p.x, p.y, Math.max(0.8, p.s * w * 0.9), Math.max(0.4, p.s * w * 0.3), 0, 0, 7);
|
||||
ctx.fill();
|
||||
}
|
||||
// ...and the OUTER LINES. The reference reads as a plane because its edges are drawn, not just
|
||||
// implied by where the fill stops.
|
||||
// ONE gradient, along the LENGTH only. There was a second one across the width (near-white core
|
||||
// falling to blue at the edges) and it is gone: across a shape this narrow it read as a highlight
|
||||
// painted down the middle rather than as depth. The plane is a flat solid colour now, dimming as
|
||||
// it runs away from you — that is the whole shading.
|
||||
const c1 = corner(u0, -w0), c2 = corner(u0, w0), c3 = corner(1, w1), c4 = corner(1, -w1);
|
||||
ctx.strokeStyle = 'rgb(210,238,255)'; // the outer lines, solid like the rest of it
|
||||
ctx.lineWidth = 1;
|
||||
const mix = (c, m) => `rgb(${Math.round(c[0] * m)},${Math.round(c[1] * m)},${Math.round(c[2] * m)})`;
|
||||
const grad = ctx.createLinearGradient((c1.x + c2.x) / 2, (c1.y + c2.y) / 2,
|
||||
(c3.x + c4.x) / 2, (c3.y + c4.y) / 2);
|
||||
grad.addColorStop(0, mix(BEAM_MID, 1));
|
||||
grad.addColorStop(1, mix(BEAM_MID, 0.5));
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath(); ctx.moveTo(c1.x, c1.y); ctx.lineTo(c2.x, c2.y); ctx.lineTo(c3.x, c3.y); ctx.lineTo(c4.x, c4.y);
|
||||
ctx.closePath(); ctx.fill();
|
||||
|
||||
// ...and the OUTER LINES. It reads as a plane because its edges are drawn, not merely implied by
|
||||
// where the fill stops.
|
||||
ctx.strokeStyle = 'rgb(196,230,255)'; // the outer lines — width is a knob (beamBorder)
|
||||
ctx.lineWidth = TUNE.beamBorder;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c2.x, c2.y); ctx.lineTo(c3.x, c3.y);
|
||||
ctx.moveTo(c1.x, c1.y); ctx.lineTo(c4.x, c4.y);
|
||||
@@ -667,8 +658,8 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, from) {
|
||||
// The cut line: BLUEISH and faint. Near-white and bold, it read as a HUD marker stamped on the
|
||||
// sprite rather than as light crossing a body — it has to whisper, not label.
|
||||
const CUT_COL = 'rgba(58,175,255,0.85)'; // the laser icon's blue (#1d99e8), lifted so a flat bar reads
|
||||
// The plane's three colours, edge -> core. Opaque: see the gradient in drawBeamWedge.
|
||||
const BEAM_EDGE = [26, 62, 178];
|
||||
// The plane's colour. Opaque, one flat blue, dimmed with distance — see drawBeamWedge. BEAM_CORE is
|
||||
// still used by the band variant's hot centre line (TUNE.beamBand).
|
||||
const BEAM_MID = [63, 116, 255];
|
||||
const BEAM_CORE = [206, 238, 255];
|
||||
const CUT_MAX = 8; // bodies that get a cut line in one frame (nearest first)
|
||||
@@ -732,7 +723,7 @@ function drawBeamCuts(dirX, dirZ, ox, oz, L) {
|
||||
const along = rx * ux + rz * uz;
|
||||
if (along < 0.5 || along > L) continue; // behind him, or past the end
|
||||
const off = Math.abs(rx * uz - rz * ux);
|
||||
const halfW = TUNE.beamBand ? TUNE.beamBandW * 2 // must match fireBeam's test, or the cut
|
||||
const halfW = TUNE.beamBand >= 0.5 ? TUNE.beamBandW * 2 // must match fireBeam's test, or the cut
|
||||
: TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * (along / L); // marks bodies the shot will miss
|
||||
if (off > halfW + 0.8) continue;
|
||||
const gp = proj(zb.x, 0, zb.z), h = zombieH(zb);
|
||||
@@ -1266,7 +1257,7 @@ export function render() {
|
||||
const L = drawBeamWedge(b.ux, b.uz, b.x, b.y, b.z, 1 - k);
|
||||
drawBeamCuts(b.ux, b.uz, b.x, b.z, L);
|
||||
} else if (laserAim) {
|
||||
const L = drawBeamWedge(laserAim.v.vx, laserAim.v.vz, P.x, TUNE.beamOriginY, P.z, 0); // the preview is the same solid plane
|
||||
const L = drawBeamWedge(laserAim.v.vx, laserAim.v.vz, P.x, TUNE.beamY + TUNE.beamOriginLift, P.z, 0); // the preview is the same solid plane
|
||||
drawBeamCuts(laserAim.v.vx, laserAim.v.vz, P.x, P.z, L); // "already while aiming"
|
||||
}
|
||||
if (band && !laserAim) drawAimMark(band, bandU0, flying); // ...now that there is a body to mark
|
||||
|
||||
+2
-2
@@ -304,7 +304,7 @@ export function fireBeam(dirX, dirZ) {
|
||||
const d = Math.hypot(dirX, dirZ) || 1, ux = dirX / d, uz = dirZ / d;
|
||||
// out of the PLAYER at chest height — same point the cone is measured from, so what you see and
|
||||
// what it kills are one geometry
|
||||
G.beam = { x: P.x, y: TUNE.beamOriginY, z: P.z, ux, uz, t: TUNE.beamT };
|
||||
G.beam = { x: P.x, y: TUNE.beamY + TUNE.beamOriginLift, z: P.z, ux, uz, t: TUNE.beamT };
|
||||
boom(0.55); G.shake = { t: 0.18, mag: 3 }; G.boomFlash = 0.08;
|
||||
for (let i = zombies.length - 1; i >= 0; i--) {
|
||||
const zb = zombies[i];
|
||||
@@ -315,7 +315,7 @@ export function fireBeam(dirX, dirZ) {
|
||||
const off = Math.abs(rx * uz - rz * ux); // lateral offset from the lane
|
||||
// the kill follows whichever LOOK is on, or the A/B would be a lie: a narrow band that deletes a
|
||||
// cone-wide crowd tells you nothing about how the narrow band plays
|
||||
const halfW = TUNE.beamBand ? TUNE.beamBandW * 2
|
||||
const halfW = TUNE.beamBand >= 0.5 ? TUNE.beamBandW * 2
|
||||
: TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * (along / TUNE.beamLen);
|
||||
if (off > halfW + hitR(zb)) continue;
|
||||
// THE BEAM DELETES ORDINARY BODIES. hp does not enter into it: a plane of light that left a
|
||||
|
||||
Reference in new issue
Block a user