Add main menu, 3v3 with goalies, and dead-puck faceoffs.

Players pick 1-on-1 shootout or 3-on-3 scrimmage; 3v3 gets nets,
goalies, scoring, OOB whistles to the nearest faceoff circle, and
one-way board re-entry for skaters who leave the ice.
This commit is contained in:
ryanfitzpatrickio
2026-08-03 10:28:11 -05:00
parent 94d24205dc
commit dd819e991d
8 changed files with 1371 additions and 116 deletions
+59
View File
@@ -32,6 +32,65 @@ export const MARKINGS = Object.freeze({
zoneDotX: 20.2,
});
/**
* All nine faceoff dots: centre, four neutral-zone, four end-zone.
* Order is stable so tests and HUD labels can index if they want.
*/
export const FACEOFF_DOTS = Object.freeze([
Object.freeze({ id: 'centre', x: 0, z: 0 }),
Object.freeze({ id: 'nz-pp', x: MARKINGS.faceoffDotX, z: MARKINGS.faceoffDotZ }),
Object.freeze({ id: 'nz-pm', x: MARKINGS.faceoffDotX, z: -MARKINGS.faceoffDotZ }),
Object.freeze({ id: 'nz-mp', x: -MARKINGS.faceoffDotX, z: MARKINGS.faceoffDotZ }),
Object.freeze({ id: 'nz-mm', x: -MARKINGS.faceoffDotX, z: -MARKINGS.faceoffDotZ }),
Object.freeze({ id: 'ez-pp', x: MARKINGS.zoneDotX, z: MARKINGS.faceoffDotZ }),
Object.freeze({ id: 'ez-pm', x: MARKINGS.zoneDotX, z: -MARKINGS.faceoffDotZ }),
Object.freeze({ id: 'ez-mp', x: -MARKINGS.zoneDotX, z: MARKINGS.faceoffDotZ }),
Object.freeze({ id: 'ez-mm', x: -MARKINGS.zoneDotX, z: -MARKINGS.faceoffDotZ }),
]);
/** Nearest faceoff dot to a world point — where a whistle drops the next draw. */
export function nearestFaceoffDot(x, z) {
let best = FACEOFF_DOTS[0];
let bestD = Infinity;
for (const d of FACEOFF_DOTS) {
const dd = (d.x - x) * (d.x - x) + (d.z - z) * (d.z - z);
if (dd < bestD) {
bestD = dd;
best = d;
}
}
return best;
}
/**
* Is the puck still in play?
*
* Horizontal: must be on the ice surface (small inset so "on the boards" is
* still playable, but over the glass / past the outline is dead).
* Vertical: above the glass, under the slab, or impossibly high is unplayable.
*/
export function puckPlayable(x, y, z, radius = 0.0381) {
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
return { ok: false, reason: 'nan' };
}
// Far outside the barn entirely (escaped continuous collision).
if (Math.abs(x) > RINK.halfX + 4 || Math.abs(z) > RINK.halfZ + 4) {
return { ok: false, reason: 'escaped' };
}
// Under the ice or stuck in the slab.
if (y < -0.15) return { ok: false, reason: 'under' };
// Over the glass. Boards are ~1.07 m; glass is visual only above that.
if (y > RINK.boardHeight + RINK.glassHeight * 0.55) {
return { ok: false, reason: 'over' };
}
// Centre past the board line — the puck has left the playing surface.
// Tiny slack so a rattle against the boards does not whistle every contact.
if (rinkPenetration(x, z, 0).dist > radius * 0.75) {
return { ok: false, reason: 'out' };
}
return { ok: true, reason: '' };
}
/**
* Centre of the corner arc nearest (x, z), and the sign of the quadrant.
* Points outside the straight sections belong to exactly one corner.