103 lines
3.8 KiB
JavaScript
103 lines
3.8 KiB
JavaScript
import { MARKINGS, RINK } from './rink.js';
|
||
|
||
/**
|
||
* The net, and what counts as a goal.
|
||
*
|
||
* Pure numbers and pure predicates, so the shootout logic and the tests can
|
||
* agree about scoring without a physics world in the room.
|
||
*/
|
||
|
||
/** Regulation: 6ft wide, 4ft tall, 44in deep. */
|
||
export const NET = Object.freeze({
|
||
width: 1.83,
|
||
height: 1.22,
|
||
depth: 1.12,
|
||
postRadius: 0.048,
|
||
/** Crease: 6ft radius arc off the goal line. */
|
||
creaseRadius: 1.83,
|
||
});
|
||
|
||
/**
|
||
* Goal line X for an end. `end` is +1 for the +X end, −1 for −X.
|
||
* The net's mouth sits *on* this line, opening back toward centre ice.
|
||
*/
|
||
export const goalLineX = (end) => end * MARKINGS.goalLine;
|
||
|
||
/**
|
||
* Has the puck fully crossed the line, between the posts and under the bar?
|
||
*
|
||
* "Fully" is the rule and it matters: a puck resting on the line is not a goal.
|
||
* The whole puck has to be past, so the test is against the puck's leading edge
|
||
* — its centre plus its radius.
|
||
*/
|
||
export function isGoal(puck, end, puckRadius = 0.0381) {
|
||
const line = goalLineX(end);
|
||
// Leading edge past the line, travelling into the net.
|
||
const past = end > 0 ? puck.x - puckRadius > line : puck.x + puckRadius < line;
|
||
if (!past) return false;
|
||
// ...but not out the back of it.
|
||
if (Math.abs(puck.x - line) > NET.depth) return false;
|
||
if (Math.abs(puck.z) > NET.width / 2 - puckRadius) return false;
|
||
return (puck.y ?? 0) < NET.height;
|
||
}
|
||
|
||
/** True once the puck is behind the goal line but wide or high — a miss. */
|
||
export function isWide(puck, end, puckRadius = 0.0381) {
|
||
const line = goalLineX(end);
|
||
const past = end > 0 ? puck.x - puckRadius > line : puck.x + puckRadius < line;
|
||
return past && !isGoal(puck, end, puckRadius);
|
||
}
|
||
|
||
/**
|
||
* Where a goalie should stand, given where the puck is.
|
||
*
|
||
* Angle play, which is the whole of goaltending positioning: stand on the line
|
||
* between the puck and the middle of the net, `depth` metres out from the goal
|
||
* line. Cover the angle and the shooter has nothing to shoot at; the rest is
|
||
* reflexes.
|
||
*
|
||
* Returns a point, clamped so the goalie never wanders past the posts by more
|
||
* than a pad's width — a goalie who chases the puck to the corner has left an
|
||
* open net, which reads as broken rather than as aggressive.
|
||
*/
|
||
export function goalieSpot(puck, end, depth = 0.55, out = { x: 0, z: 0 }) {
|
||
const line = goalLineX(end);
|
||
// Aim from the middle of the goal mouth toward the puck.
|
||
const dx = puck.x - line;
|
||
const dz = puck.z - 0;
|
||
const len = Math.hypot(dx, dz);
|
||
if (len < 1e-4) {
|
||
out.x = line - end * 0.1;
|
||
out.z = 0;
|
||
return out;
|
||
}
|
||
out.x = line + (dx / len) * depth;
|
||
out.z = (dz / len) * depth;
|
||
|
||
// Two clamps, and *both* were originally the wrong way round — between them
|
||
// they teleported the goalie onto the puck and then pinned them to the goal
|
||
// line, which threw away every bit of angle the depth was there to buy.
|
||
//
|
||
// "Out" means toward centre ice, which is decreasing x at the +X end. So
|
||
// coming out past the puck is `out.x < puck.x` there, and going behind the
|
||
// line is `out.x > line`.
|
||
if (end > 0 ? out.x < puck.x : out.x > puck.x) out.x = puck.x;
|
||
if (end > 0 ? out.x > line : out.x < line) out.x = line;
|
||
// Stay within the posts, plus a little for a pad sticking out.
|
||
const limit = NET.width / 2 + 0.22;
|
||
out.z = Math.max(-limit, Math.min(limit, out.z));
|
||
return out;
|
||
}
|
||
|
||
/** Centre ice, facing the end being shot at — where a shootout attempt starts. */
|
||
export function shootoutStart(end) {
|
||
return { x: 0, z: 0, yaw: end > 0 ? Math.PI / 2 : -Math.PI / 2 };
|
||
}
|
||
|
||
/** Is the puck still in a sensible place for a live attempt? */
|
||
export function attemptLive(puck, end) {
|
||
if (Math.abs(puck.z) > RINK.halfZ) return false;
|
||
// Past the goal line at that end, one way or another, ends it.
|
||
return end > 0 ? puck.x < goalLineX(end) + NET.depth : puck.x > goalLineX(end) - NET.depth;
|
||
}
|