Top-down uses the server's angle; first person uses the browser's. So a disagreement between those two is invisible in one view and violent in the other. That asymmetry is why this looked like a renderer bug for so long.
And they were disagreeing.player.angle = input.angle lived inside movePlayer, which the end and over phases return before ever reaching, and which a dead player never enters (if (!player.alive) return). So between rounds, and for anyone lying dead, the server's copy of your angle froze while your browser kept following your mouse.
yaw.js takes the server's angle only when it disagrees with what the client recently sent. Once those sent angles aged out of SENT_MEMORY, the frozen one stopped looking like an echo and started looking like a correction — so the view snapped back to where you were last recorded looking, again and again.
Measured, a client looking around for twenty seconds:
state
view snaps
alive, live phase
0.1 a second
alive, END phase (round just decided)
0.8 a second
DEAD, waiting for the round
0.8 a second
alive, buy phase
0.1 a second
After the fix: 0.1 everywhere, which is the baseline in a live round.
The fix is a rule, not a tuning number: looking around is not moving. Movement is what a phase and a death gate; where your head points is recorded every tick regardless. The assignment moves out of movePlayer into tick, for every player, in every phase.
Why the buy phase was fine and the end phase was not — movePlayer runs during buy — is also why this only ever bit as a round turned over, which is exactly where you said it did.
305 tests. The new one walks a seat through live, dead, end, over and buy and asserts the angle follows; putting the assignment back inside movePlayer fails that test and nothing else.
Worth recording what this was NOT, since I tested each one: your body's prediction (0 backward steps in 7,667 frames even at 40ms jitter and 5% loss), the interpolation of other players across a round-start teleport (0 units), the resolution ladder (descends once, never flaps), and the raycaster's cost (0.04ms a frame whether 0 or 9 bodies are in view). Every one of those came back clean, which is what left the angle as the only thing that could differ between two views of the same data.
What you told me found this, not my probing. The readout said
30 Hz in, so the wire was fine — and pressing V stopped it dead.That second fact is the whole diagnosis. The two views draw with different angles:
Top-down uses the server's angle; first person uses the browser's. So a disagreement between those two is invisible in one view and violent in the other. That asymmetry is why this looked like a renderer bug for so long.
And they were disagreeing.
player.angle = input.anglelived insidemovePlayer, which theendandoverphasesreturnbefore ever reaching, and which a dead player never enters (if (!player.alive) return). So between rounds, and for anyone lying dead, the server's copy of your angle froze while your browser kept following your mouse.yaw.jstakes the server's angle only when it disagrees with what the client recently sent. Once those sent angles aged out ofSENT_MEMORY, the frozen one stopped looking like an echo and started looking like a correction — so the view snapped back to where you were last recorded looking, again and again.Measured, a client looking around for twenty seconds:
After the fix: 0.1 everywhere, which is the baseline in a live round.
The fix is a rule, not a tuning number: looking around is not moving. Movement is what a phase and a death gate; where your head points is recorded every tick regardless. The assignment moves out of
movePlayerintotick, for every player, in every phase.Why the buy phase was fine and the end phase was not —
movePlayerruns during buy — is also why this only ever bit as a round turned over, which is exactly where you said it did.305 tests. The new one walks a seat through live, dead, end, over and buy and asserts the angle follows; putting the assignment back inside
movePlayerfails that test and nothing else.Worth recording what this was NOT, since I tested each one: your body's prediction (0 backward steps in 7,667 frames even at 40ms jitter and 5% loss), the interpolation of other players across a round-start teleport (0 units), the resolution ladder (descends once, never flaps), and the raycaster's cost (0.04ms a frame whether 0 or 9 bodies are in view). Every one of those came back clean, which is what left the angle as the only thing that could differ between two views of the same data.