Files
2026-08-03 16:04:46 -05:00

122 lines
3.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Player definition — identity traits that are not sim state.
*
* Appearance (body style, jersey) and locomotion (velocity, effort) live
* elsewhere. This bag is the stable "who is this skater" record: how they hold
* a stick, which side they shoot from, and anything else that picks a pose set
* rather than a frame of motion.
*
* Kept plain and three.js-free so the same record can travel over the wire.
*/
/** Which side of the body the stick lives on, and which way they shoot. */
export const SHOT_SIDE = Object.freeze({
LEFT: 'left',
RIGHT: 'right',
});
/**
* Defaults for a new player bag.
*
* Authored stickwork (GRIP targets, arm poses) is written for a **right**
* shot: top hand on the right, lower hand on the left, forehand at X in
* skater space. A **left** shot mirrors that set across the body midline.
*/
export const PLAYER_DEFAULTS = Object.freeze({
shotSide: SHOT_SIDE.RIGHT,
});
/**
* Accept the spellings we are likely to see and fold them to `'left' | 'right'`.
*
* @param {unknown} v
* @returns {'left' | 'right'}
*/
export function normalizeShotSide(v) {
if (v === SHOT_SIDE.LEFT || v === 'L' || v === 'l' || v === -1 || v === false) {
return SHOT_SIDE.LEFT;
}
if (v === SHOT_SIDE.RIGHT || v === 'R' || v === 'r' || v === 1 || v === true) {
return SHOT_SIDE.RIGHT;
}
return PLAYER_DEFAULTS.shotSide;
}
/**
* Sign used to mirror authored right-shot content onto a left shot.
* `+1` = as authored (right), `-1` = mirror across the sagittal plane (left).
*
* @param {unknown} side
* @returns {1 | -1}
*/
export function shotSign(side) {
return normalizeShotSide(side) === SHOT_SIDE.LEFT ? -1 : 1;
}
/**
* Top hand on the stick for this shot side.
* Right shot: right hand on top. Left shot: left hand on top.
*
* @param {unknown} side
* @returns {'L' | 'R'}
*/
export function topHandFor(side) {
return normalizeShotSide(side) === SHOT_SIDE.LEFT ? 'L' : 'R';
}
/**
* Lower (blade-side) hand — the one IK pins to the shaft.
*
* @param {unknown} side
* @returns {'L' | 'R'}
*/
export function lowerHandFor(side) {
return normalizeShotSide(side) === SHOT_SIDE.LEFT ? 'R' : 'L';
}
/**
* Rough real-world split (~62% left shot in the NHL). Used when a roster
* entry does not specify a side, so a lineup is not all clones.
*
* @param {number} u01 uniform in [0, 1)
* @returns {'left' | 'right'}
*/
export function rollShotSide(u01) {
const u = Number.isFinite(u01) ? u01 : 0.5;
return u < 0.62 ? SHOT_SIDE.LEFT : SHOT_SIDE.RIGHT;
}
/**
* Normalize a partial player bag. Missing keys take the defaults.
*
* @param {unknown} raw
* @returns {{ shotSide: 'left' | 'right' }}
*/
export function normalizePlayer(raw) {
if (!raw || typeof raw !== 'object') {
return { shotSide: PLAYER_DEFAULTS.shotSide };
}
return {
shotSide: normalizeShotSide(/** @type {{ shotSide?: unknown }} */ (raw).shotSide),
};
}
/**
* Compact wire form for roster snapshots.
* @returns {{ ss: 'L' | 'R' }}
*/
export function packPlayer(player) {
const p = normalizePlayer(player);
return { ss: p.shotSide === SHOT_SIDE.LEFT ? 'L' : 'R' };
}
/** Inverse of packPlayer — also accepts a full `{ shotSide }` bag. */
export function unpackPlayer(raw) {
if (!raw || typeof raw !== 'object') return normalizePlayer(null);
if ('shotSide' in raw) return normalizePlayer(raw);
const ss = /** @type {{ ss?: unknown }} */ (raw).ss;
if (ss === 'L' || ss === 'l') return { shotSide: SHOT_SIDE.LEFT };
if (ss === 'R' || ss === 'r') return { shotSide: SHOT_SIDE.RIGHT };
return normalizePlayer(null);
}