benjivers/val Public Public: anyone can read this; the crew can change it.
The camera shook at spawn because it was drawn like somebody else
Reported from play: 'when i spawn in the screen is all shakey'. The buy phase turns prediction off on purpose — the spawn pen is a match rule the browser does not know, and a guess that ignored it would spend the phase being dragged back through a door. But with prediction off, your own body fell through to the path written for OTHER people, and collected both of its treatments: drawn at now-minus-the-interpolation-delay, so the camera sat behind your own mouse, and run through the arrival slide, which treats a body it has not seen before as someone walking into view and slides it in from where it last saw them. On a respawn that is the camera sliding across the room, every round, while the mouse says otherwise. Your own body is now drawn from the newest word about it and nothing else whenever it is not predicted. Other bodies keep both treatments, which is where they belong.
2 files
changed +26
−0
src/client.test.ts +18
| @@ -451,3 +451,21 @@ test("a snapshot with no seat in it draws the watching view instead of throwing" | ||
| 451 | 451 | runFrame(); |
| 452 | 452 | expect(String(nodes.get("prompt")!.textContent)).toContain("cookie"); |
| 453 | 453 | }); |
| 454 | ||
| 455 | test("your own camera is never drawn as a stranger arriving", async () => { | |
| 456 | // At spawn the buy phase turns prediction off, and the frame used to fall | |
| 457 | // through to the treatments meant for other people: drawn in the past, and | |
| 458 | // slid in from where it was last seen. On a respawn that slides the CAMERA | |
| 459 | // across the room — the shake somebody reported the first time they played | |
| 460 | // a round from the start. | |
| 461 | const { spectatorSnapshot } = await import("./arena.ts"); | |
| 462 | expect(typeof spectatorSnapshot).toBe("function"); | |
| 463 | const bundle = await Bun.file(new URL("../static/val.js", import.meta.url)).text(); | |
| 464 | // The rule is small enough to state: own id short-circuits before both. | |
| 465 | const look = bundle.slice(bundle.indexOf("const look = (p) =>")); | |
| 466 | const body = look.slice(0, look.indexOf("};") + 2); | |
| 467 | const ownReturn = body.indexOf("if (p.id === snap.me.id) return p;"); | |
| 468 | const slideCall = body.indexOf("slide(presence"); | |
| 469 | expect(ownReturn).toBeGreaterThan(-1); | |
| 470 | expect(slideCall).toBeGreaterThan(ownReturn); | |
| 471 | }); |
static/val.js +8
| @@ -347,6 +347,14 @@ const worldToScreen = (p) => ({ x: p.x * scale(), y: p.y * scale() }); | ||
| 347 | 347 | */ |
| 348 | 348 | const look = (p) => { |
| 349 | 349 | if (predicting() && p.id === snap.me.id) return drawnMe; |
| 350 | // Your own body, when it is not being predicted — the buy phase, or dead — | |
| 351 | // is drawn from the newest word about it and nothing else. It must not go | |
| 352 | // through the two treatments below, which exist for OTHER people: drawing | |
| 353 | // it in the past puts the camera behind your own mouse, and the arriving | |
| 354 | // slide treats a respawn as somebody walking into view and slides the | |
| 355 | // camera in from the last place it saw you. Together they are the shake at | |
| 356 | // spawn. | |
| 357 | if (p.id === snap.me.id) return p; | |
| 350 | 358 | // A body still arriving is drawn part of the way in from where it came, |
| 351 | 359 | // rather than at the position it had already reached when we first saw it. |
| 352 | 360 | return slide(presence, p.id, bodyAt(timeline, p, drawAt), nowSec, slid); |