Fire that looks like fire, a rack you can leave, and a boss you can read

The band work from f352612 rides along; everything else is this round's list.

- proj returns a NEGATIVE scale behind the camera, so four ellipse call sites
  could be handed negative radii — ctx.ellipse throws on those, and the throw
  killed the rest of the frame (no HUD, no rack, no band). All four now skip
  points behind the camera.
- a burst tin is no longer a commitment: tapping another slot aborts it, spends
  it and hands back the serve loop. Lives in selectWeapon, so the rack tap and
  the 1-0 keys both get it. A horde still locks the rack — that is the one place
  your finger is already spoken for. Spending splices the rack, so the tap
  resolves to a weapon id and re-finds it rather than trusting the index.
- beacon 1.6 -> 2.4 rounds/s: a lob cannon you could not leave AND could not
  feel working was two problems.
- FIRE. Flamer throws fat embers instead of a one-pixel thread; burning bodies
  get nine tongues over a heat bloom plus smoke; the fire ball gets a halo and a
  real tail; an ash death gets twice the motes at twice the life over a ground
  bloom and a column. The flamer had no sfx case at all and was using the gun's
  dry tick — it has its own roar now.
- chapter bosses carry an HP bar over their head. The no-HP-bar rule stands for
  everything else and the comment says why a boss is the exception.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-09 13:21:57 +02:00
1 parent f352612de3
commit 655b86d27f
7 files changed
+184 -31

No files matched your search

+33 -3
View File
@@ -565,7 +565,27 @@ const slotOf = id => { // -1 if you are not carrying it
};
export function selectWeapon(i) {
if (G.interlude) return; // the sudden-death duel is played under tennis rules — plain balls only
// A HORDE is the one place the rack stays locked: your finger is steering the man, the racket
// fires itself, and there is no gesture left to spend on a tin. Everything else can be switched
// out of, including mid-burst — see below.
if (G.horde) return;
if (slotStock(i) <= 0) return; // dud: nothing in that slot (slot 0 is only ever empty if a spent
// A BURST IS NOT A COMMITMENT. Committing to a burst tin used to lock the rack for its whole
// window — eleven seconds of beacon lobs while a late-game crowd walks into you is a death you
// can watch coming and do nothing about, which is exactly what Oliver hit. Tapping another slot
// ABORTS it: the tin is spent (you did fire it) and the serve loop comes straight back, the same
// handover updateZombies does when the window ends on its own. Both input paths land here — the
// rack tap and the 1-0 keys — so the abort lives here rather than in either of them.
if (G.mgT > 0) {
// ...but spending a tin can EMPTY it, and spendSlot splices an empty entry out, which shifts
// every slot above it down one. A tap is at a slot; what you meant was the weapon you could see
// in it. So resolve to the id first and look it up again afterwards — otherwise aborting a
// beacon in slot 1 to reach the rocket in slot 4 hands you whatever slid into slot 4.
const want = slotId(i);
G.mgT = 0; endBurst(); setupPoint();
const j = slotOf(want);
i = j >= 0 ? j : 0; // the tin we just spent WAS the one tapped: fall back to BALL
}
G.weapon = i; // temp has just been cleared, and then it is BALL again)
sfx('weapon-click2.mp3', 0.45); // a real mechanical click, quiet — switching is audible, not loud
}
@@ -1527,9 +1547,19 @@ export function update(dt) {
// --- ball physics
if (ball.active) {
if (ball.flame) parts.push({ // fire trail: one ember per frame, brief and rising
x: ball.x, y: ball.y, z: ball.z, t: 0.22, fire: true,
vx: (Math.random() * 2 - 1) * 0.6, vy: 0.8, vz: (Math.random() * 2 - 1) * 0.6 });
// fire trail. One brief ember a frame read as a spark, not as a ball that is ON FIRE — this is
// the weapon whose whole pitch is "kills on any touch and sets the others alight", so it now
// sheds THREE embers a frame with real spread, they live long enough to hang as a tail behind
// the arc, and every third leaves a puff of smoke drifting off the top of it.
if (ball.flame) {
for (let k = 0; k < 3; k++) parts.push({
x: ball.x + (Math.random() * 2 - 1) * 0.14, y: ball.y + (Math.random() * 2 - 1) * 0.14,
z: ball.z + (Math.random() * 2 - 1) * 0.14, t: 0.34 + Math.random() * 0.26, fire: true,
vx: (Math.random() * 2 - 1) * 0.9, vy: 0.9 + Math.random() * 0.7, vz: (Math.random() * 2 - 1) * 0.9 });
if (Math.random() < 0.34) parts.push({ x: ball.x, y: ball.y + 0.15, z: ball.z,
vx: (Math.random() * 2 - 1) * 0.3, vy: 0.7, vz: (Math.random() * 2 - 1) * 0.3,
t: 0.5 + Math.random() * 0.4, smoke: true });
}
ball.vy -= (ball.g || TUNE.gravity) * dt;
const oldZ = ball.z;
ball.x += ball.vx * dt; ball.y += ball.vy * dt; ball.z += ball.vz * dt;