Release Healer Man 0.1.7

This commit is contained in:
phenom committed 2026-08-22 15:53:43 -04:00
1 parent b12fb84859
commit eca6b5db9b
99 files changed
+1931 -150

No files matched your search

+62
View File
@@ -11,6 +11,10 @@ export interface PreparedCollisionTrimesh {
readonly indices: ArrayLike<number>;
}
// A surface must be within 30 degrees of vertical to occlude combat sight.
// This deliberately rejects floors, ramps, slopes, and ceilings.
const MAX_SIGHT_BLOCKER_NORMAL_Y = 0.5;
export function prepareCollision(root: Object3D): Group {
root.updateWorldMatrix(true, true);
const inverseRootMatrix = root.matrixWorld.clone().invert();
@@ -60,3 +64,61 @@ export function collisionTrimeshes(root: Object3D): readonly PreparedCollisionTr
});
return result;
}
export function collisionLineOfSightTrimeshes(root: Object3D): readonly PreparedCollisionTrimesh[] {
const result: PreparedCollisionTrimesh[] = [];
root.traverse((node) => {
const mesh = node as Mesh;
if (!mesh.isMesh) return;
const position = mesh.geometry.getAttribute("position");
const sourceIndices = mesh.geometry.index?.array
?? Uint32Array.from({ length: position.count }, (_, vertex) => vertex);
const blockerSourceIndices: number[] = [];
for (let offset = 0; offset + 2 < sourceIndices.length; offset += 3) {
const first = Number(sourceIndices[offset]);
const second = Number(sourceIndices[offset + 1]);
const third = Number(sourceIndices[offset + 2]);
const firstX = position.getX(first);
const firstY = position.getY(first);
const firstZ = position.getZ(first);
const abX = position.getX(second) - firstX;
const abY = position.getY(second) - firstY;
const abZ = position.getZ(second) - firstZ;
const acX = position.getX(third) - firstX;
const acY = position.getY(third) - firstY;
const acZ = position.getZ(third) - firstZ;
const normalX = abY * acZ - abZ * acY;
const normalY = abZ * acX - abX * acZ;
const normalZ = abX * acY - abY * acX;
const normalLength = Math.hypot(normalX, normalY, normalZ);
if (normalLength <= Number.EPSILON) continue;
if (Math.abs(normalY) / normalLength > MAX_SIGHT_BLOCKER_NORMAL_Y) continue;
blockerSourceIndices.push(first, second, third);
}
if (!blockerSourceIndices.length) return;
const remappedVertices: number[] = [];
const remappedIndices = new Uint32Array(blockerSourceIndices.length);
const remap = new Map<number, number>();
blockerSourceIndices.forEach((sourceIndex, indexOffset) => {
let remappedIndex = remap.get(sourceIndex);
if (remappedIndex === undefined) {
remappedIndex = remap.size;
remap.set(sourceIndex, remappedIndex);
remappedVertices.push(
position.getX(sourceIndex),
position.getY(sourceIndex),
position.getZ(sourceIndex),
);
}
remappedIndices[indexOffset] = remappedIndex;
});
result.push({
id: `${mesh.uuid}-line-of-sight`,
vertices: Float32Array.from(remappedVertices),
indices: remappedIndices,
});
});
return result;
}