/** * Step and slide, on a grid written out here rather than the real map, so * the cases are the ones being argued about: a wall, an inner corner, and a * body already pressed against both. */ import { expect, test } from "bun:test"; import { BODY_R, circleBlocked, moveSpeed, RUN, slideCircle } from "./glide.ts"; import { moveCircle } from "./map.ts"; const TILE = 80; // A room with solid walls, open from (1,1) to (3,3). const room = ["#####", "#...#", "#...#", "#...#", "#####"]; const solid = (col: number, row: number): boolean => col < 0 || row < 0 || row >= room.length || col >= room[0]!.length || room[row]![col] === "#"; const slide = (x: number, y: number, dx: number, dy: number) => slideCircle(x, y, dx, dy, BODY_R, solid, TILE); test("an unobstructed step is the step it was asked for", () => { const to = slide(200, 200, 12, -7); expect(to.x).toBeCloseTo(212, 9); expect(to.y).toBeCloseTo(193, 9); expect(slide(200, 200, 0, 0)).toEqual({ x: 200, y: 200 }); }); test("a wall keeps the motion along it: running into it still slides", () => { // Hard against the right-hand wall (x = 320 - 16), running down and right. const to = slide(300, 160, 10, 10); expect(to.y).toBeCloseTo(170, 6); // the free axis goes through whole expect(to.x).toBeLessThanOrEqual(304); expect(to.x).toBeGreaterThan(300); // and the blocked one creeps up to the wall expect(circleBlocked(to.x, to.y, BODY_R, solid, TILE)).toBe(false); }); test("a corner is one place, whichever axis is resolved first", () => { // The bottom-right inner corner of the room: both walls at once. const diagonal = slide(280, 280, 40, 40); // Getting there one axis at a time, either order, lands in the same place. const alongX = slide(280, 280, 40, 0); const xThenY = slide(alongX.x, alongX.y, 0, 40); const alongY = slide(280, 280, 0, 40); const yThenX = slide(alongY.x, alongY.y, 40, 0); expect(xThenY.x).toBeCloseTo(diagonal.x, 3); expect(xThenY.y).toBeCloseTo(diagonal.y, 3); expect(yThenX.x).toBeCloseTo(diagonal.x, 3); expect(yThenX.y).toBeCloseTo(diagonal.y, 3); // And it is the corner: a body's radius from both walls, inside them. expect(diagonal.x).toBeGreaterThan(320 - BODY_R - 1.5); expect(diagonal.x).toBeLessThanOrEqual(320 - BODY_R); expect(diagonal.y).toBeGreaterThan(320 - BODY_R - 1.5); expect(diagonal.y).toBeLessThanOrEqual(320 - BODY_R); }); test("a body held against a wall stops moving instead of alternating", () => { // This is the skip: the old resolution snapped to the tile edge on the // blocked tick and moved freely on the next, so a player leaning on a // wall produced a position that changed every tick with no input change. let at = { x: 300, y: 200 }; const seen: number[] = []; for (let i = 0; i < 12; i++) { at = slide(at.x, at.y, 8.3, 0); seen.push(at.x); } const settled = seen.slice(4); expect(Math.max(...settled) - Math.min(...settled)).toBeLessThan(0.05); expect(Math.max(...settled)).toBeLessThanOrEqual(320 - BODY_R); }); test("a corner is a fixed point: pressed into it, nothing moves at all", () => { let at = { x: 300, y: 300 }; for (let i = 0; i < 6; i++) at = slide(at.x, at.y, 8.3, 8.3); const settled = slide(at.x, at.y, 8.3, 8.3); expect(settled.x).toBeCloseTo(at.x, 6); expect(settled.y).toBeCloseTo(at.y, 6); }); test("a step never ends inside a wall, from anywhere in the room", () => { for (let x = 96; x < 320 - 16; x += 7) for (let y = 96; y < 320 - 16; y += 7) for (const [dx, dy] of [ [30, 0], [-30, 0], [0, 30], [0, -30], [24, 24], [-24, 24], [24, -24], [-24, -24], ] as const) { const to = slide(x, y, dx, dy); expect(circleBlocked(to.x, to.y, BODY_R, solid, TILE)).toBe(false); } }); test("the real map moves by the same maths", () => { // Mid, walking east into whatever is there: the map's own binding. const to = moveCircle(1200, 640, 9, 0, BODY_R); expect(to.x).toBeGreaterThanOrEqual(1200); expect(to.x).toBeLessThanOrEqual(1209); expect(to.y).toBe(640); }); test("speed is the one table both sides read", () => { expect(moveSpeed({})).toBe(RUN); expect(moveSpeed({ haste: true })).toBeCloseTo(RUN * 1.35, 9); expect(moveSpeed({ slow: true })).toBeCloseTo(RUN * 0.55, 9); expect(moveSpeed({ heavy: true })).toBeCloseTo(RUN * 0.86, 9); expect(moveSpeed({ stopped: true, haste: true })).toBe(0); });