Three commits, all 181 tests green, and the server boots and serves / in under 3ms with the new bundle.
1. The bots aim like something with hands (src/aim.ts). A bot's hold on a target is now plain data — when it was acquired, when it was last seen, how wrong the barrel is — and three honest costs sit on it: a 200–350ms reaction drawn on acquisition that gates the trigger; a cone that opens to ~6° on the swing and settles toward 0.5° the longer one target is held; and a term for how fast the target is crossing the sight line. The error wanders inside the cone rather than being redrawn each tick, because noise resampled at 30Hz averages to a perfect barrel over a burst — that was the aimbot feel. A hold survives a 0.4s blink so a smoke edge does not cost a fresh reaction. The spread stays shared in weapons.ts, every roll comes from the match's seeded random, and a seed still replays identically.
2. The wall skip was the server, not the camera.moveCircle snapped a blocked step to col * TILE - r where col was the column of the centre of the refused position, not of the wall it hit: walking east into the wall at x=320 from x=298 put the body at 224 — 74 units backwards — then it walked forward and snapped back again, every tick you leaned on a wall. The maths moved to src/glide.ts, which projects the motion onto the wall and creeps the refused component up to the surface by halving, so the result is a fixed point and a corner resolves to one place whichever component is taken first.
3. Your own body is now drawn from a prediction (static/predict.js), running your movement forward from your own keys through that same glide.ts — one copy, so the guess lands where the answer is about to be. The last snapshot is carried forward by the input held since it arrived (capped at 0.25s), the drawn body leans onto that and is never drawn past it, past 40 units of disagreement the server's answer is taken outright, and nothing is sent.
Left out deliberately: no input-sequence acknowledgement or replay — the unacknowledged part of your movement is measured in time rather than in a seq the server echoes, which is simpler and settles exactly onto the server when you stop, but is an approximation under jitter. The buy phase is not predicted (its spawn pen is a match rule the browser does not know) and neither are ability walls (movement through one is corrected by the lean rather than predicted); both are noted in the code. Other players are still smoothed as before.
First thirty seconds in the preview: take a seat, hold W into any wall and strafe along it — the view stays put against the wall instead of jumping backwards, and corners no longer chatter. Then stand in the open and let a bot round the corner onto you: there is a beat before the first shot, and the first burst goes wide before it tightens; strafing keeps it wide.
Tests: src/glide.test.ts covers the collision directly (corner, fixed point, no step ever ending inside a wall), src/aim.test.ts covers the reaction gate as a pure function of acquisition time plus the settle and the crossing term, src/predict.test.ts checks the browser's step equals moveCircle's on every open tile of the real map from eight directions, and src/client.test.ts now stops the arena and proves the drawn body still walks — while the seat on the server does not move. I exercised it through the bundled-client test and a live server boot; I have no browser here, so the visual claims above are from those, not from a real window.
Three commits, all 181 tests green, and the server boots and serves
/in under 3ms with the new bundle.1. The bots aim like something with hands (
src/aim.ts). A bot's hold on a target is now plain data — when it was acquired, when it was last seen, how wrong the barrel is — and three honest costs sit on it: a 200–350ms reaction drawn on acquisition that gates the trigger; a cone that opens to ~6° on the swing and settles toward 0.5° the longer one target is held; and a term for how fast the target is crossing the sight line. The error wanders inside the cone rather than being redrawn each tick, because noise resampled at 30Hz averages to a perfect barrel over a burst — that was the aimbot feel. A hold survives a 0.4s blink so a smoke edge does not cost a fresh reaction. The spread stays shared inweapons.ts, every roll comes from the match's seededrandom, and a seed still replays identically.2. The wall skip was the server, not the camera.
moveCirclesnapped a blocked step tocol * TILE - rwherecolwas the column of the centre of the refused position, not of the wall it hit: walking east into the wall at x=320 from x=298 put the body at 224 — 74 units backwards — then it walked forward and snapped back again, every tick you leaned on a wall. The maths moved tosrc/glide.ts, which projects the motion onto the wall and creeps the refused component up to the surface by halving, so the result is a fixed point and a corner resolves to one place whichever component is taken first.3. Your own body is now drawn from a prediction (
static/predict.js), running your movement forward from your own keys through that sameglide.ts— one copy, so the guess lands where the answer is about to be. The last snapshot is carried forward by the input held since it arrived (capped at 0.25s), the drawn body leans onto that and is never drawn past it, past 40 units of disagreement the server's answer is taken outright, and nothing is sent.Left out deliberately: no input-sequence acknowledgement or replay — the unacknowledged part of your movement is measured in time rather than in a seq the server echoes, which is simpler and settles exactly onto the server when you stop, but is an approximation under jitter. The buy phase is not predicted (its spawn pen is a match rule the browser does not know) and neither are ability walls (movement through one is corrected by the lean rather than predicted); both are noted in the code. Other players are still smoothed as before.
First thirty seconds in the preview: take a seat, hold W into any wall and strafe along it — the view stays put against the wall instead of jumping backwards, and corners no longer chatter. Then stand in the open and let a bot round the corner onto you: there is a beat before the first shot, and the first burst goes wide before it tightens; strafing keeps it wide.
Tests:
src/glide.test.tscovers the collision directly (corner, fixed point, no step ever ending inside a wall),src/aim.test.tscovers the reaction gate as a pure function of acquisition time plus the settle and the crossing term,src/predict.test.tschecks the browser's step equalsmoveCircle's on every open tile of the real map from eight directions, andsrc/client.test.tsnow stops the arena and proves the drawn body still walks — while the seat on the server does not move. I exercised it through the bundled-client test and a live server boot; I have no browser here, so the visual claims above are from those, not from a real window.🤖 Generated with Claude Code
Closes #8