time for a snapshot
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# Step 08 — Resolve (move *to* the wall, not through it)
|
||||
|
||||
Detection is done. Now for **response** — actually reacting to the hit. This first
|
||||
half is almost embarrassingly small, but it introduces the idea the whole loop
|
||||
(step 10) is built on: **the frame isn't all-or-nothing.**
|
||||
|
||||
## Concept
|
||||
|
||||
`sweptAABB` hands you a `Hit` with a `time` between 0 and 1 — the fraction of the
|
||||
frame at which you'd collide. So instead of moving the full displacement `v`
|
||||
(which would bury you inside the wall), you move only the part of it that happens
|
||||
*before* impact:
|
||||
|
||||
```
|
||||
no hit → newPos = pos + v (nothing in the way: take the whole move)
|
||||
hit at t → newPos = pos + v * t (stop exactly at the contact point)
|
||||
```
|
||||
|
||||
That's it — you already have `add` and `scale`; this is them, gated on the hit.
|
||||
|
||||
### The leftover that matters
|
||||
|
||||
Here's the seed for step 10: if you hit at `t = 0.3`, you only used **30%** of
|
||||
this frame. The other **70%** is still owed to the player — they should keep
|
||||
moving for the rest of the frame, just not *into* the wall. That leftover time is
|
||||
exactly why sliding (step 09) and the loop (step 10) exist. A collision doesn't
|
||||
end the frame; it **interrupts** it.
|
||||
|
||||
> Real-engine footnote: production code usually moves to `t - EPSILON` (a hair
|
||||
> *short* of contact) so floating-point error can't leave the box a sliver inside
|
||||
> the wall, where the next frame's sweep would start already-overlapping. Your
|
||||
> `nage` does this with `Math.max(0, time - EPSILON)`. We keep the kata exact so
|
||||
> the numbers stay clean — just know that tiny backoff is there for a real reason.
|
||||
|
||||
## Task
|
||||
|
||||
Implement `resolve(pos, v, hit)` in `resolve.ts`: return the full move when `hit`
|
||||
is `null`, otherwise the position at contact. `vec/add/scale` and the `Hit` type
|
||||
are in `given.ts`.
|
||||
|
||||
```sh
|
||||
bun test workshop/steps/08-resolve
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
// Finished in earlier steps.
|
||||
export type Vec = { x: number; y: number };
|
||||
export const vec = (x: number, y: number): Vec => ({ x, y });
|
||||
export const add = (a: Vec, b: Vec): Vec => ({ x: a.x + b.x, y: a.y + b.y });
|
||||
export const scale = (a: Vec, s: number): Vec => ({ x: a.x * s, y: a.y * s });
|
||||
|
||||
/** From step 06/07. */
|
||||
export type Hit = { time: number; normal: Vec };
|
||||
@@ -0,0 +1,19 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { vec } from "./given.ts";
|
||||
import { resolve } from "./resolve.ts";
|
||||
|
||||
test("no collision -> full move", () => {
|
||||
expect(resolve(vec(0, 0), vec(10, 5), null)).toEqual({ x: 10, y: 5 });
|
||||
});
|
||||
|
||||
test("collision at t=0.3 -> stop at contact", () => {
|
||||
const hit = { time: 0.3, normal: vec(-1, 0) };
|
||||
const p = resolve(vec(0, 0), vec(10, 0), hit);
|
||||
expect(p.x).toBeCloseTo(3);
|
||||
expect(p.y).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
test("collision at t=0 -> don't move at all", () => {
|
||||
const hit = { time: 0, normal: vec(-1, 0) };
|
||||
expect(resolve(vec(5, 5), vec(10, 10), hit)).toEqual({ x: 5, y: 5 });
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { add, type Hit, scale, type Vec } from "./given.ts";
|
||||
|
||||
/**
|
||||
* The new position after this frame's move, stopping at a collision if there is
|
||||
* one.
|
||||
* hit === null -> pos + v (take the whole move)
|
||||
* hit -> pos + v * time (stop at the contact point)
|
||||
*/
|
||||
export function resolve(pos: Vec, v: Vec, hit: Hit | null): Vec {
|
||||
return hit ? add(pos, scale(v, hit.time)) : add(pos, v);
|
||||
}
|
||||
Reference in New Issue
Block a user