Public: anyone can read this; the crew can change it.

forked from trevato/cs · proposes

Transient effects become engine: a capped ring of timestamped events, pruned without allocating #166

Merged opened this

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

Merged

`src/engine/effects.ts` is a capped, timestamped ring of whatever a game puts in it: `createEffects(capacity)` builds both stores whole — a slot per event and a `Float64Array` of stamps — `pushEffect(fx, event, at)` writes one slot, `effectAt(fx, i)` hands back the game's own object in push order, `expireEffects(fx, now, ttl)` compacts the survivors towards the head, and `resetEffects` empties it.…

shipped production is serving it deploy ae07c2d

opened this

src/engine/effects.ts is a capped, timestamped ring of whatever a game puts in it: createEffects(capacity) builds both stores whole — a slot per event and a Float64Array of stamps — pushEffect(fx, event, at) writes one slot, effectAt(fx, i) hands back the game's own object in push order, expireEffects(fx, now, ttl) compacts the survivors towards the head, and resetEffects empties it. I chose one lifetime per buffer over a per-event one because both games already keep everything for the same span and decide per kind by age in the frame; the expiry keeps an event whose stamp is strictly after now - ttl, which is the shooter's old e.at > cutoff to the letter. At the cap the oldest is dropped rather than the buffer grown, the scene pool's policy pointed the other way.

What the shooter's frame allocated: the old world.effects = world.effects.filter(…) made one array per tick with anything alive — stepping a live 5v5 with everyone firing, that is 64 arrays a second, every second, and 5,373 of a 90-second round's 5,760 ticks (93%), plus whatever the growing array reallocated on push. After: zero, for any number of events; the peak live count in that same round is 138, against an EFFECT_CAPACITY of 4096 picked so the frame's line budget is what limits a busy frame and never the memory of what happened. bun test is 715 tests in 10.0s, green; the app bundle is 85,010 B raw / 38,852 B gzip against #39's 88,000 / 40,500 ceiling.

src/client/world.ts keeps its Effect type and its two-second cutoff unchanged; scene.ts, hud.ts and main.ts read the ring by index with no copy, and src/rl/arena.ts resets it per step in a walk of the live events rather than of the capacity. replay.test.ts's golden digests are byte-identical — effects were never folded into them and the prune drops exactly what it dropped before. frames.test.ts now captures the effect ring's two stores beside the scene's pool and compares all four by identity after two hundred frames; src/engine/effects.test.ts is the module's own, including the 5,000-event flood and the two-frame identity claim. boundary.test.ts, purity.test.ts and guide.test.ts stay green with no rule loosened — the door's surface list, GUIDE.md's new section and the README's engine list all gained the six names, and Float64Array joined the guide's list of platform words.

What I chose not to do: the range's tracer stays as it is. It is one preallocated Trail object written over on each shot with one shot in the air at a time, so it already allocates nothing and a one-slot ring would be more machinery than the struct it replaces; I have not opened an issue for it because my answer is that it should not move. The buffer also stays dumb, as asked: no ids, no priorities, no callbacks.

🤖 Generated with Claude Code

Closes #67

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs