import { describe, expect, test } from "bun:test"; import { angleColumn, bandLight, castRay, columnAngle, columnOffset, FOV, hitResult, project, projection, projectResult, sliceHeight, smokeAlong, spriteColumnVisible, wallLight, } from "../../static/engine/raycast.js"; const W = 900; describe("columns and angles", () => { test("the middle column looks straight ahead", () => { expect(columnOffset(W / 2 - 0.5, W)).toBeCloseTo(0, 6); expect(columnAngle(W / 2 - 0.5, W, 1.2)).toBeCloseTo(1.2, 6); }); test("the outer columns sit at the edges of the field of view", () => { expect(columnOffset(0, W)).toBeCloseTo(-FOV / 2, 2); expect(columnOffset(W - 1, W)).toBeCloseTo(FOV / 2, 2); }); test("a column maps to an angle and back again", () => { for (const col of [0, 123, 450, 899]) { expect(angleColumn(columnOffset(col, W), W)).toBeCloseTo(col + 0.5, 4); } }); test("columns are not spread evenly — that is what keeps walls straight", () => { const middle = columnOffset(451, W) - columnOffset(450, W); const edge = columnOffset(1, W) - columnOffset(0, W); expect(edge).toBeLessThan(middle); }); }); describe("wall slices", () => { test("a wall twice as far is half as tall", () => { const proj = projection(W); expect(sliceHeight(160, proj, 80)).toBeCloseTo(sliceHeight(80, proj, 80) / 2, 6); }); test("a north–south face is darker than an east–west one at the same range", () => { expect(wallLight(200, 1, 80)).toBeLessThan(wallLight(200, 0, 80)); }); test("further is dimmer, and never fully black", () => { expect(wallLight(900, 0, 80)).toBeLessThan(wallLight(120, 0, 80)); expect(wallLight(5000, 1, 80)).toBeGreaterThan(0); }); test("the floor is dimmest at the horizon", () => { expect(bandLight(2, 200)).toBeLessThan(bandLight(180, 200)); }); }); // A four-by-four room: solid border, open inside. const room = (col: number, row: number): boolean => col <= 0 || row <= 0 || col >= 3 || row >= 3; describe("castRay", () => { const out = hitResult(); test("finds the wall straight ahead and calls it an east–west face", () => { castRay(room, 120, 120, 0, 80, 2000, out); expect(out.wall).toBe(true); expect(out.dist).toBeCloseTo(120, 6); // from x=120 to the face at x=240 expect(out.side).toBe(0); expect(out.col).toBe(3); }); test("a ray upward strikes a north–south face", () => { castRay(room, 120, 120, -Math.PI / 2, 80, 2000, out); expect(out.wall).toBe(true); expect(out.dist).toBeCloseTo(40, 6); expect(out.side).toBe(1); expect(out.row).toBe(0); }); test("a ray that runs out of range reports no wall", () => { castRay(room, 120, 120, 0, 80, 50, out); expect(out.wall).toBe(false); expect(out.dist).toBe(50); }); test("standing inside a wall is zero distance, not an infinite loop", () => { castRay(room, 10, 10, 0.3, 80, 2000, out); expect(out.dist).toBe(0); expect(out.wall).toBe(true); }); test("a diagonal ray agrees with walking the same line by hand", () => { castRay(room, 120, 120, Math.PI / 4, 80, 2000, out); let step = 0; while (step < 2000 && !room(Math.floor((120 + Math.cos(Math.PI / 4) * step) / 80), Math.floor((120 + Math.sin(Math.PI / 4) * step) / 80))) step += 0.25; expect(out.dist).toBeCloseTo(step, 0); }); test("it fills the caller's object and allocates nothing", () => { const same = castRay(room, 120, 120, 0, 80, 2000, out); expect(same).toBe(out); }); }); describe("projecting a body onto the screen", () => { const proj = projection(W); const out = projectResult(); test("something dead ahead lands in the middle of the screen", () => { project(100, 100, 0, 500, 100, W, proj, 80, out); expect(out.behind).toBe(false); expect(out.column).toBeCloseTo(W / 2, 4); expect(out.perp).toBeCloseTo(400, 6); }); test("something to the right lands right of centre", () => { project(100, 100, 0, 500, 260, W, proj, 80, out); expect(out.column).toBeGreaterThan(W / 2); }); test("something behind the camera is not drawn at all", () => { project(100, 100, 0, -400, 100, W, proj, 80, out); expect(out.behind).toBe(true); expect(out.size).toBe(0); }); test("the further body is the smaller one", () => { project(100, 100, 0, 300, 100, W, proj, 80, out); const near = out.size; project(100, 100, 0, 900, 100, W, proj, 80, out); expect(out.size).toBeLessThan(near); }); test("perpendicular depth, not straight-line distance, so the edges do not bulge", () => { project(100, 100, 0, 500, 300, W, proj, 80, out); expect(out.perp).toBeLessThan(out.dist); }); }); describe("z-clip", () => { test("a body nearer than the wall behind it is drawn", () => { expect(spriteColumnVisible(300, 900)).toBe(true); }); test("a body behind the corner is hidden", () => { expect(spriteColumnVisible(900, 300)).toBe(false); }); test("a body flush against the wall does not bleed through it", () => { expect(spriteColumnVisible(299, 300)).toBe(false); }); }); describe("smoke as a volume", () => { test("a ray through the middle of a smoke is fully fogged", () => { expect(smokeAlong(0, 0, 1, 0, 400, 0, 110, 1000)).toBe(1); }); test("a ray that misses it is not fogged at all", () => { expect(smokeAlong(0, 0, 1, 0, 400, 400, 110, 1000)).toBe(0); }); test("a wall in front of the smoke is not fogged by it", () => { expect(smokeAlong(0, 0, 1, 0, 400, 0, 110, 200)).toBe(0); }); test("clipping the edge fogs less than going through the middle", () => { const edge = smokeAlong(0, 0, 1, 0, 400, 100, 110, 1000); expect(edge).toBeGreaterThan(0); expect(edge).toBeLessThan(1); }); test("standing in the middle of one is nearly opaque in every direction", () => { expect(smokeAlong(400, 0, 1, 0, 400, 0, 110, 1000)).toBeGreaterThan(0.9); expect(smokeAlong(400, 0, 0, -1, 400, 0, 110, 1000)).toBeGreaterThan(0.9); }); });