The trail reads as motion, never as residue: the ghosts fade with the sprint
The lingering doubles were a real bug, not a taste problem: racketAI's !incoming return kept the last chase's vx/vz on the body, so the speed ghosts trailed a zombie that had already stopped. The velocity dies with the chase now (tested). The look is halved with it: ghost alphas 0.22/0.10 -> 0.11/0.05, trailK 0.05 -> 0.035 — a hint of speed in the corner of the eye, never a second body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ea9af5442
commit
98b01fea5a
6 files changed
+22
-5
No files matched your search
@@ -559,6 +559,12 @@ bin ich auf deinen Input angewiesen."* Two cheap effects in the existing registe
|
|||||||
Both are panel-tunable (`trailMin` 0 kills both). Not tried, on the shelf if these fail the
|
Both are panel-tunable (`trailMin` 0 kills both). Not tried, on the shelf if these fail the
|
||||||
phone: a one-frame lean/skid sprite on hard direction changes (needs drawn frames, per the
|
phone: a one-frame lean/skid sprite on hard direction changes (needs drawn frames, per the
|
||||||
draw-frames rule), and court-colored skid streaks on the ground behind a hard stop.
|
draw-frames rule), and court-colored skid streaks on the ground behind a hard stop.
|
||||||
|
**First verdict, same day** (*"dezenter, schneller ausfaden — teilweise bleiben die doppelten
|
||||||
|
ghosts länger sichtbar"*): the lingering was a real bug — racketAI's `!incoming` return kept the
|
||||||
|
last chase's `vx/vz` on the body, so the ghosts trailed a zombie that had already stopped; the
|
||||||
|
velocity now dies with the chase (and a test pins it). The look was then halved with it: ghost
|
||||||
|
alphas 0.22/0.10 -> 0.11/0.05, `trailK` 0.05 -> 0.035. The trail must read as motion, never as
|
||||||
|
residue or a second body.
|
||||||
|
|
||||||
## Session log: THE LOB (BUILT, THEN CUT) AND TWO THINGS THE MULTIPLAYER TEST FOUND (2026-08-22, same day)
|
## Session log: THE LOB (BUILT, THEN CUT) AND TWO THINGS THE MULTIPLAYER TEST FOUND (2026-08-22, same day)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -675,6 +675,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="src/main.js?v=310"></script>
|
<script type="module" src="src/main.js?v=311"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+2
-1
@@ -622,7 +622,8 @@ export const TUNE = {
|
|||||||
// back along the velocity). Procedural register only — dust and afterimage, never painted into
|
// back along the velocity). Procedural register only — dust and afterimage, never painted into
|
||||||
// a sprite, and nothing draws at walking pace: trailMin sits above every shamble.
|
// a sprite, and nothing draws at walking pace: trailMin sits above every shamble.
|
||||||
trailMin: 3.6, // m/s of ground speed before a body earns dust + ghosts (0 = both off)
|
trailMin: 3.6, // m/s of ground speed before a body earns dust + ghosts (0 = both off)
|
||||||
trailK: 0.05, // s each ghost trails behind along the velocity (two samples: 1x and 2x)
|
trailK: 0.035, // s each ghost trails behind along the velocity (two samples: 1x and 2x).
|
||||||
|
// 0.05 on the first cut — "der Effekt sollte dezenter sein": shorter smear
|
||||||
runDustEvery: 0.5, // m of ground covered per dust puff while sprinting
|
runDustEvery: 0.5, // m of ground covered per dust puff while sprinting
|
||||||
fps: 60, // frame cap — 30 halves battery/heat but Oliver felt it; smoothness wins (0 = uncapped)
|
fps: 60, // frame cap — 30 halves battery/heat but Oliver felt it; smoothness wins (0 = uncapped)
|
||||||
lowPower: 1, // >= 0.5 caps RES at 3 (−44% of pixels painted per frame) — the battery saver,
|
lowPower: 1, // >= 0.5 caps RES at 3 (−44% of pixels painted per frame) — the battery saver,
|
||||||
|
|||||||
+4
-1
@@ -1425,7 +1425,10 @@ function racketAI(rz, dt) {
|
|||||||
rz.swingCool -= dt;
|
rz.swingCool -= dt;
|
||||||
const incoming = ball.active && ball.lastHit === 'P' && ball.vz < 0
|
const incoming = ball.active && ball.lastHit === 'P' && ball.vz < 0
|
||||||
&& (G.interlude || (ball.pow || 0) < TUNE.zSplatQ); // in the duel it plays EVERY ball — it's tennis now
|
&& (G.interlude || (ball.pow || 0) < TUNE.zSplatQ); // in the duel it plays EVERY ball — it's tennis now
|
||||||
if (!incoming) { if (rz.pose === 'run') rz.pose = 'idle'; return; }
|
// ...and the sprint velocity dies WITH the chase. This return kept the last chase's vx/vz on
|
||||||
|
// the body, so the speed ghosts trailed a zombie that had already stopped ("teilweise bleiben
|
||||||
|
// die doppelten ghosts länger sichtbar") — the trail must read as motion, never as residue.
|
||||||
|
if (!incoming) { rz.vx = 0; rz.vz = 0; if (rz.pose === 'run') rz.pose = 'idle'; return; }
|
||||||
// ANTICIPATION. He used to run at the ball's LIVE position, and against a serve that is a
|
// ANTICIPATION. He used to run at the ball's LIVE position, and against a serve that is a
|
||||||
// trap: for the first third of the flight the ball is still up by YOUR baseline, so he ran
|
// trap: for the first third of the flight the ball is still up by YOUR baseline, so he ran
|
||||||
// deep and straight while the ball was already leaving sideways, and only started sliding
|
// deep and straight while the ball was already leaving sideways, and only started sliding
|
||||||
|
|||||||
+4
-2
@@ -814,7 +814,9 @@ function drawZombie(zb, ghost) {
|
|||||||
// during a named frame — a collapsing corpse must not smear on stale velocity.
|
// during a named frame — a collapsing corpse must not smear on stale velocity.
|
||||||
if (!ghost && TUNE.trailMin && !zb.showFrame
|
if (!ghost && TUNE.trailMin && !zb.showFrame
|
||||||
&& Math.hypot(zb.vx || 0, zb.vz || 0) > TUNE.trailMin) {
|
&& Math.hypot(zb.vx || 0, zb.vz || 0) > TUNE.trailMin) {
|
||||||
for (const [k, a] of [[2, 0.10], [1, 0.22]]) {
|
// alphas halved from 0.10/0.22 on the first phone verdict ("dezenter"): a hint of speed in
|
||||||
|
// the corner of the eye, never a second body
|
||||||
|
for (const [k, a] of [[2, 0.05], [1, 0.11]]) {
|
||||||
ctx.save(); ctx.globalAlpha *= a;
|
ctx.save(); ctx.globalAlpha *= a;
|
||||||
drawZombie({ ...zb, x: zb.x - zb.vx * TUNE.trailK * k, z: zb.z - (zb.vz || 0) * TUNE.trailK * k }, true);
|
drawZombie({ ...zb, x: zb.x - zb.vx * TUNE.trailK * k, z: zb.z - (zb.vz || 0) * TUNE.trailK * k }, true);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -1801,7 +1803,7 @@ function drawChar(ch, ghost) {
|
|||||||
// MOVEMENT READS: the same two-ghost speed streak the bodies wear (see drawZombie) — gameplay's
|
// MOVEMENT READS: the same two-ghost speed streak the bodies wear (see drawZombie) — gameplay's
|
||||||
// mover writes P.vx/vz, so only a genuine sprint (auto-run chase, horde steer) ever draws one.
|
// mover writes P.vx/vz, so only a genuine sprint (auto-run chase, horde steer) ever draws one.
|
||||||
if (!ghost && TUNE.trailMin && Math.hypot(ch.vx || 0, ch.vz || 0) > TUNE.trailMin) {
|
if (!ghost && TUNE.trailMin && Math.hypot(ch.vx || 0, ch.vz || 0) > TUNE.trailMin) {
|
||||||
for (const [k, a] of [[2, 0.10], [1, 0.22]]) {
|
for (const [k, a] of [[2, 0.05], [1, 0.11]]) { // halved with drawZombie's — one look, both bodies
|
||||||
ctx.save(); ctx.globalAlpha *= a;
|
ctx.save(); ctx.globalAlpha *= a;
|
||||||
drawChar({ ...ch, x: ch.x - ch.vx * TUNE.trailK * k, z: ch.z - ch.vz * TUNE.trailK * k }, true);
|
drawChar({ ...ch, x: ch.x - ch.vx * TUNE.trailK * k, z: ch.z - ch.vz * TUNE.trailK * k }, true);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|||||||
@@ -2102,6 +2102,11 @@ test('THE READ: a habit is learned by repetition and a break wrong-foots the rac
|
|||||||
hit(1); // the break
|
hit(1); // the break
|
||||||
assert.ok(d.surprised && !d.expected, 'the break wrong-foots him for this one flight');
|
assert.ok(d.surprised && !d.expected, 'the break wrong-foots him for this one flight');
|
||||||
assert.equal(d.laneRun, 1, '...and the new lane starts its own book');
|
assert.equal(d.laneRun, 1, '...and the new lane starts its own book');
|
||||||
|
// ...and the sprint dies with the chase: the rig just deactivated the ball, so one tick later
|
||||||
|
// the body carries no velocity — a stale vx kept the speed ghosts on a zombie that had already
|
||||||
|
// stopped ("teilweise bleiben die doppelten ghosts länger sichtbar")
|
||||||
|
update(DT);
|
||||||
|
assert.equal(zb.vx || 0, 0, 'no chase, no velocity — the ghost trail cannot outlive the sprint');
|
||||||
G.modal = null; G.modalQ.length = 0;
|
G.modal = null; G.modalQ.length = 0;
|
||||||
endMatch();
|
endMatch();
|
||||||
// the dials must MATTER at the top and vanish at the bottom — pinned so they cannot drift:
|
// the dials must MATTER at the top and vanish at the bottom — pinned so they cannot drift:
|
||||||
|
|||||||
Reference in new issue
Block a user