deadhead-space-freight-tycoon/apps/web/test/belt-rocks.test.ts

91 lines
3.9 KiB
TypeScript

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);
});
});