91 lines
3.9 KiB
TypeScript
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);
|
|
});
|
|
});
|