Add the Blender flashlight asset with a 600 lm beam and reduced bloom, shared-mode self badge, and compact phone controls. Relocate the summit marker, blend terrain seams, widen walking routes, and align local server configuration on port 2027.
83 lines
4.4 KiB
TypeScript
83 lines
4.4 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three';
|
|
import { createTerrainGeometry, terrainGeometryHeightAt } from '../src/scene/terrainGeometry.ts';
|
|
import { applySummitTerrain } from '../src/scene/summitTerrain.ts';
|
|
import { SUMMIT_TERRAIN } from '../src/constants/summitTerrain.ts';
|
|
import { SUMMIT_PLATEAU } from '../src/constants/summitPlateau.ts';
|
|
import { structurePosition } from '../src/scene/structures.ts';
|
|
import { STRUCTURES } from '../src/constants/structures.ts';
|
|
|
|
const meta = JSON.parse(readFileSync(new URL('../public/terrain/gwanaksan.json', import.meta.url), 'utf8'));
|
|
const bytes = readFileSync(new URL('../public/terrain/gwanaksan.f32', import.meta.url));
|
|
const heights = new Float32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
const start = structurePosition(SUMMIT_TERRAIN.start.latitude, SUMMIT_TERRAIN.start.longitude, meta.origin);
|
|
|
|
test('authored terrain edges join the surrounding slope without vertical steps', () => {
|
|
const geometry = createTerrainGeometry(meta, heights, true);
|
|
applySummitTerrain(geometry, meta);
|
|
const [left, top, right, bottom] = SUMMIT_PLATEAU.atlasBounds;
|
|
const height = (x: number, z: number) => terrainGeometryHeightAt(geometry, start.x+x, start.z+z);
|
|
try {
|
|
// Scan across all four seams, including the last authored half-metre cell.
|
|
for (const [edge, from, to, horizontal] of [
|
|
[left, top, bottom, true], [right, top, bottom, true],
|
|
[top, left, right, false], [bottom, left, right, false],
|
|
] as const) {
|
|
for (let along = from; along <= to; along += .5) {
|
|
for (let offset = -1; offset < 1; offset += .25) {
|
|
const a = horizontal ? height(edge+offset, along) : height(along, edge+offset);
|
|
const b = horizontal ? height(edge+offset+.25, along) : height(along, edge+offset+.25);
|
|
assert.ok(Math.abs(b-a) < .5, `terrain seam at ${edge}, ${along}: ${Math.abs(b-a)}m step over 0.25m`);
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
geometry.dispose();
|
|
}
|
|
});
|
|
|
|
test('local reconstruction preserves source data and every vertex outside 200m', () => {
|
|
const geometry = createTerrainGeometry(meta, heights, true);
|
|
const p = geometry.getAttribute('position');
|
|
const before = p.array.slice();
|
|
const source = heights.slice();
|
|
applySummitTerrain(geometry, meta);
|
|
let changed = 0;
|
|
for (let i=0; i<p.count; i++) {
|
|
const distance = Math.hypot(p.getX(i)-start.x, p.getZ(i)-start.z);
|
|
if (distance >= 200) assert.equal(p.getY(i), before[i*3+1]);
|
|
if (p.getY(i) !== before[i*3+1]) changed++;
|
|
}
|
|
assert.ok(changed > 100);
|
|
assert.deepEqual(heights, source);
|
|
geometry.dispose();
|
|
});
|
|
|
|
test('radar building and dome clear the saddle from the estimated eye position', () => {
|
|
const geometry = createTerrainGeometry(meta, heights, true);
|
|
applySummitTerrain(geometry, meta);
|
|
const ground = terrainGeometryHeightAt(geometry,start.x,start.z);
|
|
assert.ok(Math.abs(ground + meta.observerSurfaceElevation - 620) < 1);
|
|
const radar = structurePosition(STRUCTURES.radar.latitude,STRUCTURES.radar.longitude,meta.origin);
|
|
radar.y = terrainGeometryHeightAt(geometry,radar.x,radar.z) + 7;
|
|
const eye = new Vector3(start.x,ground+1.7,start.z);
|
|
assert.ok(radar.y > eye.y + 8, 'radar must stand above observer on its rock');
|
|
const material = new MeshBasicMaterial();
|
|
const mesh = new Mesh(geometry,material);
|
|
const ray = new Raycaster(eye,radar.clone().sub(eye).normalize(),0,eye.distanceTo(radar));
|
|
assert.equal(ray.intersectObject(mesh).length,0,'ridge must not hide radar building');
|
|
for (const [x,z] of [[start.x,start.z],[radar.x,radar.z],[0,65],[start.x+199,start.z],
|
|
[start.x-12,start.z-22],[start.x-2,start.z-45],[start.x+5,start.z-35],
|
|
[start.x-9,start.z-12],[start.x-9,start.z-11],[start.x-9,start.z-13]]) {
|
|
const hit = new Raycaster(new Vector3(x,1000,z),new Vector3(0,-1,0)).intersectObject(mesh)[0];
|
|
assert.ok(Math.abs(hit.point.y-terrainGeometryHeightAt(geometry,x,z))<1e-4);
|
|
}
|
|
const joint = terrainGeometryHeightAt(geometry,start.x-9,start.z-12);
|
|
const shoulders = (terrainGeometryHeightAt(geometry,start.x-9,start.z-11)
|
|
+ terrainGeometryHeightAt(geometry,start.x-9,start.z-13))/2;
|
|
assert.ok(shoulders-joint>.15,'large fracture must be a physical depression, not only a painted line');
|
|
geometry.dispose(); material.dispose();
|
|
});
|