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

forked from trevato/cs

Bodies stand on the floor: a contact shadow under everything that moves, in the engine so every game gets it #330

Merged opened this

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

Merged

The tree is clean and the branch is pushed with nothing ahead of the remote — the work landed in the previous turn and `bun run test` was green (1274 pass, 0 fail) before the push.

shipped production is serving it deploy ae07c2d

opened this

The tree is clean and the branch is pushed with nothing ahead of the remote — the work landed in the previous turn and bun run test was green (1274 pass, 0 fail) before the push.

src/engine/scene.ts gains a third pool beside the instances and the lines: pushShadow(scene, pos, ground, reach, radius, strength) writes a point on the surface, a radius and a strength, widening by 1 + t and fading by (1 - t)² with height as a fraction of the reach, and answering false — nothing written — at or past it. The capacity is optional and zero when left out, so the example pays for no pool. src/engine/renderer.ts draws them as one blended quad each between the opaque pass and the lines: depth tested, depthMask(false), polygonOffset(-1, -1) rather than a world-space lift so the decision is unit-free, and a soft 1 - r² dome so there is no rim to see the quad by. The order is the answer to the blending risk — the smoke cloud is opaque in this frame and has already written depth, so a cloud in front of a shadow hides it and a shadow never darkens a cloud.

The shooter casts one under every living player (including the one the camera is inside, so you can see your own), the bomb and every grenade, traced with groundUnder against the level's boxes at a 96-unit reach; the range casts one under each plate, from box.min[2] and against RANGE_ROOM, at 128. groundUnder now takes Pick<Hull, "radius"> — it only ever read that field — so a grenade and a plate can ask without inventing an eye height. Cost measured headless: the scene benches still pass under their unchanged 0.15 ms and 0.02 ms ceilings, and the draw is four GL calls per shadow plus five for the pass, with a frame that casts none making none of them. Fill rate is capped by construction: a shadow is never wider than twice the radius the game named — 44 units for a player, 45 for a standard plate.

What is not done: no real-browser frame time. There is no GPU or browser in this sandbox, so the before/after milliseconds the issue asks for are missing; the numbers above are the CPU-side scene build and the GL call count, and the visual claims are held only by the data tests. In the preview, look at / — a player standing, then jumping, the disc should widen and wash out, and a player on a crate should be shadowed on the crate and not on the sand below — and /range, where each plate carries a soft patch on the lane that tracks the sliding drill. The door is 0.3.0 for two arrived names, Shadow and pushShadow; the range's ceiling moved to 41,500 raw / 17,200 gzip against 38,690 / 16,050 measured by bun run scripts/build.ts, most of which is the renderer's third program arriving whether a game casts a shadow or not.

🤖 Generated with Claude Code

Budget reached at $10.24; this is where it stopped.

Closes #279

commented

main is merged and pushed; bun run test is green at 1297 pass, 0 fail. The only conflict was the entry table in scripts/build.test.ts, where both sides had raised a ceiling — #300's app bundle for the room socket, #279's range bundle for the shadow pass — so the resolution keeps both numbers, and src/games.ts had already auto-merged the same way. A rebuild confirms it: app 117,567 raw / 45,659 gzip under 122,000 / 47,500, range 38,690 / 16,050 under 41,500 / 17,200.

The change itself: src/engine/scene.ts gains a third pool beside the instances and the lines. pushShadow(scene, pos, ground, reach, radius, strength) writes a point on the surface, a radius and a strength, widening by 1 + t and fading by (1 - t)² with height as a fraction of the reach, and answering false — nothing written — at or past it. The capacity is optional and zero when left out, so the example pays for no pool. src/engine/renderer.ts draws them as one blended quad each between the opaque pass and the lines: depth tested, depthMask(false), polygonOffset(-1, -1) rather than a world-space lift so the decision is unit-free, and a 1 - r² dome so there is no rim to see the quad by. The order is the answer to the blending risk — the smoke cloud is opaque in this frame and has already written depth, so a cloud in front of a shadow hides it and a shadow never darkens a cloud.

The shooter casts one under every living player (including the one the camera is inside, so you can see your own), the bomb and every grenade, traced with groundUnder against the level's boxes at a 96-unit reach; the range casts one under each plate, from box.min[2] against RANGE_ROOM, at 128. groundUnder now takes Pick<Hull, "radius"> — it only ever read that field. The scene benches still pass under their unchanged 0.15 ms and 0.02 ms ceilings; the draw is four GL calls per shadow plus five for the pass, and a frame that casts none makes none of them. Fill rate is capped by construction: never wider than twice the radius the game named — 44 units for a player, 45 for a standard plate.

Still not done: no real-browser frame time. There is no GPU here, so the before/after milliseconds the issue asks for are missing and the visual claims are held only by data tests. In the preview, look at / — a player standing, then jumping, the disc should widen and wash out, and a player on a crate should be shadowed on the crate and not on the sand below — and /range, where each plate carries a soft patch on the lane that tracks the sliding drill. The door is 0.3.0 for two arrived names, Shadow and pushShadow.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs