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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user