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
@@ -591,33 +591,46 @@ function drawBeamWedge(dirX, dirZ, ox, oy, oz, alpha, from) {
|
||||
if (from >= 1) return;
|
||||
const px = -uz, pz = ux; // the lane's perpendicular, on the ground
|
||||
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
|
||||
// something he fired; the plane starts at racket height and settles onto the court as it goes out,
|
||||
// which is what makes the near end look attached to him.
|
||||
const yAt = u => oy + (0.05 - oy) * Math.min(1, u * 2.2);
|
||||
// It LEAVES THE RACKET as a POINT and opens out. Drawn flat on the lawn it read as a shape lying
|
||||
// at his feet; starting wide it read as a bar hanging in mid-air. Narrow at the strings, settling
|
||||
// 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 * 1.7);
|
||||
const corner = (u, w) => proj(ox + ux * (u * TUNE.beamLen) + px * w, yAt(u),
|
||||
oz + uz * (u * TUNE.beamLen) + pz * w);
|
||||
const u0 = Math.max(0, from), w0 = wAt(u0), w1 = wAt(1);
|
||||
const c1 = corner(u0, -w0), c2 = corner(u0, w0), c3 = corner(1, w1), c4 = corner(1, -w1);
|
||||
const u0 = Math.max(0, from);
|
||||
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
|
||||
// blue read as a pale grey smear on the lawn — the colour is the identity of the shot.
|
||||
const g = ctx.createLinearGradient(c1.x, c1.y, c4.x, c4.y);
|
||||
g.addColorStop(0, `rgba(255,255,255,${0.92 * alpha})`);
|
||||
g.addColorStop(0.14, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${0.88 * alpha})`);
|
||||
g.addColorStop(1, `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${0.46 * alpha})`);
|
||||
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 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.strokeStyle = `rgba(210,232,255,${0.42 * alpha})`; ctx.lineWidth = 1;
|
||||
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); ctx.stroke();
|
||||
// ...and the erase front itself: a bright bar racing away down the lane
|
||||
if (from > 0) {
|
||||
ctx.strokeStyle = `rgba(255,255,255,${0.85 * alpha})`; ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(c1.x, c1.y); ctx.lineTo(c2.x, c2.y); ctx.stroke();
|
||||
// THREE NESTED WEDGES: a wide violet-blue bloom, a cyan body, and a white-hot core down the
|
||||
// middle. One flat quad with a gradient along its length was a coloured shape; layers that share
|
||||
// an axis and get brighter toward the centre are what read as sci-fi light.
|
||||
const layers = [
|
||||
{ k: 1.00, col: [90, 60, 255], a: 0.30 }, // outer bloom, violet
|
||||
{ k: 0.62, col: rgb, a: 0.50 }, // the body, in the weapon's blue
|
||||
{ k: 0.28, col: [130, 235, 255], a: 0.62 }, // inner, cyan
|
||||
{ k: 0.09, col: [255, 255, 255], a: 0.92 }, // the core
|
||||
];
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'lighter'; // light ADDS — this is the whole sci-fi trick
|
||||
for (const L of layers) {
|
||||
const w0 = wAt(u0) * L.k, w1 = wAt(1) * L.k;
|
||||
const c1 = corner(u0, -w0), c2 = corner(u0, w0), c3 = corner(1, w1), c4 = corner(1, -w1);
|
||||
// ...and it fades ALONG its length too, so the far end dissolves into the ads instead of
|
||||
// stopping at a hard edge
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -731,10 +744,6 @@ function drawChar(ch) {
|
||||
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 === '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];
|
||||
drawShadow(p.x, p.y, hpx * 0.22, hpx * 0.06);
|
||||
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); }
|
||||
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
|
||||
const farSide = [
|
||||
{ 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
|
||||
nearSide.push({ z: (G.eatenAt || P).z - 0.12, f: drawDying });
|
||||
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
|
||||
|
||||
// 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();
|
||||
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
|
||||
: pt.pink ? (pt.t > 0.2 ? UI.pink : '#ff9df6') // a barrel bursting: debris, not gore
|
||||
: 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 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
|
||||
ctx.globalAlpha = 0.35; ctx.fillStyle = pe.col || UI.ball;
|
||||
ctx.beginPath(); ctx.arc(pp.x, pp.y, r * 2.1, 0, 7); ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
||||
for (const [rf, af] of [[2.6, 0.20], [1.6, 0.34]]) { // a real bloom, not one flat disc
|
||||
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 = '#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
|
||||
if (G.bossBurst) {
|
||||
|
||||
Reference in new issue
Block a user