Bots' holes cut the world too — wake-fix + fullscreen · Three things shipped together: (1) the shader cutout now applies to every hole, not just the player's — bots' holes dissolve the ground and streets just like yours. (2) Wake-fix in the dynamic mesh ground: rebuilds every FixedUpdate (no movement threshold) and uses a swept-cull that culls cells the hole's footprint passes *through* this tick, so cubes the hole passes over at high speed actually fall now. (3) A fullscreen toggle button on every version page — works on desktop, Android Chrome, and iOS Safari 16.4+.
HoleClippable.shader globals were single-hole in v0.7: _HoleCenter (Vector4), _HoleRadius (float). v0.8 promotes them to fixed-size arrays of 4: _HoleCenters[4], _HoleRadii[4]. No _HoleCount — the activation gate is the per-slot radius check (_HoleRadii[i] > 0.001), so unused slots are no-ops.InsideAnyHole(p) helper that [unroll]-iterates the 4 slots and returns true if the fragment's XZ falls inside any active hole's footprint AND below that hole's Y plane. One discard per fragment instead of one check per fragment.HoleClipFeeder.cs rewritten: walks every non-defeated HoleActor, fills up to 4 slots with (x, y + clipPlaneOffset, z) and growth.Radius, zeroes the rest. Defeated bots drop their slot the same frame they're defeated, so their (invisible during respawn) hole stops cutting until they're back.Shader.SetGlobalVectorArray / Shader.SetGlobalFloatArray once per LateUpdate. Cap of 4 holes matches the current scene (player + 3 bots); raising it is a one-line MaxHoles constant change plus the matching HOLE_MAX in the shader.PrototypeSceneBuilder needed no changes — the cylindrical wall, dynamic mesh ground, and disc-at-Y=-4.8 were all already per-actor (they iterate pgm.Actors). Only the shader globals were the lone single-hole holdout.GroundTileManager only rebuilt when a hole moved >8 cm or its radius changed >3 cm. At 9 m/s × 20 ms FixedUpdate = 18 cm/tick, the cell-cutout mesh always trailed the hole's actual position by one tick.CellIntersectsHole only checked the cell against the hole's *current* footprint. A cube the hole passed *over* between ticks (entered + exited within one FixedUpdate) was never inside the current footprint at any rebuild moment, so its cell was never culled.sleepThreshold = 0.005 re-sleep next solver step. A 20 ms support-gap accelerates a body by only 0.2 m/s and 2 mm of fall — right at the threshold. Even a 1-tick gap often wasn't enough to commit to falling.rebuildMoveThreshold and rebuildRadiusThreshold dropped to 0 (rebuild every FixedUpdate when anything moves). HoleSnapshot gains a PrevCenter field. A Dictionary<HoleActor, Vector2> lastCenters tracks each actor's last-frame XZ; refreshed at the end of CollectHoles (defeated/destroyed actors drop out automatically).CellIntersectsHole now samples the segment prev → current at half-radius spacing and runs the standard closest-point-on-AABB-to-circle check at each sample. Always conservative (might cull marginally extra cells, never fewer). At normal play speed the segment is shorter than half the radius so the loop runs 1–2 times per cell-hole pair — cost is imperceptible.HoleGameFrame wraps the iframe with a small toggle button at bottom-right. Calls containerRef.current.requestFullscreen() (or webkitRequestFullscreen for older Safari).aspect-video constraint when fullscreen via Tailwind's [&:fullscreen]: arbitrary variants — canvas fills the screen edge-to-edge instead of letterboxing on phones that aren't 16:9.fullscreenchange and the webkit-prefixed event so the button label flips correctly when the user exits via ESC / mobile back-gesture.requestFullscreen isn't available (e.g. iOS Safari pre-16.4) so it doesn't appear and lie. Modern iOS / Android / desktop all just work.