122 lines
3.5 KiB
JavaScript
122 lines
3.5 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|