Merge goalie-tester worktree: modes, 3v3 goalies, OOB faceoffs, CF deploy.

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.
This commit is contained in:
ryanfitzpatrickio
2026-08-03 10:41:13 -05:00
parent 7d28facd68
commit f9aa382f30
18 changed files with 3121 additions and 130 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.