Initial commit

This commit is contained in:
ryanfitzpatrickio
2026-08-03 10:28:11 -05:00
parent 65bfc3dcb4
commit 6c08153e42
63 changed files with 15790 additions and 1 deletions
+103
View File
@@ -0,0 +1,103 @@
import * as THREE from 'three';
import { createGoalie } from '../src/character/goalie.js';
import { done, ok, section } from './harness.mjs';
/**
* Goalie presentation: skeleton, gear, and stance selection.
*
* Save logic and angle play are covered by shootout.mjs — this file pins the
* things that used to be a capsule-and-box placeholder.
*/
const DT = 1 / 60;
function make() {
const scene = new THREE.Group();
const goalie = createGoalie(null, scene, { end: 1, team: 1, seed: 42 });
return { scene, goalie };
}
section('goalie is a skinned skeleton, not a capsule');
{
const { goalie } = make();
ok(goalie.skelData.list.length >= 20, `has a full bone list (${goalie.skelData.list.length})`);
ok(goalie.bodyMesh?.isSkinnedMesh, 'body is a SkinnedMesh');
ok(goalie.gear.padL.parent === goalie.skelData.bones.shinL, 'left pad is on the left shin');
ok(goalie.gear.padR.parent === goalie.skelData.bones.shinR, 'right pad is on the right shin');
ok(goalie.gear.trapper.parent === goalie.skelData.bones.handL, 'trapper is on the left hand');
ok(goalie.gear.blocker.parent === goalie.skelData.bones.handR, 'blocker is on the right hand');
ok(goalie.gear.mask.parent === goalie.skelData.bones.head, 'mask is on the head');
ok(goalie.gear.stick.parent === goalie.skelData.bones.handR, 'paddle is in the blocker hand');
goalie.destroy();
}
section('nothing produces NaN while tracking');
{
const { goalie } = make();
let bad = false;
for (let i = 0; i < 180; i++) {
goalie.update(DT, {
x: 15 + i * 0.08,
y: 0.1 + 0.5 * Math.sin(i * 0.1),
z: Math.sin(i * 0.07) * 2,
});
for (const b of goalie.skelData.list) {
for (const e of b.matrixWorld.elements) {
if (!Number.isFinite(e)) bad = true;
}
}
}
ok(!bad, 'bone matrices stay finite');
goalie.destroy();
}
section('stances respond to the puck');
{
const { goalie } = make();
// Far out: ready.
for (let i = 0; i < 60; i++) goalie.update(DT, { x: 10, y: 0.5, z: 0 });
ok(goalie.animator.state === 'ready', `idle crease is ready (${goalie.animator.state})`);
// Low and closing: butterfly.
for (let i = 0; i < 90; i++) {
goalie.update(DT, { x: 12 + i * 0.15, y: 0.1, z: 0.2 });
}
ok(
goalie.animator.state === 'butterfly',
`low attack draws a butterfly (${goalie.animator.state})`,
);
// High and close: reach.
for (let i = 0; i < 45; i++) {
goalie.update(DT, { x: goalie.pos.x + 2, y: 1.25, z: goalie.pos.z });
}
ok(goalie.animator.state === 'reach', `high puck draws a reach (${goalie.animator.state})`);
goalie.destroy();
}
section('goalie drops low in the butterfly');
{
const { goalie } = make();
for (let i = 0; i < 40; i++) goalie.update(DT, { x: 18, y: 0.5, z: 0 });
const readyY = goalie.skelData.bones.root.position.y;
const foot = new THREE.Vector3();
goalie.skelData.bones.footL.getWorldPosition(foot);
ok(Math.abs(foot.y - 0.085) < 0.04, `ready feet are on the ice (y=${foot.y.toFixed(3)})`);
for (let i = 0; i < 80; i++) {
goalie.update(DT, { x: goalie.pos.x + 1.2, y: 0.08, z: 0 });
}
const flyY = goalie.skelData.bones.root.position.y;
ok(flyY < readyY - 0.1, `butterfly drops the hips (${readyY.toFixed(2)}${flyY.toFixed(2)})`);
goalie.skelData.bones.footL.getWorldPosition(foot);
ok(Math.abs(foot.y - 0.085) < 0.04, `butterfly feet stay on the ice (y=${foot.y.toFixed(3)})`);
// Pads flare wider than the ready stance.
const inv = new THREE.Matrix4().copy(goalie.mover.matrixWorld).invert();
const fL = foot.clone().applyMatrix4(inv);
goalie.skelData.bones.footR.getWorldPosition(foot);
const fR = foot.clone().applyMatrix4(inv);
ok(Math.abs(fL.x - fR.x) > 0.9, `butterfly opens the stance (width ${(fL.x - fR.x).toFixed(2)})`);
goalie.destroy();
}
done('goalie');