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');