80 lines
3.1 KiB
JavaScript
80 lines
3.1 KiB
JavaScript
import { test, describe } from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { Terrain } from '../js/terrain.js';
|
||
|
|
|
||
|
|
describe('Terrain', () => {
|
||
|
|
test('heightfield is deterministic across instances (persistent worlds)', () => {
|
||
|
|
const a = new Terrain(2000, 100);
|
||
|
|
const b = new Terrain(2000, 100);
|
||
|
|
a.createMesh();
|
||
|
|
b.createMesh();
|
||
|
|
for (const [x, z] of [[0, 0], [317, -812], [-999, 999], [123.5, -45.25]]) {
|
||
|
|
assert.equal(a.getHeightAt(x, z), b.getHeightAt(x, z), `height differs at ${x},${z}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('heightfield is deterministic across rebuilds of the same instance', () => {
|
||
|
|
const t = new Terrain(2000, 100);
|
||
|
|
t.createMesh();
|
||
|
|
const first = t.getHeightAt(317, -812);
|
||
|
|
t.createMesh();
|
||
|
|
assert.equal(t.getHeightAt(317, -812), first);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('airport apron is flattened inside the radius and untouched outside it', () => {
|
||
|
|
const rough = new Terrain(2000, 100);
|
||
|
|
rough.buildHeightData(); // heightfield only, no flattening
|
||
|
|
const flat = new Terrain(2000, 100);
|
||
|
|
flat.createMesh(); // createMesh() flattens a 200 m radius
|
||
|
|
|
||
|
|
// The apron centre happens to be naturally flat in this world, so assert on the
|
||
|
|
// strongest reduction found anywhere inside the radius rather than on one point.
|
||
|
|
let best = null;
|
||
|
|
for (let x = -190; x <= 190; x += 5) {
|
||
|
|
for (let z = -190; z <= 190; z += 5) {
|
||
|
|
if (Math.hypot(x, z) > 190) continue;
|
||
|
|
const delta = rough.getHeightAt(x, z) - flat.getHeightAt(x, z);
|
||
|
|
if (!best || delta > best.delta) best = { x, z, delta };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert.ok(best && best.delta > 1, `createMesh() did not flatten inside the 200 m radius (best delta: ${best && best.delta})`);
|
||
|
|
|
||
|
|
// Flattening must stop at the radius, not bleed into the wider world.
|
||
|
|
for (const [x, z] of [[210, 0], [0, 210], [300, 300], [-400, 250]]) {
|
||
|
|
assert.equal(flat.getHeightAt(x, z), rough.getHeightAt(x, z), `flattening leaked outside the radius at ${x},${z}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('height outside the heightfield bounds is zero, not garbage', () => {
|
||
|
|
const t = new Terrain(2000, 100);
|
||
|
|
t.createMesh();
|
||
|
|
for (const [x, z] of [[1500, 0], [0, 1500], [-1500, -1500]]) {
|
||
|
|
assert.equal(t.getHeightAt(x, z), 0, `expected 0 outside bounds at ${x},${z}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('heights stay within a sane range for a 2000 m world', () => {
|
||
|
|
const t = new Terrain(2000, 100);
|
||
|
|
t.createMesh();
|
||
|
|
let min = Infinity;
|
||
|
|
let max = 0;
|
||
|
|
for (let x = -990; x <= 990; x += 30) {
|
||
|
|
for (let z = -990; z <= 990; z += 30) {
|
||
|
|
const h = t.getHeightAt(x, z);
|
||
|
|
min = Math.min(min, h);
|
||
|
|
max = Math.max(max, h);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
assert.ok(min >= 0, `negative terrain height: ${min}`);
|
||
|
|
assert.ok(max > 10 && max < 200, `implausible max terrain height: ${max}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('mesh has one vertex colour per vertex', () => {
|
||
|
|
const t = new Terrain(2000, 50);
|
||
|
|
const mesh = t.createMesh();
|
||
|
|
const pos = mesh.geometry.attributes.position;
|
||
|
|
const col = mesh.geometry.attributes.color;
|
||
|
|
assert.equal(col.count, pos.count, 'colour attribute count does not match position count');
|
||
|
|
assert.equal(col.itemSize, 3);
|
||
|
|
});
|
||
|
|
});
|