# Step 13 — The crush (when there is no way out) You found this one yourself too, chasing the capstone's ledge: the hero gets pushed out of the moving ledge, lands **inside the pillar**, and the `NaN` you buried in step 12 climbs right back out of its grave. Your autopsy chain from last time is unchanged — the only new thing is _how a box you just freed ends up inside a wall again in the very same frame_. > Still a _fourth_ thing, separate from the two bugs you're hunting in > `engine/system/physics.ts` — no spoilers there. ## Why one pass isn't enough Step 12's `safeMoveAndSlide` walks the walls **once, in array order**, fixing each overlap it meets. For one wall that's airtight. But a depenetration push is a _teleport_ — and a teleport can land you inside a wall the loop already checked and cleared, or one it hasn't reached yet (in which case it works, by luck of the ordering). A resolver whose correctness depends on the order of the wall array isn't a resolver — it's a coin flip. Two experiments in a scratch file, **predicting each outcome before running** (use your step-12 code as-is): 1. Hero `{x: 9, y: 1, w: 2, h: 2}`, walls `A = {x: 10, y: 0, w: 4, h: 4}` and `B = {x: 4, y: 0, w: 4.5, h: 1.4}`. The hero overlaps only `A`. Run one pass with the array `[A, B]`, then with `[B, A]`. Where does the hero end up in each case, and is it free? Explain the difference before moving on. 2. Walls `{x: 10, y: -5, w: 4, h: 10}` and `{x: 6.5, y: -5, w: 2, h: 10}` — a gap 1.5 wide. Hero (2 wide) at `{x: 9, y: 0}`. Apply `penetrationVector` pushes in a loop and log `x` each time. Does it converge? What number does `x` bounce between, and _why will it never stop_? ## The negotiation, and when it honestly fails The fix for experiment 1 is patience: don't do one pass — **repeat whole passes until a full pass finds nothing to fix**. That clean pass is your proof of freedom. Each pass is cheap, and in sane geometry it settles in one or two. But experiment 2 shows the negotiation can be _unwinnable_: when the gap is narrower than the box, **no overlap-free position exists**. No amount of math fixes that, because it isn't a math problem — it's a game-design question, and every game answers it differently. Mario between a Thwomp and the floor: crushed = death. Some engines let the wall shove you _through_ its partner. Zelda-flavored games mostly refuse the situation: solid wins, the hero holds still until the gap opens. We take that one — it's the smallest honest answer: **cap the passes, and if the cap fires, report it** (`settled: false`) instead of pretending. You already believe in caps; your step-10 loop carries one for exactly the same reason. Last session you proposed armoring `sweepInterval` against the `Infinity` directly. You can — see the optional section — but notice what that answer skips over: even with the `NaN` gone, _what should a crushed hero do?_ The kernel can't know; it only measures. Deciding is the resolver's job. Keeping **detection** and **policy** separate is the actual lesson of this step. ## Task Two functions in `crush.ts`: 1. `resolveOverlaps(box, walls)` — the negotiation: passes until clean or capped, returning `{x, y, settled}`. 2. `safeMoveAndSlide(box, v, walls)` — step 12's version rebuilt on top of it. Settled → sweep as usual. Crushed → **don't feed the sweep an overlapping box** (you know its opinion of those); the box stays where the resolver left it and waits. One warning on the `EPSILON` slack: apply it **only along the axis you actually pushed**. Before you port your step-12 slack code verbatim, play computer with `pv = { x: -3, y: 0 }` and watch what your two lines do to `y`. (That was my "bonus" question last session — it's still open, and one of the tests refuses to look away.) ```sh bun test workshop/steps/13-crush ``` ## Optional 1 — the airbag Defense in depth: even if some future caller hands `moveAndSlide` an overlapping box directly, it should return finite numbers — wrong-ish, maybe, but _finite_. You traced in step 12 exactly which value poisons the well. The loop already clamps it once (`Math.max(0, …)`) — find the **other** line that trusts `nearest.time` to be non-negative. The fix is almost nothing. Then write the test step 12 should have had: `moveAndSlide` (not `safe…`) with an overlapping start returns finite coordinates. ## Optional 2 — the capstone payoff Port `resolveOverlaps` into `11-capstone/game.js` and rebuild its `safeMoveAndSlide` on it. Now let the ledge squeeze the hero against the pillar, and against the border. Watch closely: instead of vanishing, the hero should squirt around the ledge like a watermelon seed pinched between two fingers. Then earn the effect: the ledge is 16 tall and the hero is 12. As the ledge digs deeper, which of `penetrationVector`'s four escapes wins, and at what depth does the winner change? That flip _is_ the watermelon seed.