Fire the laser as light, and stop him jogging on the spot
The auto-fire jog was one guard. gameplay skipped the pose update in mgSteer mode, so the pose stuck on 'idle' -- and render covered for it by faking a run cycle off G.idleT, which animated whether he was moving or not. That fake is what read as "this hectic run animation... odd all the time". The pose follows MOVEMENT in every mode now and the fake is deleted, so auto-fire, the horde and the melee racket all use the ordinary run animation and standing still is standing still. The comment that justified the fake -- two rackets when idle -- was solving a smaller problem than it caused; at sprite scale the overlap does not read. The laser, second pass, all of it "it must read as light he fired": - it draws AFTER the net and every body. It was drawn before the far-side pass, so the net and the crowd painted over it, which is exactly why it looked like it was lying on the floor and sitting under the net. - beamLen 26 -> 34: at 26 it stopped just past the far baseline and read as cut off in mid-court. It carries into the courtside ads now. - beamW0 0.35 -> 0.06, a POINT at the strings. At 0.35 it left the racket as a 0.7 m bar hanging in mid-air instead of something emitted from it. - it DELETES ordinary bodies, hp irrelevant. A plane of light that left a roller man standing made it a worse ball, not a weapon. A boss is the exception and takes dmgBeam 8 -- severe, survivable. - four nested wedges composited 'lighter' (violet bloom, blue, cyan, white core), each fading along its length. One quad with a gradient was a coloured shape. Rocket: speed 17 -> 38, and a real jet plume laid tight ON the flight line rather than scattered sparks, which looked like a firework. Beacon: a finer one-pixel thread and a much brighter ball, so the BALL is the bright thing and the trail only says where it has been. And the bug in the middle of that: firePellet stamps a whitelist of fields onto the round, and jet/trailR went onto the WEAPONS rows without being added to the list -- so the rocket flew with no plume and nothing errored. Any new pellet field has to be added in both places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46e268973d
commit
91053b4c35
6 files changed
+151
-62
No files matched your search
+38
@@ -320,6 +320,44 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
|
|||||||
frame entirely. `xLim = Math.max(xVis, COURT.W / 2)` — the camera limit, floored at the sideline so
|
frame entirely. `xLim = Math.max(xVis, COURT.W / 2)` — the camera limit, floored at the sideline so
|
||||||
every legal ball stays catchable.
|
every legal ball stays catchable.
|
||||||
|
|
||||||
|
## Session log: the second pass on the barrel weapons (2026-08-03)
|
||||||
|
|
||||||
|
**The auto-fire jog is gone, and it was one guard.** `gameplay` skipped the pose update in `mgSteer`
|
||||||
|
mode (`if (!mgSteer && ...) P.pose = moving ? 'run' : 'idle'`), so the pose stuck on 'idle' — and
|
||||||
|
`render` covered for it by faking a run cycle off `G.idleT`, which animated whether he was moving or
|
||||||
|
not. That fake is what Oliver saw: *"this hectic run animation... looked odd all the time."* The pose
|
||||||
|
follows MOVEMENT in every mode now and the fake is deleted, so auto-fire (and the horde, and the
|
||||||
|
melee racket) uses the ordinary run animation and standing still is standing still. The comment that
|
||||||
|
justified the fake — "the idle sprite has a racket at his hip AND drawMgRacket puts one in his hands,
|
||||||
|
two rackets" — was solving a smaller problem than it caused; at sprite scale the overlap does not read.
|
||||||
|
|
||||||
|
**The laser, second pass.** Four things, all of them "it must read as light he fired":
|
||||||
|
- **It draws AFTER the net and every body.** It was drawn before the far-side pass, so the net and
|
||||||
|
the crowd painted over it — which is precisely why it read as lying on the floor and being *under*
|
||||||
|
the net. Light in the air goes last.
|
||||||
|
- **`beamLen` 26 -> 34.** At 26 it stopped a metre past the far baseline and read as being CUT OFF in
|
||||||
|
mid-court. It now carries into the courtside ads, which is where light should stop.
|
||||||
|
- **`beamW0` 0.35 -> 0.06** — a POINT at the strings. At 0.35 it left the racket as a 0.7 m bar
|
||||||
|
hanging in mid-air rather than as something emitted.
|
||||||
|
- **It DELETES ordinary bodies**, hp irrelevant (`killZombie`, not a wound) — a plane of light that
|
||||||
|
left a roller man standing made it a worse ball, not a weapon. A BOSS is the exception and takes
|
||||||
|
`dmgBeam` 8: "they also must suffer severely" but they survive.
|
||||||
|
- **The look is four nested wedges composited `lighter`** — violet bloom, the weapon's blue, cyan,
|
||||||
|
white core — each fading along its length. One quad with a gradient was a coloured shape; layers
|
||||||
|
sharing an axis and brightening toward the centre is what reads as sci-fi.
|
||||||
|
|
||||||
|
**Rocket:** speed 17 -> 38 and a real JET plume. The plume is laid down tight ON the flight line
|
||||||
|
(`jet`, spread along this frame's step, near-zero velocity, additive hot-white -> orange -> grey)
|
||||||
|
instead of scattered sparks with their own velocities, which looked like a firework.
|
||||||
|
|
||||||
|
**Beacon:** a finer trail (`trail` 1 at `trailR` 0.035, one-pixel motes) and a much brighter ball
|
||||||
|
(`glow` 3.2, a two-ring additive bloom and a fat white core), so the BALL is the bright thing and the
|
||||||
|
trail only says where it has been.
|
||||||
|
|
||||||
|
**The bug in the middle of all that:** `firePellet` stamps a whitelist of fields onto the round, and
|
||||||
|
`jet`/`trailR` were added to the WEAPONS rows without being added to that list — so the rocket flew
|
||||||
|
with no plume at all and nothing errored. **Any new pellet field has to be added in both places.**
|
||||||
|
|
||||||
## TEST MODE — triple-tap the scoreboard
|
## TEST MODE — triple-tap the scoreboard
|
||||||
|
|
||||||
A sandbox for trying new content without playing twenty games to reach it. Three taps on the
|
A sandbox for trying new content without playing twenty games to reach it. Three taps on the
|
||||||
|
|||||||
@@ -240,11 +240,15 @@ export const TUNE = {
|
|||||||
testLevel: 20, // the roster level pickVariant pretends to be at, so every body type can turn up
|
testLevel: 20, // the roster level pickVariant pretends to be at, so every body type can turn up
|
||||||
testBarrelEvery: 14,// s between barrels, so the pickup itself is testable too
|
testBarrelEvery: 14,// s between barrels, so the pickup itself is testable too
|
||||||
// ---- THE LASER's wedge: aim and release like a serve, and a plane of light widens down the lane
|
// ---- THE LASER's wedge: aim and release like a serve, and a plane of light widens down the lane
|
||||||
beamLen: 26, // m it reaches — the whole court, so a lane you aim is a lane you clear
|
beamLen: 34, // m it reaches. Was 26, which stopped at the far baseline and read as the beam
|
||||||
beamW0: 0.35, // m half-width at the racket...
|
// being CUT OFF in mid-court; the player stands at z~24, so this carries it past
|
||||||
beamW1: 4.2, // ...and at the far end. It is a CONE: the spread is the point of it
|
// the baseline and up into the courtside ads, which is where light should stop.
|
||||||
|
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
|
||||||
beamT: 0.42, // s the wedge stays drawn
|
beamT: 0.42, // s the wedge stays drawn
|
||||||
dmgBeam: 3, // hp the beam takes off anything it catches (dmgBomb is 3, and it reads the same)
|
dmgBeam: 8, // what it takes off a BOSS. An ordinary body is not damaged by the beam, it is
|
||||||
|
// DELETED by it (see fireBeam); bosses "must suffer severely" but survive.
|
||||||
// ---- the weapon barrel: the only way to get the four weapons that are not on the duel ladder
|
// ---- the weapon barrel: the only way to get the four weapons that are not on the duel ladder
|
||||||
barrelEvery: 3, // games between barrels being rolled on (like the ball machine's machEvery)
|
barrelEvery: 3, // games between barrels being rolled on (like the ball machine's machEvery)
|
||||||
barrelRoll: 2.6, // m/s it trundles across the far court — fast enough to have to be led
|
barrelRoll: 2.6, // m/s it trundles across the far court — fast enough to have to be led
|
||||||
@@ -460,7 +464,7 @@ export const WEAPONS = {
|
|||||||
// A LOBBED tennis ball twice the weight of yours, thrown on its own beat, changing colour every
|
// A LOBBED tennis ball twice the weight of yours, thrown on its own beat, changing colour every
|
||||||
// shot and dragging its colour behind it. The showiest thing in the game and the one you meet first.
|
// shot and dragging its colour behind it. The showiest thing in the game and the one you meet first.
|
||||||
beacon: { label: 'BEACON', icon: 'w_beacon', tin: 1, kind: 'burst', len: 11, rate: 1.6,
|
beacon: { label: 'BEACON', icon: 'w_beacon', tin: 1, kind: 'burst', len: 11, rate: 1.6,
|
||||||
r: 1.2, dmg: 2, hp: 4, arc: 1, hue: 1, trail: 3, glow: 1.6, speed: 19,
|
r: 1.2, dmg: 2, hp: 4, arc: 1, hue: 1, trail: 1, trailR: 0.035, glow: 3.2, speed: 19,
|
||||||
desc: ['IT THROWS ITSELF', 'TWICE THE BALL, EVERY COLOUR'] },
|
desc: ['IT THROWS ITSELF', 'TWICE THE BALL, EVERY COLOUR'] },
|
||||||
// "make this one way more pronounced": twice the rounds a second, a wider and longer cone, fat
|
// "make this one way more pronounced": twice the rounds a second, a wider and longer cone, fat
|
||||||
// glowing rounds that throw their own embers, and every one of them lights what it touches.
|
// glowing rounds that throw their own embers, and every one of them lights what it touches.
|
||||||
@@ -478,9 +482,9 @@ export const WEAPONS = {
|
|||||||
desc: ['AIM AND RELEASE', 'A WIDENING WALL OF LIGHT'] },
|
desc: ['AIM AND RELEASE', 'A WIDENING WALL OF LIGHT'] },
|
||||||
// Lobbed, self-throwing, and it detonates like a bomb where it lands. Deliberately a LATE pickup
|
// Lobbed, self-throwing, and it detonates like a bomb where it lands. Deliberately a LATE pickup
|
||||||
// (see pickBarrelWeapon) — Oliver: "best later in the game when there is really a lot going on".
|
// (see pickBarrelWeapon) — Oliver: "best later in the game when there is really a lot going on".
|
||||||
rocket: { label: 'ROCKET', icon: 'w_rocket', tin: 1, kind: 'burst', len: 9, rate: 0.85,
|
rocket: { label: 'ROCKET', icon: 'w_rocket', tin: 1, kind: 'burst', len: 9, rate: 1.1,
|
||||||
r: 0.75, dmg: 3, hp: 9, arc: 1, boom: true, trail: 2, speed: 17,
|
r: 0.75, dmg: 3, hp: 9, arc: 1, boom: true, jet: 3, speed: 38,
|
||||||
col: '#ffd050', glow: 1.4,
|
col: '#ffd050', glow: 1.8,
|
||||||
desc: ['IT LAUNCHES ITSELF', 'AND EVERY ONE IS A BOMB'] },
|
desc: ['IT LAUNCHES ITSELF', 'AND EVERY ONE IS A BOMB'] },
|
||||||
};
|
};
|
||||||
// What a pink barrel can be carrying — the four above, which you cannot win any other way. NOT a
|
// What a pink barrel can be carrying — the four above, which you cannot win any other way. NOT a
|
||||||
|
|||||||
+5
-1
@@ -1204,7 +1204,11 @@ export function update(dt) {
|
|||||||
P.z = Math.max(P_BOUNDS.zMin, Math.min(P_BOUNDS.zMax, P.z));
|
P.z = Math.max(P_BOUNDS.zMin, Math.min(P_BOUNDS.zMax, P.z));
|
||||||
const moving = mx || mz || assist !== 0;
|
const moving = mx || mz || assist !== 0;
|
||||||
P.runT += moving ? dt : 0;
|
P.runT += moving ? dt : 0;
|
||||||
if (!mgSteer && P.pose !== 'swing' && P.pose !== 'serve') P.pose = moving ? 'run' : 'idle';
|
// Auto-fire used to be excluded here, which left the pose stuck on 'idle' — and render then
|
||||||
|
// faked a run cycle off G.idleT so he was not a statue. That fake is what read as "this hectic
|
||||||
|
// run animation" whether he was moving or not. The pose follows MOVEMENT in every mode now, so
|
||||||
|
// auto-fire uses the ordinary run animation and standing still is standing still.
|
||||||
|
if (P.pose !== 'swing' && P.pose !== 'serve') P.pose = moving ? 'run' : 'idle';
|
||||||
if (mx) P.facing = mx > 0 ? 1 : -1;
|
if (mx) P.facing = mx > 0 ? 1 : -1;
|
||||||
else if (Math.abs(assist) > 0.25) P.facing = assist > 0 ? 1 : -1; // hysteresis: small corrections don't flip facing
|
else if (Math.abs(assist) > 0.25) P.facing = assist > 0 ? 1 : -1; // hysteresis: small corrections don't flip facing
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -179,6 +179,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js?v=100"></script>
|
<script type="module" src="main.js?v=101"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -591,33 +591,46 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, alpha, from) {
|
|||||||
if (from >= 1) return;
|
if (from >= 1) return;
|
||||||
const px = -uz, pz = ux; // the lane's perpendicular, on the ground
|
const px = -uz, pz = ux; // the lane's perpendicular, on the ground
|
||||||
const wAt = u => TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * u;
|
const wAt = u => TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * u;
|
||||||
// It LEAVES THE RACKET. Drawn flat on the lawn it read as a shape lying at his feet rather than as
|
// It LEAVES THE RACKET as a POINT and opens out. Drawn flat on the lawn it read as a shape lying
|
||||||
// something he fired; the plane starts at racket height and settles onto the court as it goes out,
|
// at his feet; starting wide it read as a bar hanging in mid-air. Narrow at the strings, settling
|
||||||
// which is what makes the near end look attached to him.
|
// onto the court as it goes, is what makes the near end look attached to him.
|
||||||
const yAt = u => oy + (0.05 - oy) * Math.min(1, u * 2.2);
|
const yAt = u => oy + (0.05 - oy) * Math.min(1, u * 1.7);
|
||||||
const corner = (u, w) => proj(ox + ux * (u * TUNE.beamLen) + px * w, yAt(u),
|
const corner = (u, w) => proj(ox + ux * (u * TUNE.beamLen) + px * w, yAt(u),
|
||||||
oz + uz * (u * TUNE.beamLen) + pz * w);
|
oz + uz * (u * TUNE.beamLen) + pz * w);
|
||||||
const u0 = Math.max(0, from), w0 = wAt(u0), w1 = wAt(1);
|
const u0 = Math.max(0, from);
|
||||||
const c1 = corner(u0, -w0), c2 = corner(u0, w0), c3 = corner(1, w1), c4 = corner(1, -w1);
|
|
||||||
const rgb = RGB(WEAPONS.laser.col);
|
const rgb = RGB(WEAPONS.laser.col);
|
||||||
// White only right at the near edge; BLUE for the length of it. A gradient that ran white -> faint
|
// THREE NESTED WEDGES: a wide violet-blue bloom, a cyan body, and a white-hot core down the
|
||||||
// blue read as a pale grey smear on the lawn — the colour is the identity of the shot.
|
// middle. One flat quad with a gradient along its length was a coloured shape; layers that share
|
||||||
const g = ctx.createLinearGradient(c1.x, c1.y, c4.x, c4.y);
|
// an axis and get brighter toward the centre are what read as sci-fi light.
|
||||||
g.addColorStop(0, `rgba(255,255,255,${0.92 * alpha})`);
|
const layers = [
|
||||||
g.addColorStop(0.14, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${0.88 * alpha})`);
|
{ k: 1.00, col: [90, 60, 255], a: 0.30 }, // outer bloom, violet
|
||||||
g.addColorStop(1, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${0.46 * alpha})`);
|
{ k: 0.62, col: rgb, a: 0.50 }, // the body, in the weapon's blue
|
||||||
ctx.fillStyle = g;
|
{ k: 0.28, col: [130, 235, 255], a: 0.62 }, // inner, cyan
|
||||||
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.fill();
|
{ k: 0.09, col: [255, 255, 255], a: 0.92 }, // the core
|
||||||
// the two burning edges: HAIRLINES. At 2.5px they were heavier than anything else on the court and
|
];
|
||||||
// read as a drawn outline rather than as light — "the borders of the laser are quite big".
|
ctx.save();
|
||||||
ctx.strokeStyle = `rgba(210,232,255,${0.42 * alpha})`; ctx.lineWidth = 1;
|
ctx.globalCompositeOperation = 'lighter'; // light ADDS — this is the whole sci-fi trick
|
||||||
ctx.beginPath(); ctx.moveTo(c2.x, c2.y); ctx.lineTo(c3.x, c3.y);
|
for (const L of layers) {
|
||||||
ctx.moveTo(c1.x, c1.y); ctx.lineTo(c4.x, c4.y); ctx.stroke();
|
const w0 = wAt(u0) * L.k, w1 = wAt(1) * L.k;
|
||||||
// ...and the erase front itself: a bright bar racing away down the lane
|
const c1 = corner(u0, -w0), c2 = corner(u0, w0), c3 = corner(1, w1), c4 = corner(1, -w1);
|
||||||
if (from > 0) {
|
// ...and it fades ALONG its length too, so the far end dissolves into the ads instead of
|
||||||
ctx.strokeStyle = `rgba(255,255,255,${0.85 * alpha})`; ctx.lineWidth = 1.5;
|
// stopping at a hard edge
|
||||||
ctx.beginPath(); ctx.moveTo(c1.x, c1.y); ctx.lineTo(c2.x, c2.y); ctx.stroke();
|
const g = ctx.createLinearGradient((c1.x + c2.x) / 2, (c1.y + c2.y) / 2,
|
||||||
|
(c3.x + c4.x) / 2, (c3.y + c4.y) / 2);
|
||||||
|
g.addColorStop(0, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},${L.a * alpha})`);
|
||||||
|
g.addColorStop(0.55, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},${L.a * 0.75 * alpha})`);
|
||||||
|
g.addColorStop(1, `rgba(${L.col[0]},${L.col[1]},${L.col[2]},0)`);
|
||||||
|
ctx.fillStyle = g;
|
||||||
|
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.fill();
|
||||||
}
|
}
|
||||||
|
// the erase front: a bright bar racing away down the lane
|
||||||
|
if (from > 0) {
|
||||||
|
const wf = wAt(u0);
|
||||||
|
const f1 = corner(u0, -wf), f2 = corner(u0, wf);
|
||||||
|
ctx.strokeStyle = `rgba(255,255,255,${0.9 * alpha})`; ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath(); ctx.moveTo(f1.x, f1.y); ctx.lineTo(f2.x, f2.y); ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,10 +744,6 @@ function drawChar(ch) {
|
|||||||
if (ch.pose === 'swing') name = pre + (ch.poseT < 0.15 ? 'swing1' : 'swing2');
|
if (ch.pose === 'swing') name = pre + (ch.poseT < 0.15 ? 'swing1' : 'swing2');
|
||||||
if (ch.pose === 'serve') name = pre + (G.phase === 'toss' ? 'toss' : 'smash');
|
if (ch.pose === 'serve') name = pre + (G.phase === 'toss' ? 'toss' : 'smash');
|
||||||
if (ch.pose === 'run') p.y -= Math.floor(ch.runT * 18) % 2; // 1px run bob
|
if (ch.pose === 'run') p.y -= Math.floor(ch.runT * 18) % 2; // 1px run bob
|
||||||
// Auto-fire: the idle sprite carries a racket at his hip AND drawMgRacket puts one in his hands —
|
|
||||||
// two rackets. The run frames are the racket-less art, so he jogs on the spot instead.
|
|
||||||
if (ch.side === 'P' && ch.pose === 'idle' && G.mode === 'splatter' && (G.horde || G.mgT > 0))
|
|
||||||
name = pre + 'run' + (Math.floor(G.idleT * 3 * runN) % runN + 1);
|
|
||||||
const img = SPR[name];
|
const img = SPR[name];
|
||||||
drawShadow(p.x, p.y, hpx * 0.22, hpx * 0.06);
|
drawShadow(p.x, p.y, hpx * 0.22, hpx * 0.06);
|
||||||
if (img) {
|
if (img) {
|
||||||
@@ -1104,15 +1113,6 @@ export function render() {
|
|||||||
if (laserAim) { const c = dragContact(); drawBeamWedge(laserAim.v.vx, laserAim.v.vz, c.x, c.y, c.z, 0.34, 0); }
|
if (laserAim) { const c = dragContact(); drawBeamWedge(laserAim.v.vx, laserAim.v.vz, c.x, c.y, c.z, 0.34, 0); }
|
||||||
else if (band) drawShotBand(band, bandU0, flying);
|
else if (band) drawShotBand(band, bandU0, flying);
|
||||||
|
|
||||||
// THE LASER's fired wedge, ERASING ALONG ITS OWN PATH: the whole plane is there at the flash and
|
|
||||||
// then it is wiped away from the racket outward, so the shot reads as the energy travelling down
|
|
||||||
// the lane rather than as a decal fading out where it lay. Oliver: "the release could erase the
|
|
||||||
// laserplane along the path."
|
|
||||||
if (G.beam) {
|
|
||||||
const b = G.beam, k = Math.max(0, b.t / TUNE.beamT); // 1 at the flash, 0 as it dies
|
|
||||||
drawBeamWedge(b.ux, b.uz, b.x, b.y, b.z, 1, 1 - k);
|
|
||||||
}
|
|
||||||
|
|
||||||
// far-side drawables in z order (far → near), then the net, then the near side
|
// far-side drawables in z order (far → near), then the net, then the near side
|
||||||
const farSide = [
|
const farSide = [
|
||||||
{ z: COURT.netZ - 0.01, f: () => drawStatic('x_ump', UMP_X, COURT.netZ, UMP_H) },
|
{ z: COURT.netZ - 0.01, f: () => drawStatic('x_ump', UMP_X, COURT.netZ, UMP_H) },
|
||||||
@@ -1136,6 +1136,13 @@ export function render() {
|
|||||||
if (G.mode === 'eaten' && G.eatenT < EATEN_BURST) // the dying man takes his place in the queue
|
if (G.mode === 'eaten' && G.eatenT < EATEN_BURST) // the dying man takes his place in the queue
|
||||||
nearSide.push({ z: (G.eatenAt || P).z - 0.12, f: drawDying });
|
nearSide.push({ z: (G.eatenAt || P).z - 0.12, f: drawDying });
|
||||||
nearSide.sort((a, b) => a.z - b.z).forEach(d => d.f());
|
nearSide.sort((a, b) => a.z - b.z).forEach(d => d.f());
|
||||||
|
// THE LASER's fired wedge, ERASING ALONG ITS OWN PATH. Drawn HERE — after the net and after every
|
||||||
|
// body — because it is light in the air, not a decal on the grass. Drawn before them it went
|
||||||
|
// UNDER the net and behind the crowd, which is exactly why it read as lying on the floor.
|
||||||
|
if (G.beam) {
|
||||||
|
const b = G.beam, k = Math.max(0, b.t / TUNE.beamT); // 1 at the flash, 0 as it dies
|
||||||
|
drawBeamWedge(b.ux, b.uz, b.x, b.y, b.z, 1, 1 - k);
|
||||||
|
}
|
||||||
if (band && !laserAim) drawAimMark(band, bandU0, flying); // ...now that there is a body to mark
|
if (band && !laserAim) drawAimMark(band, bandU0, flying); // ...now that there is a body to mark
|
||||||
|
|
||||||
// server idly bouncing the ball while waiting
|
// server idly bouncing the ball while waiting
|
||||||
@@ -1174,6 +1181,22 @@ export function render() {
|
|||||||
ctx.beginPath(); ctx.arc(pp.x | 0, pp.y | 0, r, 0, 7); ctx.fill();
|
ctx.beginPath(); ctx.arc(pp.x | 0, pp.y | 0, r, 0, 7); ctx.fill();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// a ROCKET's exhaust: a soft round puff that fades hot-white -> orange -> grey, drawn additively
|
||||||
|
// so the plume glows instead of stippling. This is the difference between a motor and a firework.
|
||||||
|
if (pt.jet) {
|
||||||
|
const age = Math.min(1, pt.t / 0.6), r = Math.max(1.2, pp.s * 0.028 * (1.8 - age));
|
||||||
|
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
||||||
|
ctx.fillStyle = age > 0.72 ? `rgba(255,246,214,${age})`
|
||||||
|
: age > 0.38 ? `rgba(255,170,60,${age * 0.8})` : `rgba(120,110,110,${age * 0.5})`;
|
||||||
|
ctx.beginPath(); ctx.arc(pp.x, pp.y, r, 0, 7); ctx.fill(); ctx.restore();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pt.fine) { // the beacon's thread: ONE pixel, so the ball stays the bright thing
|
||||||
|
ctx.globalAlpha = Math.min(1, pt.t * 4);
|
||||||
|
ctx.fillStyle = pt.spark; ctx.fillRect(pp.x | 0, pp.y | 0, 1, 1);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ctx.fillStyle = pt.spark ? pt.spark // a thrown round dragging its colour
|
ctx.fillStyle = pt.spark ? pt.spark // a thrown round dragging its colour
|
||||||
: pt.pink ? (pt.t > 0.2 ? UI.pink : '#ff9df6') // a barrel bursting: debris, not gore
|
: pt.pink ? (pt.t > 0.2 ? UI.pink : '#ff9df6') // a barrel bursting: debris, not gore
|
||||||
: pt.fire ? (pt.t > 0.12 ? '#ffc030' : '#ff8010')
|
: pt.fire ? (pt.t > 0.12 ? '#ffc030' : '#ff8010')
|
||||||
@@ -1192,12 +1215,18 @@ export function render() {
|
|||||||
const sh = proj(pe.x, 0, pe.z); drawShadow(sh.x, sh.y, Math.max(1, sh.s * 0.04 * sc), Math.max(1, sh.s * 0.02 * sc), 0.25);
|
const sh = proj(pe.x, 0, pe.z); drawShadow(sh.x, sh.y, Math.max(1, sh.s * 0.04 * sc), Math.max(1, sh.s * 0.02 * sc), 0.25);
|
||||||
const pp = proj(pe.x, pe.y, pe.z), r = Math.max(1, pp.s * 0.05 * sc);
|
const pp = proj(pe.x, pe.y, pe.z), r = Math.max(1, pp.s * 0.05 * sc);
|
||||||
if (pe.glow) { // the exotic rounds carry their own light, the plain pellet does not
|
if (pe.glow) { // the exotic rounds carry their own light, the plain pellet does not
|
||||||
ctx.globalAlpha = 0.35; ctx.fillStyle = pe.col || UI.ball;
|
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
||||||
ctx.beginPath(); ctx.arc(pp.x, pp.y, r * 2.1, 0, 7); ctx.fill();
|
for (const [rf, af] of [[2.6, 0.20], [1.6, 0.34]]) { // a real bloom, not one flat disc
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = af * Math.min(1.6, pe.glow);
|
||||||
|
ctx.fillStyle = pe.col || UI.ball;
|
||||||
|
ctx.beginPath(); ctx.arc(pp.x, pp.y, r * rf, 0, 7); ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore(); ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
ctx.fillStyle = pe.col || UI.ball; ctx.beginPath(); ctx.arc(pp.x, pp.y, r, 0, 7); ctx.fill();
|
ctx.fillStyle = pe.col || UI.ball; ctx.beginPath(); ctx.arc(pp.x, pp.y, r, 0, 7); ctx.fill();
|
||||||
ctx.fillStyle = '#fffbe0'; ctx.beginPath(); ctx.arc(pp.x, pp.y, Math.max(0.5, r * 0.45), 0, 7); ctx.fill(); // hot core: it reads sharp, not soft
|
// a WHITE-HOT centre, bigger on a glowing round: the ball itself should be the brightest thing
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.beginPath(); ctx.arc(pp.x, pp.y, Math.max(0.6, r * (pe.glow ? 0.62 : 0.45)), 0, 7); ctx.fill();
|
||||||
}
|
}
|
||||||
// the chapter boss's shockwave: a ring of blood mist blown out through the crowd noise
|
// the chapter boss's shockwave: a ring of blood mist blown out through the crowd noise
|
||||||
if (G.bossBurst) {
|
if (G.bossBurst) {
|
||||||
|
|||||||
+26
-12
@@ -314,10 +314,11 @@ export function fireBeam(dirX, dirZ) {
|
|||||||
const off = Math.abs(rx * uz - rz * ux); // lateral offset from the lane
|
const off = Math.abs(rx * uz - rz * ux); // lateral offset from the lane
|
||||||
const halfW = TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * (along / TUNE.beamLen);
|
const halfW = TUNE.beamW0 + (TUNE.beamW1 - TUNE.beamW0) * (along / TUNE.beamLen);
|
||||||
if (off > halfW + hitR(zb)) continue;
|
if (off > halfW + hitR(zb)) continue;
|
||||||
|
// THE BEAM DELETES ORDINARY BODIES. hp does not enter into it: a plane of light that left a
|
||||||
|
// roller man standing was the thing that made it feel like a worse ball rather than a weapon.
|
||||||
|
// A BOSS is the exception and takes a heavy wound instead — "they also must suffer severely".
|
||||||
if (isBoss(zb)) { if (wound(zb, TUNE.dmgBeam)) killZombie(i, true); }
|
if (isBoss(zb)) { if (wound(zb, TUNE.dmgBeam)) killZombie(i, true); }
|
||||||
else if (zb.variant === 'o') killZombie(i, true); // it pops a barrel too, which is fair
|
else killZombie(i, !!G.horde);
|
||||||
else if ((zb.hpMax || 0) > 1 && (zb.hp -= TUNE.dmgBeam) > 0) woundBurst(zb);
|
|
||||||
else splatter(i);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,7 +574,7 @@ export function firePellet(w = WEAPONS.gun) {
|
|||||||
const pe = { x: P.x, y: 0.9, z: P.z, vx: Math.sin(a) * s, vz: -Math.cos(a) * s,
|
const pe = { x: P.x, y: 0.9, z: P.z, vx: Math.sin(a) * s, vz: -Math.cos(a) * s,
|
||||||
t: w.life ?? 1.2, r: w.r ?? TUNE.mgPelletR, dmg: w.dmg ?? TUNE.dmgPellet, hp: w.hp ?? TUNE.dmgPelletHp,
|
t: w.life ?? 1.2, r: w.r ?? TUNE.mgPelletR, dmg: w.dmg ?? TUNE.dmgPellet, hp: w.hp ?? TUNE.dmgPelletHp,
|
||||||
pierce: !!w.pierce, boom: !!w.boom, ignite: w.ignite ? (w.igniteR ?? 1.8) : 0,
|
pierce: !!w.pierce, boom: !!w.boom, ignite: w.ignite ? (w.igniteR ?? 1.8) : 0,
|
||||||
trail: w.trail || 0, col: w.col, glow: w.glow };
|
trail: w.trail || 0, trailR: w.trailR, jet: w.jet || 0, col: w.col, glow: w.glow };
|
||||||
// A LOBBED round: it is thrown, not sprayed. Solved so it comes down about where the crowd is, so
|
// A LOBBED round: it is thrown, not sprayed. Solved so it comes down about where the crowd is, so
|
||||||
// "it throws itself" reads as aiming rather than as a flat stream that happens to be fat.
|
// "it throws itself" reads as aiming rather than as a flat stream that happens to be fat.
|
||||||
if (w.arc) {
|
if (w.arc) {
|
||||||
@@ -952,15 +953,28 @@ export function updateZombies(dt) {
|
|||||||
pe.vy -= pe.g * dt; pe.y += pe.vy * dt;
|
pe.vy -= pe.g * dt; pe.y += pe.vy * dt;
|
||||||
if (pe.y <= 0.05) { pe.y = 0.05; landed = true; }
|
if (pe.y <= 0.05) { pe.y = 0.05; landed = true; }
|
||||||
}
|
}
|
||||||
// a colour dragged behind it — the beacon and the rocket are supposed to be seen from across
|
// A JET PLUME, not a shower of sparks: the exhaust is laid down tight on the flight line,
|
||||||
// the court, and this is what makes a lobbed round read as a THROWN object rather than a dot
|
// barely moving, so it reads as a rocket motor drawing a line through the air. Scattered
|
||||||
if (pe.trail && parts.length < 220)
|
// sparks with their own velocities looked like a firework, which is not what a rocket does.
|
||||||
|
if (pe.jet && parts.length < 240)
|
||||||
|
for (let k = 0; k < pe.jet; k++) {
|
||||||
|
const b = (k + Math.random()) / pe.jet; // spread ALONG this frame's step
|
||||||
|
parts.push({
|
||||||
|
x: pe.x - pe.vx * dt * b + (Math.random() * 2 - 1) * 0.05,
|
||||||
|
y: pe.y - (pe.vy || 0) * dt * b + (Math.random() * 2 - 1) * 0.05,
|
||||||
|
z: pe.z - pe.vz * dt * b + (Math.random() * 2 - 1) * 0.05,
|
||||||
|
vx: 0, vy: 0.25, vz: 0, // it hangs and lifts, it does not fly off
|
||||||
|
t: 0.30 + Math.random() * 0.30, jet: 1, spark: '#fff2c0' });
|
||||||
|
}
|
||||||
|
// ...and a FINE thread for the beacon: one small mote a frame, tight to the ball, so the ball
|
||||||
|
// itself is the bright thing and the trail only says where it has been.
|
||||||
|
if (pe.trail && parts.length < 240)
|
||||||
for (let k = 0; k < pe.trail; k++) parts.push({
|
for (let k = 0; k < pe.trail; k++) parts.push({
|
||||||
x: pe.x + (Math.random() * 2 - 1) * 0.12, y: pe.y + (Math.random() * 2 - 1) * 0.12,
|
x: pe.x + (Math.random() * 2 - 1) * (pe.trailR ?? 0.12),
|
||||||
z: pe.z + (Math.random() * 2 - 1) * 0.12,
|
y: pe.y + (Math.random() * 2 - 1) * (pe.trailR ?? 0.12),
|
||||||
vx: (Math.random() * 2 - 1) * 0.5, vy: (Math.random() * 2 - 1) * 0.5 + 0.3,
|
z: pe.z + (Math.random() * 2 - 1) * (pe.trailR ?? 0.12),
|
||||||
vz: (Math.random() * 2 - 1) * 0.5,
|
vx: 0, vy: 0.12, vz: 0,
|
||||||
t: 0.18 + Math.random() * 0.22, spark: pe.col || '#fff' });
|
t: 0.16 + Math.random() * 0.16, fine: 1, spark: pe.col || '#fff' });
|
||||||
for (let j = zombies.length - 1; j >= 0; j--) {
|
for (let j = zombies.length - 1; j >= 0; j--) {
|
||||||
const zb = zombies[j];
|
const zb = zombies[j];
|
||||||
if (!zb) continue;
|
if (!zb) continue;
|
||||||
|
|||||||
Reference in new issue
Block a user