Initial commit

This commit is contained in:
ryanfitzpatrickio
2026-08-03 06:43:21 -05:00
commit 7ee3e9d02f
63 changed files with 15792 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
import * as THREE from 'three';
export const V3 = (x = 0, y = 0, z = 0) => new THREE.Vector3(x, y, z);
export const UP = V3(0, 1, 0);
export const FWD = V3(0, 0, 1);
export const clamp = (x, a, b) => (x < a ? a : x > b ? b : x);
export const lerp = (a, b, t) => a + (b - a) * t;
export const smooth = (t) => t * t * (3 - 2 * t);
export function assert(cond, msg) {
if (!cond) throw new Error('ASSERT FAILED: ' + msg);
}
export function lerpAngle(a, b, t) {
let d = b - a;
while (d > Math.PI) d -= Math.PI * 2;
while (d < -Math.PI) d += Math.PI * 2;
return a + d * t;
}
const _sd1 = new THREE.Vector3();
const _sd2 = new THREE.Vector3();
/** Distance from point `p` to segment a-b; writes the closest point into `out`. */
export function segDist(p, a, b, out) {
_sd1.subVectors(b, a);
_sd2.subVectors(p, a);
const t = clamp(_sd2.dot(_sd1) / Math.max(1e-9, _sd1.lengthSq()), 0, 1);
out.copy(a).addScaledVector(_sd1, t);
return p.distanceTo(out);
}
const _u = new THREE.Vector3();
const _v = new THREE.Vector3();
const _w = new THREE.Vector3();
/**
* Closest distance between two segments, writing the closest point on each
* into `outA` / `outB`.
*
* Used to work out which limb hit which limb: both ragdolls are 18 capsules,
* and a capsule is a segment plus a radius, so the nearest pair of segments is
* the nearest pair of body parts. Standard Ericson clamped-parameter solve —
* the degenerate cases (either segment a point, or the two parallel) all fall
* out of the denominator guards rather than needing separate branches.
*/
export function segSegDistance(p1, q1, p2, q2, outA, outB) {
_u.subVectors(q1, p1);
_v.subVectors(q2, p2);
_w.subVectors(p1, p2);
const a = _u.dot(_u);
const b = _u.dot(_v);
const c = _v.dot(_v);
const d = _u.dot(_w);
const e = _v.dot(_w);
const D = a * c - b * b;
let sN;
let sD = D;
let tN;
let tD = D;
if (D < 1e-9) {
// Parallel or degenerate: pin the first parameter and solve the second.
sN = 0;
sD = 1;
tN = e;
tD = c;
} else {
sN = b * e - c * d;
tN = a * e - b * d;
if (sN < 0) {
sN = 0;
tN = e;
tD = c;
} else if (sN > sD) {
sN = sD;
tN = e + b;
tD = c;
}
}
if (tN < 0) {
tN = 0;
if (-d < 0) sN = 0;
else if (-d > a) sN = sD;
else {
sN = -d;
sD = a;
}
} else if (tN > tD) {
tN = tD;
if (-d + b < 0) sN = 0;
else if (-d + b > a) sN = sD;
else {
sN = -d + b;
sD = a;
}
}
const s = Math.abs(sD) < 1e-9 ? 0 : sN / sD;
const t = Math.abs(tD) < 1e-9 ? 0 : tN / tD;
outA.copy(p1).addScaledVector(_u, s);
outB.copy(p2).addScaledVector(_v, t);
return outA.distanceTo(outB);
}
const _euler = new THREE.Euler();
/** Write XYZ euler angles into an existing quaternion without allocating. */
export function E(out, x, y, z, order) {
_euler.set(x, y, z, order || 'XYZ');
return out.setFromEuler(_euler);
}
export { _euler };
/** Merge indexed BufferGeometries that share an attribute set. */
export function mergeGeoms(list) {
let vTotal = 0;
let iTotal = 0;
const attrNames = Object.keys(list[0].attributes);
for (const g of list) {
vTotal += g.attributes.position.count;
iTotal += g.index.count;
}
const out = new THREE.BufferGeometry();
const arrays = {};
for (const name of attrNames) {
const itemSize = list[0].attributes[name].itemSize;
const Ctor = list[0].attributes[name].array.constructor;
arrays[name] = new Ctor(vTotal * itemSize);
}
const index = new (vTotal > 65535 ? Uint32Array : Uint16Array)(iTotal);
let vOff = 0;
let iOff = 0;
for (const g of list) {
const n = g.attributes.position.count;
for (const name of attrNames) {
arrays[name].set(g.attributes[name].array, vOff * g.attributes[name].itemSize);
}
const gi = g.index.array;
for (let i = 0; i < gi.length; i++) index[iOff + i] = gi[i] + vOff;
vOff += n;
iOff += gi.length;
}
for (const name of attrNames) {
out.setAttribute(name, new THREE.BufferAttribute(arrays[name], list[0].attributes[name].itemSize));
}
out.setIndex(new THREE.BufferAttribute(index, 1));
return out;
}
/** Normalize an arbitrary geometry to position/normal/uv + index so it can merge. */
export function stripAttrs(g) {
const out = new THREE.BufferGeometry();
out.setAttribute('position', g.attributes.position);
out.setAttribute('normal', g.attributes.normal);
const n = g.attributes.position.count;
out.setAttribute('uv', g.attributes.uv || new THREE.Float32BufferAttribute(new Float32Array(n * 2), 2));
if (g.index) out.setIndex(g.index);
else {
const idx = [];
for (let i = 0; i < n; i++) idx.push(i);
out.setIndex(idx);
}
return out;
}
export function disposeObject(root) {
root.traverse((o) => {
if (o.geometry) o.geometry.dispose();
if (o.material) {
const mats = Array.isArray(o.material) ? o.material : [o.material];
for (const m of mats) {
for (const k of Object.keys(m)) if (m[k] && m[k].isTexture) m[k].dispose();
m.dispose();
}
}
});
}
+27
View File
@@ -0,0 +1,27 @@
// Seeded PRNG. One integer seed drives every generated detail of a fighter.
//
// The showcase this grew out of used a module-level generator, which is fine
// for one character on screen. A match has at least two, and they have to be
// independently reproducible from their own seeds, so the generator is an
// object that gets threaded through the builders instead.
export function makeRng(seed) {
let a = seed | 0;
const f = () => {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
return {
seed,
f,
range: (lo, hi) => lo + (hi - lo) * f(),
int: (lo, hi) => Math.floor(lo + (hi + 0.9999 - lo) * f()),
pick: (arr) => arr[Math.floor(f() * arr.length) % arr.length],
// Independent sub-stream, so adding a generator in one place doesn't shift
// every value drawn after it.
fork: (salt) => makeRng((Math.imul(seed ^ salt, 0x9e3779b1) ^ (seed >>> 3)) | 0),
};
}