Split The code is public; the app answers only to the crew.

Bullet holes that stay: a fixed ring of decals, drawn in one call #144

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Pushed. `bun run check` is green (577 tests, 40 client test files).

shipped merged

opened this

Pushed. bun run check is green (577 tests, 40 client test files).

---

Shooting a wall now leaves something behind. Every round that hits the level marks the surface it hit — turned by the normal the impact effect already carried, lifted half a unit off the wall so it cannot z-fight — and the mark stays for the round instead of for a quarter of a second.

src/client/decals.ts is the ring, and it is the only new concept: 64 slots allocated once at start-up, the oldest overwritten when it is full, each a fan of six triangles about a dark centre. The marks are drawn multiplied into the wall behind them (DST_COLOR, ZERO), so a vertex colour of 1 is a wall untouched and the fade is nothing but the centre colour walking back up to white over 25 seconds of the clock, never a count of frames. That needs a position and a colour, which is exactly what the line program already takes — so all 64 holes are one drawArrays of triangles, inside the pass the tracers were already in: one more draw, no third program, no texture, no per-decal draw and nothing allocated per shot. The frame's draw rule in renderer.test.ts gained a decals term and the worst round the game can build is now 60 draws against its budget of 80, still two programs switched four times.

The renderer takes each impact once, on the tick it was stamped (an impact effect lives two seconds and a frame meets it 120 times), and drops the ring when match.round moves or the clock runs backwards — a fresh round starts on clean walls, which is the round-restart test. Only the level is marked: world.ts's impact effect now says whether the trace reported a surface, so blood and the five sparks a grenade throws into open air mark nothing.

What I chose not to do: models.ts:198's unit quad is untouched and there is no instance buffer — instancing a textured quad would have meant a third program and a per-decal attribute stream for something that is 1152 vertices in total, and the fan carries its own falloff in vertex colours for free. And nothing culls decals per frame: at a low render scale the pass is fragment work like everything else, so it shrinks with the buffer, and off-screen it is one draw whose triangles are clipped — the only guard worth having is the one that is there, which skips the draw and the upload entirely when the ring is empty or everything in it has faded.

In the preview: spray a wall on the T side of mid and walk away — the marks are still there when you come back, they overlap into a patch where you held the trigger, they darken a crate on both faces a round punched through, and they are gone the moment the next round's freeze time starts.

🤖 Generated with Claude Code

Closes #109

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs