Make sound carry information: pan every world sound around the listener's view, and give a running player footsteps, so someone coming up behind you is something you can hear before you see them.
Today audio.shot scales one mono gain by distance and nothing else, so a rifle behind you and a rifle in front of you are the same sound; Effect has declared { kind: "sound"; pos; name } since the world was written (src/client/world.ts:70) and nothing has ever pushed one. In a defusal game that is the largest missing sense.
Done looks like
[ ] A new module — src/client/ears.ts — turns a source position, the listener's position and the listener's yaw into plain data: { pan: -1..1, volume: 0..1 }. No Web Audio in it, so it is tested headlessly.
[ ] src/client/ears.test.ts: a source dead ahead pans 0; one 90° to the listener's left pans to the left channel and one to the right pans right (assert the sign, and say in a comment which way yaw grows — world.ts already had to write that comment down for the damage wedges); a source directly behind is roughly centred and no louder than the same distance ahead; volume falls with distance, never exceeds 1, and never divides by zero at zero distance.
[ ] audio.shot routes through a StereoPannerNode fed by ears, keeping the distance falloff it has now. audio.pain and audio.hit stay centred — they happen to you, not somewhere.
[ ] world.ts pushes { kind: "sound", name: "step", pos, at } for every player, person and bot alike, when their feet have covered one stride on the ground. Cadence comes from distance travelled, not from the frame or a timer, so it speeds up when they do.
[ ] Silent movement stays silent: nothing while ducking, nothing while holding walk (Shift) — those are the two ways CS lets you move quietly — and nothing while airborne.
[ ] src/client/world.test.ts: a player running in a straight line for one second leaves a plausible number of step effects (a stride is about 60–70 units at 250 u/s, so 3–4); the same player walking or ducking leaves none; half the speed leaves about half the steps.
[ ] main.ts's feelEffects plays a footstep for sound effects from the watched player's ears (it already takes ears: Player for exactly this reason), and drops any further than earshot — pick a distance, name the constant, say what you picked.
[ ] bun test passes, and the README sentence about sound mentions footsteps and which way they are panned.
The policy's contract.src/client/observe.ts is 32 fixed numbers and the shipped int8 weights mean nothing if they move. Adding a stepDistance field to Player is fine; changing speeds, eye height or anything observe reads is not, and would make the win rate on the page a lie.
The training hot path.src/rl/arena.ts steps applyInput directly and empties world.effects every tick (arena.ts:271), so an allocation per player per tick lands in every one of the millions of training steps. Keep the push behind the stride test, where it happens three times a second, not sixty.
Ten players' footsteps are the first thing that will make world.effects long — it is trimmed on a two-second window in update, and feelEffects must keep only reading effects newer than lastSound so a step is never played twice.
Create-and-forget node graphs are the shape audio.ts already uses; keep it. A footstep is bounded by the stride, so it needs no rate limit of its own — say so rather than adding one.
Crew
vesper — input, feel and the browser's own APIs, with the measurement written down.
Make sound carry information: pan every world sound around the listener's view, and give a running player footsteps, so someone coming up behind you is something you can hear before you see them.
Today
audio.shotscales one mono gain by distance and nothing else, so a rifle behind you and a rifle in front of you are the same sound;Effecthas declared{ kind: "sound"; pos; name }since the world was written (src/client/world.ts:70) and nothing has ever pushed one. In a defusal game that is the largest missing sense.Done looks like
src/client/ears.ts— turns a source position, the listener's position and the listener's yaw into plain data:{ pan: -1..1, volume: 0..1 }. No Web Audio in it, so it is tested headlessly.src/client/ears.test.ts: a source dead ahead pans 0; one 90° to the listener's left pans to the left channel and one to the right pans right (assert the sign, and say in a comment which way yaw grows —world.tsalready had to write that comment down for the damage wedges); a source directly behind is roughly centred and no louder than the same distance ahead; volume falls with distance, never exceeds 1, and never divides by zero at zero distance.audio.shotroutes through aStereoPannerNodefed byears, keeping the distance falloff it has now.audio.painandaudio.hitstay centred — they happen to you, not somewhere.world.tspushes{ kind: "sound", name: "step", pos, at }for every player, person and bot alike, when their feet have covered one stride on the ground. Cadence comes from distance travelled, not from the frame or a timer, so it speeds up when they do.src/client/world.test.ts: a player running in a straight line for one second leaves a plausible number of step effects (a stride is about 60–70 units at 250 u/s, so 3–4); the same player walking or ducking leaves none; half the speed leaves about half the steps.main.ts'sfeelEffectsplays a footstep forsoundeffects from the watched player's ears (it already takesears: Playerfor exactly this reason), and drops any further than earshot — pick a distance, name the constant, say what you picked.bun testpasses, and the README sentence about sound mentions footsteps and which way they are panned.Files I expect to change
src/client/ears.ts(new),src/client/ears.test.ts(new),src/client/audio.ts,src/client/world.ts,src/client/world.test.ts,src/client/main.ts,README.md.Risks
src/client/observe.tsis 32 fixed numbers and the shipped int8 weights mean nothing if they move. Adding astepDistancefield toPlayeris fine; changing speeds, eye height or anythingobservereads is not, and would make the win rate on the page a lie.src/rl/arena.tsstepsapplyInputdirectly and emptiesworld.effectsevery tick (arena.ts:271), so an allocation per player per tick lands in every one of the millions of training steps. Keep the push behind the stride test, where it happens three times a second, not sixty.world.effectslong — it is trimmed on a two-second window inupdate, andfeelEffectsmust keep only reading effects newer thanlastSoundso a step is never played twice.audio.tsalready uses; keep it. A footstep is bounded by the stride, so it needs no rate limit of its own — say so rather than adding one.Crew
vesper — input, feel and the browser's own APIs, with the measurement written down.