Split The code is public; the app answers only to the crew.
Utility, part two: smoke that actually takes a sightline off the map #37
Two details this issue could not know, now that #36 has landed as #47:
- The pocket is a number.
Player.grenadesis a count withMAX_GRENADES = 1, andheis a singleWeaponIdon the fourth slot. A second kind of grenade cannot be a second count bolted on — the pocket has to become per-kind (one HE and one smoke, as in CS) and slot 4 needs a stated rule for which one it draws and how you get to the other. Say what that rule is before writing the cloud. - Reuse the flight, not a copy.
grenades.tsalready owns the throw (thrown), the bounce and the fuse, withGrenade.kindas the seam; a smoke is that same object with a different thing happening when the fuse ends. The line-of-sight rule differs though: the HE blast requiresvisible(g.pos, eye, boxes), and a cloud's own sight test is the new concept this issue is really about.
You've hit your weekly limit · resets Sep 20, 2am (UTC)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)
You've hit your weekly limit · resets Sep 20, 2am (UTC)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)
Rewritten — the body above is superseded by this comment. I cannot edit another author's issue, so read this as what done looks like now.
The blocker this issue has been parked on twice (both times an agent's run died on an API error, not on the work) is real: world.ts:428 keeps grenades as one integer, MAX_GRENADES = 1, so a smoke cannot be carried beside an HE. That half is now #130, and this issue waits on it and is only the cloud.
Done looks like:
- [ ] Smoke at $300 in the buy panel, carried through #130's pocket, thrown by exactly the path the HE uses in
grenades.ts— the fuse ends in a cloud instead of a blast, and there is no second throwing mechanism. - [ ] On coming to rest it blooms over about a second to a radius of ~144 units, holds ~15 seconds, fades. Live clouds are capped and the oldest dropped, so a long round cannot grow the list without bound.
- [ ] One function says whether a segment passes through a cloud, and
canSeeinworld.tsis its only caller besides the renderer. A bot cannot see through smoke and gains no code to know that — it simply stops seeing. Bullets are not blocked. - [ ] Drawn in
renderer.tsas a soft animated volume, with the frame budget from #89 still met with two clouds up. State the before and after you measured. - [ ]
observe.tsis untouched; if a bot's observation changes, the shipped policy's card is wrong and the pull must say so. - [ ] Tests: a segment through a cloud fails
canSeeand the same segment a second after the fade passes; a cloud list that cannot exceed its cap.
Crew: vesper — the cloud is rendering and the sight rule is geometry.
Unblocked: #130 merged as pull #142 (d54675d), so the pocket this issue was waiting on exists. What it actually looks like, so the smoke goes in against the real shape rather than the one the old body imagined:
// src/client/world.ts:47-52
/** The pocket: one slot per grenade kind, so a second kind (smoke, when it … */
grenades: Partial<Record<WeaponId, number>>;
// world.ts:441-448 — the ceiling is per kind, not shared
export const MAX_GRENADES = 1;
Three things that changes for this issue:
MAX_GRENADESis a per-kind ceiling now, and the comment beside it says the pocket deliberately has no shared total. If a smoke plus an HE should be capped at two, that decision belongs in this pull, in that same place, with the reason written down.GRENADE_IDSis derived fromWEAPONS, so a smoke becomes a grenade the pocket understands by being a weapon whosekindis"grenade"— no new list to keep in step.- The round boundary already keeps the pocket (world.ts:382) and clears only what is in the air, so a bought smoke survives to the next round exactly as an HE does. Nothing to add there; just do not undo it.
The rest of my rewrite above stands: the cloud, one sight function used by canSee and the renderer, bullets not blocked, the frame budget stated, and observe.ts untouched. Ready now.
A smoke grenade that blinds both sides for long enough to cross the ground it covers — the same thrown object issue #36 builds, with a cloud instead of a blast.
Done looks like:
canSeeinworld.ts— a bot cannot see a player through smoke, and neither can the HUD's sight checks. Bullets are not blocked: a bullet through smoke does its usual damage, as in CS.renderer.tsas a soft, animated volume that hides what is behind it, with 60 fps held with two clouds up on the machine the frame counter is measured on. Say what you measured, before and after.bots.tsreads a cloud; they simply lose the target when the sightline goes, and they still hear shots through it vialisten.canSeeflips false then true across a cloud's life; the cap holds; a cloud thrown in one round is gone in the next.bun testgreen, README updated — Playing for the key and the price, What is real for what smoke does and does not stop.Not in this issue: flashbang and molotov, and bots buying or throwing utility. Smoke is never spawned in
src/rl/arena.ts: the shipped weights were trained without it and the observation stays at 32 inputs.Files I expect to change:
src/client/smoke.ts(new, or a second concept insidegrenades.tsonly if it truly is one) + its test,src/client/world.ts,trace.ts,renderer.ts,hud.ts,weapons.ts,README.md.Risks: the sight test runs per bot per tick over every cloud and sits in the hot path of the bot loop — make it a cheap ray-versus-sphere over a small array with a zero-length fast path, and keep the arena's cost unchanged (
bun run scripts/train.ts --benchbefore and after). Translucent volume is where frame time goes: soft billboards that fade near the camera are likely enough, and a cloud must not make the whole scene shimmer or shift layout on the HUD canvas.Crew: vesper. Waits on #36 for the thrown object.