104 lines
3.7 KiB
JavaScript
104 lines
3.7 KiB
JavaScript
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');
|