Clamp dt inside pe.tick: a real, confirmed entity-entity tunneling risk after decision 0032 raised speed, made worse by zero dt clamping in the game loop
Context
Continuing autonomous development after the last round of feature/decision work (0032-0036): re-examined whether raising gravity and reducing drag (decision 0032, to make drops fall faster per direct user feedback) introduced any new correctness risk, since faster entities are exactly what makes discrete (non-swept) collision detection unsafe. glemy/game.gleam's loop computes dt directly from real requestAnimationFrame timestamps (`{timestamp -. previous} /. 1000.0`) with no clamp of any kind -- a real, not hypothetical, gap: a GC pause, a backgrounded browser tab regaining focus, or a slow device can and does produce a dt far larger than one real frame's worth. collision.overlap (glemy/pe/collision.gleam) only ever compares each frame's *end* positions -- it has no concept of the path an entity swept through between frames -- so a sufficiently large dt lets a fast-falling entity's integrated position land cleanly past another entity it should have collided with, with no frame in between where they ever actually overlapped. Confirmed this directly, not just reasoned about it: reproduced the exact failure with a synthetic large-dt pe.tick call before any fix (a falling entity ending up measurably below a target entity with a large gap, neither having registered a collision), then confirmed it's mathematically inevitable in general for this architecture (since gravity affects both entities identically in one integration step, their relative closing distance per frame reduces to `(v_a - v_b) * dt`, independent of gravity -- meaning any two entities with a large enough relative speed and a large enough dt can tunnel regardless of gravity's own value).
Options considered
Decision
Added pub const max_dt = 1.0 /. 30.0 (~0.0333s) to pe.gleam, with a doc comment explaining why this project's single-step architecture needs a much smaller bound than the classic accumulator-pattern clamp. pe.tick now does `let dt = float.min(dt, max_dt)` as its very first line, before any other use of dt -- update, tick's own pure physics primitive, is deliberately left unclamped so its existing test suite keeps testing true integration math. Nine existing pe_test.gleam tests and two game_test.gleam tests that previously passed a dt larger than max_dt (expecting it to be used exactly as given) were updated: most simply use pe.max_dt as their own dt and recompute expected values via the same formula entity.integrate/entity.settle use internally (not hand-typed products, so they can't silently drift out of sync with max_dt again); one (the cooldown-elapses-fully test) was changed to accumulate real elapsed time across 16 max_dt-sized ticks instead of one artificially large dt call, which also more faithfully matches how the real per-frame loop actually behaves.
Verification
162 Erlang / 185 JavaScript gleam test cases passing on both targets (one new permanent regression test, tick_clamps_an_unreasonably_large_dt_to_avoid_tunneling_test, replacing the ad hoc diagnostic scaffolding used to first confirm the bug and then verify the fix -- consistent with this project's standing practice of not shipping throwaway debug code). The regression test reproduces the exact scenario that demonstrated real tunneling before the fix (a target entity in open space, not resting on the floor, so the floor's own position clamp can't mask the result either way) and asserts the two entities stay correctly ordered (falling strictly above target) after a deliberately extreme 1-second synthetic hitch, which an unclamped tick would have closed the entire 45-unit gap more than three times over in one step. Re-ran tools/browser_check.ts against a real headless-Chromium session after the change: still passes cleanly with zero page errors, confirming normal 60fps gameplay (where real dt is always comfortably under max_dt) is completely unaffected by the clamp.
Consequences
A real stall (GC pause, tab backgrounding, slow device) now produces bounded slow-motion for that moment rather than a silent tunnel-through or an unphysical single-frame velocity spike -- the standard, accepted tradeoff for this failure mode. pe.max_dt is now the canonical place any future caller of pe.tick should look to understand the engine's own safety bound, rather than assuming dt is used verbatim. update remains available, deliberately unclamped, for any test or future use genuinely needing to exercise raw, unbounded integration math.