Release Healer Man 0.1.8
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { BufferAttribute, BufferGeometry, Vector3 } from "three";
|
||||
import { MeshBVH } from "three-mesh-bvh";
|
||||
import { MeshoptDecoder } from "meshoptimizer";
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const campaign = await import("../../src/game/generated/dungeonCampaignCatalog.json", {
|
||||
with: { type: "json" },
|
||||
}).then((module) => module.default);
|
||||
const useCollision = process.argv.includes("--collision");
|
||||
const requestedSlugs = new Set(process.argv.slice(2).filter((argument) => argument !== "--collision"));
|
||||
const definitions = campaign.definitions.filter((definition) => (
|
||||
!requestedSlugs.size || requestedSlugs.has(definition.id)
|
||||
));
|
||||
|
||||
if (!definitions.length) {
|
||||
throw new Error("No matching dungeon definitions. Pass one or more dungeon slugs.");
|
||||
}
|
||||
|
||||
await MeshoptDecoder.ready;
|
||||
const io = new NodeIO()
|
||||
.registerExtensions(ALL_EXTENSIONS)
|
||||
.registerDependencies({ "meshopt.decoder": MeshoptDecoder });
|
||||
const point = new Vector3();
|
||||
|
||||
function normalizerFor(accessor, array) {
|
||||
if (!accessor.getNormalized()) return (value) => value;
|
||||
if (array instanceof Int16Array) return (value) => Math.max(value / 32_767, -1);
|
||||
if (array instanceof Uint16Array) return (value) => value / 65_535;
|
||||
if (array instanceof Int8Array) return (value) => Math.max(value / 127, -1);
|
||||
if (array instanceof Uint8Array) return (value) => value / 255;
|
||||
return (value) => value;
|
||||
}
|
||||
|
||||
async function geometryTrees(file) {
|
||||
const document = await io.read(file);
|
||||
const trees = [];
|
||||
for (const node of document.getRoot().listNodes()) {
|
||||
const mesh = node.getMesh();
|
||||
if (!mesh) continue;
|
||||
const matrix = node.getWorldMatrix();
|
||||
for (const primitive of mesh.listPrimitives()) {
|
||||
const accessor = primitive.getAttribute("POSITION");
|
||||
const source = accessor?.getArray();
|
||||
if (!accessor || !source) continue;
|
||||
const normalize = normalizerFor(accessor, source);
|
||||
const positions = new Float32Array(source.length);
|
||||
for (let offset = 0; offset < source.length; offset += 3) {
|
||||
const x = normalize(source[offset]);
|
||||
const y = normalize(source[offset + 1]);
|
||||
const z = normalize(source[offset + 2]);
|
||||
positions[offset] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
|
||||
positions[offset + 1] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
|
||||
positions[offset + 2] = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
|
||||
}
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
||||
const indices = primitive.getIndices()?.getArray();
|
||||
if (indices) geometry.setIndex(new BufferAttribute(indices, 1));
|
||||
trees.push(new MeshBVH(geometry));
|
||||
}
|
||||
}
|
||||
return trees;
|
||||
}
|
||||
|
||||
function nearestDistance(trees, position) {
|
||||
point.fromArray(position);
|
||||
let distance = Infinity;
|
||||
for (const tree of trees) {
|
||||
const result = tree.closestPointToPoint(point);
|
||||
distance = Math.min(distance, result?.distance ?? Infinity);
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
|
||||
function percentile(sorted, proportion) {
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * proportion))] ?? 0;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const definition of definitions) {
|
||||
const geometryUrls = useCollision
|
||||
? definition.assets.collision.map((chunk) => chunk.url)
|
||||
: [definition.assets.navigation.url];
|
||||
const trees = (await Promise.all(geometryUrls.map((url) => (
|
||||
geometryTrees(path.join(projectRoot, "public", url.replace(/^\//, "")))
|
||||
)))).flat();
|
||||
const samples = [
|
||||
{ id: "entrance", position: definition.entrance.footPosition },
|
||||
...definition.staticSpawns.map((spawn) => ({ id: spawn.id, position: spawn.position })),
|
||||
...definition.roamingPacks.flatMap((pack) => (
|
||||
pack.waypoints.map((position, index) => ({ id: `${pack.id}:${index}`, position }))
|
||||
)),
|
||||
];
|
||||
const distances = samples
|
||||
.map((sample) => ({ ...sample, distance: nearestDistance(trees, sample.position) }))
|
||||
.sort((left, right) => left.distance - right.distance);
|
||||
const values = distances.map((sample) => sample.distance);
|
||||
results.push({
|
||||
dungeonId: definition.id,
|
||||
geometry: useCollision ? "collision" : "navigation",
|
||||
coordinateAlignment: definition.provenance.coordinateAlignment,
|
||||
coordinateTransform: definition.provenance.coordinateTransform,
|
||||
samples: values.length,
|
||||
withinOneMeter: values.filter((distance) => distance <= 1).length,
|
||||
medianDistance: percentile(values, 0.5),
|
||||
p90Distance: percentile(values, 0.9),
|
||||
maximumDistance: values.at(-1) ?? 0,
|
||||
furthest: distances.slice(-5).reverse().map(({ id, position, distance }) => ({
|
||||
id,
|
||||
position,
|
||||
distance,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
Reference in New Issue
Block a user