benjivers/val Public Public: anyone can read this; the crew can change it.

Make val feel right end to end — the whole feel pass, measured #17

Merged opened this in answer to

shipped production is serving it deploy b42aa0d

opened this

The app boots, the page answers 200, the bundle carries the new readout, and the working tree is clean. Everything is pushed.

The player said it still was not smooth after three fixes, so this pass starts by measuring instead of trusting the list. A headless wire between the real match and the real client modules (src/feel.test.ts) delivers thirty snapshots a second with the jitter measured on the box (p50 33ms, p99 41–48ms) and draws sixty frames from them. Before and after, on the same wire:

MeasureBeforeAfter
Own body walking, per-frame step spread17.0%0.0%
Walk start, largest backward step4.9 units0.0, never below full speed
Everybody else, per-frame step spread17.3%7.3%
Interpolation restarts of a body in sight00
Interpolation delay in force45–61ms, wobbling45.0–45.2ms
Render clock, motion drawn per 16.67ms frame11.9–19.0ms16.3–17.0ms
Round start, camerafaces site, then swings east one tick laterfaces site, one cut

Three causes, none of them throughput. First, the wire's rhythm was being drawn: samples were filed under their arrival time, so a late snapshot followed by an early one made a body walk slow then fast, and made your own snapshot's age, which sets how far your keys are carried, jump at every arrival. static/interp.js now files arrivals on an even clock, one interval after the last and nudged a tenth toward the real arrival, capped so a burst from a background tab cannot walk it into the future. The moment being drawn advances at real time and creeps toward its delay at two percent, so the delay adapting is never a short frame. Second, the input lag was being treated as a disagreement and clamped away at every snapshot. static/predict.js now lets the drawn body lead the projected answer by 80ms of running along its direction of travel and leaves that alone, leans across-track and behind-the-server gaps at the old rate, and holds for the lag after a stop. Ability barriers are refused the way map walls are, nobody moves in the end and over phases, and a hold never outranks the answer past the snap distance. Third, the server stomped its own spawn facing: startRound pointed everyone at the site and then reset input to an angle of zero, which the next tick copied onto the player. The input now keeps the spawn's facing.

Also in this pull: the gun kicks the moment the trigger is pulled and the snapshot's word about that shot is not a second kick, while the server still fires and lands it. The ` ` readout now prints the count of corrections taken outright (anchors, restarts, re-pointings) and the rate per minute. Two tests changed because the rule they encoded changed: the client test's lead bound now includes the slack, and its text check on the refused-step hold follows the code into stepLocal. The client test also frees the shared loopback address before connecting, which was the seat-race coin flip, and its first-person fill bound is now the column count rather than the tile count, which a corridor view legitimately exceeded. Five consecutive full-suite runs are clean. Not fixed: server.test.ts` still shares one live arena with the client test's open socket, so its spike-plant and reload-seat tests can still race the arena timer once in a dozen runs. Nothing sent by the server changed except the input angle kept at round start.

To feel it in the first ten seconds: click to lock, hold W and release, twice. The start no longer hitches backward and the stop does not drift. Then strafe along a wall with A or D held into it. Press ` `` and watch the jumps count stay flat while you walk. When the next round starts, the camera cuts once to face the site and stays there.

🤖 Generated with Claude Code

Closes #16

commented

The preview's failing test was the bundled-client test's last section, which stops the arena and measures how far the drawn body leads the server with a key held. It assumed the seat could walk. By that point the arena has been run through whole rounds with the human seat standing still, so it may be dead, mid-plant, slowed, standing inside a bot's barrier, or in the seconds after a round is decided. In each of those the guess rightly stands still and the lead is nothing, and the end-phase rule added in this pull widened that window. The test now puts the seat on its feet in a live round, clears what abilities left, lets one snapshot carry that to the client, and only then stops the clock, retrying if a round ends or a bot lands a kill in that moment. Ten full-suite runs alone and six under three-way contention are clean, and the branch is pushed.

The player said it still was not smooth after three fixes, so this pass starts by measuring instead of trusting the list. A headless wire between the real match and the real client modules (src/feel.test.ts) delivers thirty snapshots a second with the jitter measured on the box (p50 33ms, p99 41–48ms) and draws sixty frames from them. Before and after, on the same wire:

MeasureBeforeAfter
Own body walking, per-frame step spread17.0%0.0%
Walk start, largest backward step4.9 units0.0, never below full speed
Everybody else, per-frame step spread17.3%7.3%
Interpolation restarts of a body in sight00
Interpolation delay in force45–61ms, wobbling45.0–45.2ms
Render clock, motion drawn per 16.67ms frame11.9–19.0ms16.3–17.0ms
Round start, camerafaces site, then swings east one tick laterfaces site, one cut

Three causes, none of them throughput. First, the wire's rhythm was being drawn: samples were filed under their arrival time, so a late snapshot followed by an early one made a body walk slow then fast, and made your own snapshot's age, which sets how far your keys are carried, jump at every arrival. static/interp.js now files arrivals on an even clock, one interval after the last and nudged a tenth toward the real arrival, capped so a burst from a background tab cannot walk it into the future. The moment being drawn advances at real time and creeps toward its delay at two percent, so the delay adapting is never a short frame. Second, the input lag was being treated as a disagreement and clamped away at every snapshot. static/predict.js now lets the drawn body lead the projected answer by 80ms of running along its direction of travel and leaves that alone, leans across-track and behind-the-server gaps at the old rate, and holds for the lag after a stop. Ability barriers are refused the way map walls are, nobody moves in the end and over phases, and a hold never outranks the answer past the snap distance. Third, the server stomped its own spawn facing: startRound pointed everyone at the site and then reset input to an angle of zero, which the next tick copied onto the player. The input now keeps the spawn's facing.

Also in this pull: the gun kicks the moment the trigger is pulled and the snapshot's word about that shot is not a second kick, while the server still fires and lands it. The ` ` readout now prints the count of corrections taken outright (anchors, restarts, re-pointings) and the rate per minute. Tests changed because the rule they encoded changed: the client test's lead bound now includes the slack, its text check on the refused-step hold follows the code into stepLocal, and its walking measurement starts from a seat that can walk. The client test also frees the shared loopback address before connecting, which was the seat-race coin flip, and its first-person fill bound is now the column count rather than the tile count, which a corridor view legitimately exceeded. Not fixed: server.test.ts` still shares one live arena with the client test's open socket, so its spike-plant and reload-seat tests can still race the arena timer once in a dozen runs. Nothing sent by the server changed except the input angle kept at round start.

To feel it in the first ten seconds: click to lock, hold W and release, twice. The start no longer hitches backward and the stop does not drift. Then strafe along a wall with A or D held into it. Press ` `` and watch the jumps count stay flat while you walk. When the next round starts, the camera cuts once to face the site and stays there.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open esc close

Keyboard shortcuts