animations
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clamp, lerpAngle, wrapAngle } from './scalar.js';
|
||||
import { clampToRink } from './rink.js';
|
||||
import { normalizePlayer } from './player.js';
|
||||
|
||||
/**
|
||||
* Skating locomotion.
|
||||
@@ -55,6 +56,7 @@ export const SKATE = Object.freeze({
|
||||
});
|
||||
|
||||
export function createSkaterState(id, spawn = {}, opts = {}) {
|
||||
const player = normalizePlayer(opts.player ?? opts);
|
||||
return {
|
||||
id,
|
||||
name: opts.name ?? `Skater ${id}`,
|
||||
@@ -62,6 +64,12 @@ export function createSkaterState(id, spawn = {}, opts = {}) {
|
||||
seed: opts.seed ?? 1337,
|
||||
team: opts.team ?? 0,
|
||||
|
||||
/**
|
||||
* Stable identity traits — shot side, and anything else that picks a pose
|
||||
* set rather than a frame of motion. See `shared/player.js`.
|
||||
*/
|
||||
shotSide: player.shotSide,
|
||||
|
||||
x: spawn.x ?? 0,
|
||||
y: 0,
|
||||
z: spawn.z ?? 0,
|
||||
|
||||
Reference in New Issue
Block a user