The body mark was behind the body; the yellow ring was never mine
Two bugs, one report. THE MARK WAS DRAWN UNDER THE SPRITE. The whole band, target mark included, is painted before the sprite pass — which is right for the arc, since a nearer shambler should occlude the part of the flight behind it. But the mark is a mark ON a shambler, so it went down first and the body was then drawn straight over the top of it. It has never once been visible. Split out as drawAimMark and called after the near-side actors, which is the only place a highlight on a body can go. THE YELLOW CIRCLE WAS G.outMarker, not the aim at all. It is the tennis in/out call — "did that land in?" — and splatter never asks: `out` could not be true there, so it was an unconditional yellow ring on the grass carrying no information, sitting under the shambler for 1.3 s. That is longer than the gap before the next pull, which is why it was there "most of the time" and why it looked like the aim marking the feet. Now set only in tennis, from both the serve and the rally swing. Checked: tennis keeps its ring on serves AND rally shots, splatter has none anywhere, and the mark now draws over the torso at every pull strength. Note this leaves a splatter VOLLEY with no landing feedback at all. It had a ring that said nothing before, so nothing was lost, but say if the volley now feels blind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ce857fedc
commit
ff9e09e3eb
2 files changed
+35
-14
No files matched your search
+10
-2
@@ -123,8 +123,14 @@ function doHit(ch, quality, aim) {
|
||||
ball.kills = 0; // fresh shot: multi-kill streak restarts
|
||||
ball.bomb = false; ball.flame = false; // only a consuming serve arms these; any other hit clears them
|
||||
if (ch.side === 'P') { // shot marker: where this will land — red means it's going out
|
||||
const ok = G.mode !== 'tennis' || (Math.abs(tx) <= COURT.W / 2 + 0.02 && tz >= -0.02 && tz <= COURT.netZ);
|
||||
// TENNIS ONLY. It answers "did that land in?", and splatter does not ask: `out` could never be
|
||||
// true there, so it was a permanent yellow ring on the grass carrying no information — and
|
||||
// sitting under the shambler for 1.3 s, which is longer than the gap before you aim again.
|
||||
// That ring, not the new mark, is the circle that seemed to live under their feet.
|
||||
if (G.mode === 'tennis') {
|
||||
const ok = Math.abs(tx) <= COURT.W / 2 + 0.02 && tz >= -0.02 && tz <= COURT.netZ;
|
||||
G.outMarker = { x: tx, z: tz, out: !ok, t: 1.3 };
|
||||
}
|
||||
G.aiReactT = 0; // CPU hasn't read the shot yet
|
||||
}
|
||||
sfx(ch.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded racket strike, per side
|
||||
@@ -257,7 +263,9 @@ function hitServe(sv, quality, aim = null) {
|
||||
}
|
||||
}
|
||||
if (wsel > 0 && slotStock(wsel) <= 0) G.weapon = 0; // that was the last tin — hand back to plain balls
|
||||
if (sv.side === 'P') G.outMarker = { x: boxX, z: tzs, out: G.mode === 'tennis' && !inServeBox(boxX, tzs), t: 1.3 };
|
||||
// tennis only, same reason as doHit's: a splatter serve lands at a shambler's feet, and the ring
|
||||
// sat there in yellow for 1.3 s saying nothing — right where the aim mark is trying to be read.
|
||||
if (sv.side === 'P' && G.mode === 'tennis') G.outMarker = { x: boxX, z: tzs, out: !inServeBox(boxX, tzs), t: 1.3 };
|
||||
G.phase = 'rally'; sfx(sv.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded serve strike, per side
|
||||
sv.pose = 'serve'; sv.poseT = 0;
|
||||
if (quality > 0.999) { G.flash = { text: 'PERFECT', t: 0.7 }; G.hitstop = 0.05; }
|
||||
|
||||
@@ -487,20 +487,29 @@ function drawShotBand(a, u0) {
|
||||
ctx.beginPath(); ctx.moveTo(prev.x, prev.y); ctx.lineTo(p.x, p.y); ctx.stroke();
|
||||
prev = p;
|
||||
}
|
||||
// The mark sits on the BODY, not at his feet. That is where the ball actually strikes (the hit
|
||||
// test is a radius at any height under 1.9 m) and where the wound will be, so a ring on the grass
|
||||
// was pointing at the one part of the shambler the shot has nothing to do with. Filled and washed
|
||||
// out in the band's own colour rather than a hard 2px ring: it marks without looking like a decal
|
||||
// stuck on the lawn.
|
||||
ctx.globalAlpha = 1; ctx.lineWidth = 1;
|
||||
}
|
||||
|
||||
// The target mark, drawn AFTER the bodies — it is a mark ON the shambler, so painting it with the
|
||||
// rest of the band (which happens before the sprite pass, so a nearer body correctly occludes the
|
||||
// arc) hid it completely behind the very thing it was marking. That is why it was never once seen.
|
||||
// It sits on the BODY: that is where the ball actually strikes (the hit test is a radius at any
|
||||
// height under 1.9 m) and where the wound lands, so a ring on the grass pointed at the one part of
|
||||
// a shambler the shot has nothing to do with. Filled and washed out in the band's own colour
|
||||
// rather than a hard 2px ring, so it marks without looking like a decal stuck to the lawn.
|
||||
function drawAimMark(a, u0) {
|
||||
const left = 1 - u0;
|
||||
if (left <= 0.01) return;
|
||||
const col = a.q >= TUNE.zSplatQ ? (a.zt ? UI.hot : UI.warm) : '#9db4d0';
|
||||
const onBody = !!a.zt;
|
||||
const m = proj(a.tx, onBody ? 0.95 : 0, a.tz);
|
||||
ctx.globalAlpha = (onBody ? 0.34 : 0.24) * left;
|
||||
ctx.globalAlpha = (onBody ? 0.42 : 0.24) * left;
|
||||
ctx.fillStyle = col;
|
||||
ctx.beginPath();
|
||||
if (onBody) ctx.ellipse(m.x, m.y, m.s * 0.26, m.s * 0.38, 0, 0, 7); // upright, over the torso
|
||||
else ctx.ellipse(m.x, m.y, m.s * 0.30, m.s * 0.12, 0, 0, 7); // flat, lying on the grass
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1; ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function drawRing(x, z, color) {
|
||||
@@ -938,13 +947,16 @@ export function render() {
|
||||
// Because the arc terminates at the auto-picked target, a weak pull and a full one at the same
|
||||
// shambler draw the SAME curve in different colours — which is the "distance is free" rule made
|
||||
// visible instead of merely documented.
|
||||
if (G.mode === 'splatter' && !G.interlude && (G.phase === 'pretoss' || G.phase === 'toss' || G.phase === 'rally')) {
|
||||
// While pulling: the live preview, whole. After release: the same arc with its tail already
|
||||
// eaten, drawn from where the ball has got to. G.aim wins — a new pull replaces the last shot's
|
||||
// leftovers rather than drawing two bands at once.
|
||||
const band = G.aim ? previewServe(G.aim) : G.trail;
|
||||
if (band) drawShotBand(band, band === G.trail ? band.t / band.T : 0);
|
||||
}
|
||||
// leftovers rather than drawing two bands at once. Held for the frame because the MARK on the
|
||||
// far end has to wait for the bodies to be drawn (see drawAimMark below) and previewServe is a
|
||||
// full ballistic solve — worth doing once.
|
||||
const band = G.mode === 'splatter' && !G.interlude
|
||||
&& (G.phase === 'pretoss' || G.phase === 'toss' || G.phase === 'rally')
|
||||
? (G.aim ? previewServe(G.aim) : G.trail) : null;
|
||||
const bandU0 = band && band === G.trail ? band.t / band.T : 0;
|
||||
if (band) drawShotBand(band, bandU0);
|
||||
|
||||
// far-side drawables in z order (far → near), then the net, then the near side
|
||||
const farSide = [
|
||||
@@ -969,6 +981,7 @@ 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());
|
||||
if (band) drawAimMark(band, bandU0); // ...now that there is a body to mark
|
||||
|
||||
// server idly bouncing the ball while waiting
|
||||
if (G.phase === 'pretoss') {
|
||||
|
||||
Reference in new issue
Block a user