feat: initialize sunrise with 100-player shared rooms
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// Verify the exported artifacts using the same Three.js loader as the application.
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { Box3, Raycaster, Vector3 } from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { STRUCTURES } from '../src/constants/structures.ts';
|
||||
|
||||
const directory = resolve('public/models/summit');
|
||||
const manifest = JSON.parse(readFileSync(resolve(directory, 'manifest.json'), 'utf8'));
|
||||
const specs = [...STRUCTURES.towers, { id: 'weather-radar', ...STRUCTURES.radar }, ...STRUCTURES.decks];
|
||||
assert.deepEqual(manifest.assets.map(a => a.id).sort(), specs.map(s => s.id).sort());
|
||||
const report = { loader: 'Three.js GLTFLoader', checks: 'passed', assets: [] };
|
||||
for (const asset of manifest.assets) {
|
||||
const spec = specs.find(s => s.id === asset.id);
|
||||
const bytes = readFileSync(resolve(directory, asset.file));
|
||||
assert.equal(bytes.toString('ascii', 0, 4), 'glTF');
|
||||
assert.equal(bytes.readUInt32LE(4), 2);
|
||||
assert.equal(bytes.readUInt32LE(8), bytes.length);
|
||||
const jsonLength = bytes.readUInt32LE(12);
|
||||
const json = JSON.parse(bytes.toString('utf8', 20, 20 + jsonLength));
|
||||
assert.equal(json.scenes.length, 1, `${asset.id}: exactly one scene`);
|
||||
assert.equal(json.scenes[0].nodes.length, 1, `${asset.id}: exactly one asset root`);
|
||||
assert.ok(!(json.images?.length), 'No reference images or external textures');
|
||||
assert.ok(!json.buffers.some(b => b.uri), 'Embedded geometry only');
|
||||
assert.ok(!(json.cameras?.length), 'No studio camera');
|
||||
assert.ok(!json.nodes.some(n => /Cube|Studio|Camera/.test(n.name ?? '')), 'No unrelated scene objects');
|
||||
assert.equal(asset.latitude, spec.latitude);
|
||||
assert.equal(asset.longitude, spec.longitude);
|
||||
assert.equal(asset.heading, spec.heading ?? 0);
|
||||
for (const [key, value] of Object.entries(asset.dimensions)) assert.equal(value, spec[key]);
|
||||
const data = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
const gltf = await new GLTFLoader().parseAsync(data, '');
|
||||
const root = gltf.scene.getObjectByName(asset.id);
|
||||
assert.ok(root, `${asset.id}: named root survives export`);
|
||||
assert.ok(root.position.length() < 1e-6, 'No gallery offset baked into GLB');
|
||||
assert.equal(root.userData.dimensionsEstimated, true);
|
||||
let triangles = 0, meshes = 0;
|
||||
root.traverse(object => {
|
||||
if (!object.isMesh) return;
|
||||
meshes++;
|
||||
const geometry = object.geometry;
|
||||
const positions = geometry.getAttribute('position');
|
||||
assert.ok(positions.count > 0);
|
||||
assert.ok([...positions.array].every(Number.isFinite), 'Finite vertex coordinates');
|
||||
const normals = geometry.getAttribute('normal');
|
||||
assert.ok(normals && [...normals.array].every(Number.isFinite), 'Finite normals');
|
||||
if (geometry.index) {
|
||||
assert.ok([...geometry.index.array].every(i => i < positions.count));
|
||||
triangles += geometry.index.count / 3;
|
||||
} else triangles += positions.count / 3;
|
||||
assert.ok(object.material.isMeshStandardMaterial, 'PBR material survives');
|
||||
});
|
||||
gltf.scene.updateMatrixWorld(true);
|
||||
const bounds = new Box3().setFromObject(root);
|
||||
const size = bounds.getSize(new Vector3());
|
||||
assert.ok([...bounds.min.toArray(), ...bounds.max.toArray()].every(Number.isFinite));
|
||||
if (asset.kind === 'tower') {
|
||||
assert.ok(Math.abs(bounds.max.y - (spec.height + 4)) < .01, 'Original tower height + 4m antenna');
|
||||
assert.ok(Math.abs(bounds.min.y) < .15, 'Base at origin');
|
||||
} else if (asset.kind === 'radar') {
|
||||
assert.ok(Math.abs(bounds.max.y - (spec.buildingHeight + 1.8 * spec.radius)) < .1);
|
||||
const ray = new Raycaster(new Vector3(0, 3, 20), new Vector3(0, 0, -1));
|
||||
assert.ok(ray.intersectObject(root, true).length, 'Building is visible to app raycasts');
|
||||
} else {
|
||||
const ray = new Raycaster(new Vector3(.31, 2, .037), new Vector3(0, -1, 0));
|
||||
const hits = ray.intersectObject(root, true);
|
||||
assert.ok(hits.length && Math.abs(hits[0].point.y) < .02, 'Upper walking surface at y=0');
|
||||
assert.ok(bounds.min.z < -spec.length / 2 - spec.stairLength, 'Stairs extend toward -Z');
|
||||
assert.ok(asset.terrainSupportAnchors.length > 0);
|
||||
assert.ok(asset.terrainSupportAnchors.every(a => a.position.every(Number.isFinite)));
|
||||
}
|
||||
report.assets.push({ id: asset.id, bytes: bytes.length, meshes, triangles,
|
||||
bounds: { min: bounds.min.toArray(), max: bounds.max.toArray() },
|
||||
sizeMetres: size.toArray().map(n => Number(n.toFixed(3))) });
|
||||
}
|
||||
report.totalBytes = report.assets.reduce((n, a) => n + a.bytes, 0);
|
||||
report.totalTriangles = report.assets.reduce((n, a) => n + a.triangles, 0);
|
||||
writeFileSync(resolve('assets/summit/verification.json'), JSON.stringify(report, null, 2));
|
||||
console.table(report.assets.map(({ id, bytes, meshes, triangles, sizeMetres }) => ({ id, KiB: +(bytes / 1024).toFixed(1), meshes, triangles, sizeMetres: sizeMetres.join(' × ') })));
|
||||
console.log(`PASS: ${report.assets.length} assets; ${report.totalTriangles} triangles; ${(report.totalBytes / 1024 / 1024).toFixed(2)} MiB`);
|
||||
Reference in New Issue
Block a user