import { describe, expect, it } from "vitest"; import { beltMapRadii, beltRocks, placeRocks } from "../src/map/belt"; import { MAX_ZOOM, MIN_ZOOM, toScreen } from "../src/map/camera"; import { hitTest } from "../src/map/hitTest"; import { layoutScene, spriteScale } from "../src/map/layout"; import { buildScene } from "../src/map/scene"; import { mapStyle } from "../src/map/style"; import { samplePlayerState } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const views = [ { width: 1280, height: 800 }, { width: 390, height: 844 }, ]; const zooms = [MIN_ZOOM, 1, 3, 12, MAX_ZOOM]; const scene = buildScene(samplePlayerState(), NOW); describe("the belt's rocks (Step 16.7, decision 0.5.104)", () => { it("places the same rocks every time", () => { expect(beltRocks(6)).toEqual(beltRocks(6)); expect(beltRocks(6)).toHaveLength(mapStyle.belt.rocks.count); }); it("keeps every rock inside the band, denser toward its middle", () => { const { inner, outer } = beltMapRadii(); const rocks = beltRocks(6); const middle = rocks.filter((r) => { const t = (Math.hypot(r.x, r.y) - inner) / (outer - inner); expect(t).toBeGreaterThanOrEqual(0); expect(t).toBeLessThanOrEqual(1); return t > 0.25 && t < 0.75; }); // A uniform scatter would put half in the middle half; the falloff puts more there. expect(middle.length / rocks.length).toBeGreaterThan(0.6); }); it("is mostly small rocks with a few larger ones, using every rock image", () => { const rocks = beltRocks(6); const { min, max } = mapStyle.belt.rocks.sizePx; const small = rocks.filter((r) => r.sizePx < min + (max - min) / 3).length; expect(small / rocks.length).toBeGreaterThan(0.6); expect(rocks.some((r) => r.sizePx > min + ((max - min) * 2) / 3)).toBe(true); expect(new Set(rocks.map((r) => r.piece)).size).toBe(6); }); it("never draws a rock over Ceres, at any zoom or screen size", () => { const rocks = beltRocks(6); for (const view of views) { for (const zoom of zooms) { const camera = { zoom, panX: 0, panY: 0 }; const layout = layoutScene(scene, view, camera); const ceres = layout.locations.find((l) => l.id === "ceres-station")!; const placed = placeRocks(rocks, view, camera, layout.scale, ceres); for (const { center, size } of placed) { const gap = Math.hypot(center.x - ceres.center.x, center.y - ceres.center.y); expect(gap).toBeGreaterThanOrEqual(ceres.size / 2 + mapStyle.belt.rocks.ceresMarginPx); expect(gap - size / 2).toBeGreaterThanOrEqual(ceres.size / 2); } // Rocks near Ceres were left out; the rest are drawn at the sprite scale. expect(placed.length).toBeLessThanOrEqual(rocks.length); for (const { rock, size } of placed) { expect(size).toBeCloseTo(rock.sizePx * spriteScale(zoom), 9); } } } }); it("lets a tap on a rock reach whatever is beneath it", () => { const view = views[0]!; const camera = { zoom: 1, panX: 0, panY: 0 }; const layout = layoutScene(scene, view, camera); const placed = placeRocks(beltRocks(6), view, camera, layout.scale, null); for (const { center } of placed) { const hit = hitTest(scene, center, view, camera); // Never the rock: empty map, or a location whose tap target is there. if (hit !== null) { expect(hit.kind).toBe("location"); const loc = layout.locations.find((l) => hit.kind === "location" && l.id === hit.id)!; expect(Math.hypot(center.x - loc.center.x, center.y - loc.center.y)).toBeLessThanOrEqual( loc.tap / 2, ); } } // And most rocks sit over empty map. expect( placed.filter(({ center }) => hitTest(scene, center, view, camera) === null).length, ).toBeGreaterThan(placed.length / 2); expect(toScreen({ x: 0, y: 0 }, view, camera)).toEqual(layout.sun.center); }); });