Give both sides the one thing a CS round has that this game has none of: a grenade you buy in the freeze time, throw on an arc, and that hurts everyone it can see when it goes off.
Where it stands.WEAPON_LIST in src/client/weapons.ts has no thrown item. world.ts has pickups and bomb as its only non-player entities. Input carries switchTo: WeaponId | null, and the buy panel builds itself from buyable(team) in src/client/main.ts:148.
Done looks like:
[ ] A new src/client/grenades.ts owning one concept — a thrown object's flight: position, velocity, gravity, bounce and friction against Box[], a fuse. No WebGL, no audio and no HUD in it, headless-testable like every other module beside it.
[ ] world.update steps them behind the same empty-list fast path updatePickups uses (if (world.grenades.length === 0) return), so the training arena, which never spawns one, pays one comparison a tick.
[ ] An HE grenade buyable at $300, one carried at a time, in the existing buy panel — listed with its price and disabled when the wallet or the round says no, like every other item.
[ ] Held as a fourth slot: 4 on the keyboard and a 4 nade button in #touch, chosen through the existing switchTo/slotWanted path and not a new one. Fire throws it, from the eye, forward and slightly up, with the thrower's velocity added. A player holding none cannot select the slot.
[ ] It bounces off walls and crates, loses speed each time, comes to rest, and detonates 1.6 s after it left the hand, wherever it is by then.
[ ] Damage: full at the centre, falling to zero at the blast radius, reduced by armour, and nothing at all through a wall — the visible test in trace.ts, eye to blast. It counts towards the thrower's damage, and a kill goes through the same path a bullet's does: kill feed, corpse, dropped gun, checkRoundEnd. No friendly fire, as everywhere else in this match, but the thrower can kill themselves.
[ ] Feedback with no new mechanisms: a sound effect on the bounce and on the blast, panned by ears like every other world sound; the grenade visible in the world while it flies; the blast drawn with the impact/flash the renderer already has; damage taken going through hurt/flinch, so the vignette, the directional wedge and the pain sound all work for free.
[ ] The HUD says what you are holding and how many are left. startRound gives none back: a grenade thrown is gone, one kept is still yours, which is the rule the guns already follow.
[ ] Tests (src/client/grenades.test.ts and world.test.ts): a throw from a known state lands where the numbers say; a bounce keeps it inside the map; the fuse fires once and only once; damage falls off with range; a wall between the blast and a player means zero; a blast can end a round; the buy is refused outside the freeze and when one is already carried.
[ ] bun test green; README's Playing table and What is real say the key, the price and the numbers.
Not in this issue: smoke and flash (their own issues), bots buying or throwing utility (they keep buying guns), and anything in src/rl — the shipped weights were trained in an arena with no grenades, so observe.ts stays at 32 inputs and the arena stays as it is.
Files I expect to change:src/client/grenades.ts + grenades.test.ts (new), src/client/world.ts, weapons.ts, input.ts, main.ts, hud.ts, renderer.ts, models.ts, audio.ts, src/pages/game.ts, README.md, and the tests beside each.
Risks: the arena steps world.update millions of times — keep the fast path, run bun run scripts/train.ts --bench before and after, and say what you measured (the README claims ~13,000 ticks/s on one core). NO_INPUT and every Input literal in bots.ts and src/rl/arena.ts need the new field defaulted. slotWanted is typed 1 | 2 | 3 | null in input.ts — widen it deliberately rather than casting. A blast must not kill a player twice or reopen a round that is already over.
Crew: vesper — this is the loop, the physics and the renderer.
Give both sides the one thing a CS round has that this game has none of: a grenade you buy in the freeze time, throw on an arc, and that hurts everyone it can see when it goes off.
Where it stands.
WEAPON_LISTinsrc/client/weapons.tshas no thrown item.world.tshaspickupsandbombas its only non-player entities.InputcarriesswitchTo: WeaponId | null, and the buy panel builds itself frombuyable(team)insrc/client/main.ts:148.Done looks like:
src/client/grenades.tsowning one concept — a thrown object's flight: position, velocity, gravity, bounce and friction againstBox[], a fuse. No WebGL, no audio and no HUD in it, headless-testable like every other module beside it.world.updatesteps them behind the same empty-list fast pathupdatePickupsuses (if (world.grenades.length === 0) return), so the training arena, which never spawns one, pays one comparison a tick.4on the keyboard and a4 nadebutton in#touch, chosen through the existingswitchTo/slotWantedpath and not a new one. Fire throws it, from the eye, forward and slightly up, with the thrower's velocity added. A player holding none cannot select the slot.visibletest intrace.ts, eye to blast. It counts towards the thrower'sdamage, and a kill goes through the same path a bullet's does: kill feed, corpse, dropped gun,checkRoundEnd. No friendly fire, as everywhere else in this match, but the thrower can kill themselves.soundeffect on the bounce and on the blast, panned byearslike every other world sound; the grenade visible in the world while it flies; the blast drawn with the impact/flash the renderer already has; damage taken going throughhurt/flinch, so the vignette, the directional wedge and the pain sound all work for free.startRoundgives none back: a grenade thrown is gone, one kept is still yours, which is the rule the guns already follow.src/client/grenades.test.tsandworld.test.ts): a throw from a known state lands where the numbers say; a bounce keeps it inside the map; the fuse fires once and only once; damage falls off with range; a wall between the blast and a player means zero; a blast can end a round; the buy is refused outside the freeze and when one is already carried.bun testgreen; README's Playing table and What is real say the key, the price and the numbers.Not in this issue: smoke and flash (their own issues), bots buying or throwing utility (they keep buying guns), and anything in
src/rl— the shipped weights were trained in an arena with no grenades, soobserve.tsstays at 32 inputs and the arena stays as it is.Files I expect to change:
src/client/grenades.ts+grenades.test.ts(new),src/client/world.ts,weapons.ts,input.ts,main.ts,hud.ts,renderer.ts,models.ts,audio.ts,src/pages/game.ts,README.md, and the tests beside each.Risks: the arena steps
world.updatemillions of times — keep the fast path, runbun run scripts/train.ts --benchbefore and after, and say what you measured (the README claims ~13,000 ticks/s on one core).NO_INPUTand everyInputliteral inbots.tsandsrc/rl/arena.tsneed the new field defaulted.slotWantedis typed1 | 2 | 3 | nullininput.ts— widen it deliberately rather than casting. A blast must not kill a player twice or reopen a round that is already over.Crew: vesper — this is the loop, the physics and the renderer.