Bring main menu (1v1/3v3), scrimmage with nets and goalies, dead-puck whistles at the nearest faceoff circle, skater board re-entry, and Cloudflare Workers/Pages deploy config. Keep main jersey gear stack and compact goalie floaters.
103 lines
4.0 KiB
JavaScript
103 lines
4.0 KiB
JavaScript
import {
|
|
FACEOFF_DOTS, MARKINGS, RINK,
|
|
clampToRink, insideRink, nearestFaceoffDot, puckPlayable,
|
|
randomIcePoint, rinkOutline, rinkPenetration,
|
|
} from '../shared/rink.js';
|
|
import { done, near, ok, section } from './harness.mjs';
|
|
|
|
section('penetration on the straights');
|
|
{
|
|
ok(insideRink(0, 0), 'centre ice is on the ice');
|
|
ok(!insideRink(RINK.halfX + 1, 0), 'past the end boards is not');
|
|
ok(!insideRink(0, RINK.halfZ + 1), 'past the side boards is not');
|
|
|
|
const p = rinkPenetration(0, RINK.halfZ + 0.5);
|
|
near(p.dist, 0.5, 1e-9, 'side board penetration');
|
|
near(p.nz, -1, 1e-9, 'side board normal points back to centre');
|
|
}
|
|
|
|
section('penetration in the corners');
|
|
{
|
|
// The corner arc centre, pushed out along the diagonal by exactly the radius,
|
|
// has to land on the boards.
|
|
const cx = RINK.halfX - RINK.cornerR;
|
|
const cz = RINK.halfZ - RINK.cornerR;
|
|
const d = RINK.cornerR / Math.SQRT2;
|
|
const p = rinkPenetration(cx + d, cz + d);
|
|
near(p.dist, 0, 1e-9, 'diagonal from the corner centre lands on the boards');
|
|
near(Math.hypot(p.nx, p.nz), 1, 1e-9, 'corner normal is unit length');
|
|
ok(p.nx < 0 && p.nz < 0, 'corner normal points inward');
|
|
|
|
// A point in the corner quadrant but inside the arc is on the ice, even
|
|
// though it is outside neither straight wall — this is the case a plain
|
|
// rectangle test gets wrong.
|
|
ok(insideRink(cx + 1, cz + 1), 'inside the corner arc is on the ice');
|
|
ok(!insideRink(RINK.halfX - 0.5, RINK.halfZ - 0.5), 'the clipped corner is off the ice');
|
|
}
|
|
|
|
section('radius is respected');
|
|
{
|
|
ok(!insideRink(0, RINK.halfZ - 0.2, 0.36), 'a body wider than its gap does not fit');
|
|
ok(insideRink(0, RINK.halfZ - 1, 0.36), 'the same body fits with room to spare');
|
|
}
|
|
|
|
section('clamping kills inward velocity');
|
|
{
|
|
const s = { x: 0, z: RINK.halfZ + 0.2, vx: 1, vz: 3 };
|
|
const hit = clampToRink(s, 0.36, 0);
|
|
ok(hit, 'a body past the boards reports a hit');
|
|
ok(insideRink(s.x, s.z, 0.36), 'and is put back on the ice');
|
|
near(s.vz, 0, 1e-9, 'velocity into the boards is removed');
|
|
near(s.vx, 1, 1e-9, 'velocity along them is kept');
|
|
|
|
const bouncy = { x: 0, z: RINK.halfZ + 0.2, vx: 0, vz: 4 };
|
|
clampToRink(bouncy, 0.36, 0.5);
|
|
near(bouncy.vz, -2, 1e-9, 'restitution reverses half the closing speed');
|
|
|
|
const clear = { x: 0, z: 0, vx: 5, vz: 0 };
|
|
ok(!clampToRink(clear, 0.36), 'centre ice is not clamped');
|
|
near(clear.vx, 5, 1e-9, 'and keeps its speed');
|
|
}
|
|
|
|
section('outline follows the boards');
|
|
{
|
|
const outline = rinkOutline(8);
|
|
ok(outline.length === 36, 'four arcs of nine points');
|
|
let maxOff = 0;
|
|
for (const p of outline) maxOff = Math.max(maxOff, Math.abs(rinkPenetration(p.x, p.z).dist));
|
|
near(maxOff, 0, 1e-9, 'every outline point sits exactly on the boards');
|
|
}
|
|
|
|
section('random points land on the ice');
|
|
{
|
|
let n = 0;
|
|
const rand = () => ((n = (n * 1103515245 + 12345) % 2147483648) / 2147483648);
|
|
for (let i = 0; i < 500; i++) {
|
|
const p = randomIcePoint(rand, 3);
|
|
ok(insideRink(p.x, p.z, 3), `waypoint ${i} is 3m clear of the boards`);
|
|
}
|
|
}
|
|
|
|
section('faceoff dots');
|
|
{
|
|
ok(FACEOFF_DOTS.length === 9, 'nine faceoff dots');
|
|
ok(nearestFaceoffDot(0, 0).id === 'centre', 'centre ice → centre dot');
|
|
ok(nearestFaceoffDot(MARKINGS.zoneDotX, MARKINGS.faceoffDotZ).id === 'ez-pp', 'end-zone corner maps to itself');
|
|
const nearSide = nearestFaceoffDot(RINK.halfX + 2, MARKINGS.faceoffDotZ * 0.9);
|
|
ok(nearSide.id.startsWith('ez-p'), `puck past +X boards nearest end-zone (+ got ${nearSide.id})`);
|
|
const mid = nearestFaceoffDot(1, MARKINGS.faceoffDotZ);
|
|
ok(mid.id === 'nz-pp' || mid.id === 'centre', `near neutral +Z maps sensibly (${mid.id})`);
|
|
}
|
|
|
|
section('puck playable');
|
|
{
|
|
ok(puckPlayable(0, 0.02, 0).ok, 'centre ice is playable');
|
|
ok(!puckPlayable(RINK.halfX + 1, 0.02, 0).ok, 'past the end boards is dead');
|
|
ok(!puckPlayable(0, 3.5, 0).ok, 'over the glass is dead');
|
|
ok(!puckPlayable(0, -0.5, 0).ok, 'under the ice is dead');
|
|
ok(puckPlayable(0, 0.5, 0).ok, 'a lifted puck still on the ice plane is live');
|
|
}
|
|
|
|
done('rink');
|
|
|