386 lines
14 KiB
JavaScript
386 lines
14 KiB
JavaScript
import { E } from '../../core/math.js';
|
||
import { clamp, lerp } from '../../../shared/scalar.js';
|
||
import * as THREE from 'three';
|
||
|
||
/**
|
||
* Upper-body authoring for everything done with the stick.
|
||
*
|
||
* These are *override* poses, not whole-body states. They write the arms and
|
||
* some spine, and the animator blends them over the skating pose by a weight —
|
||
* because you keep skating while you shoot, and a shot that stopped the legs
|
||
* would read as a cutscene.
|
||
*
|
||
* Each one is a function of a single phase 0..1 so the animator can drive it
|
||
* from a timer, hold it (wind-up), or run it once and blend out (shoot, pass,
|
||
* poke). Poses are authored for a **right** shot (top hand right, lower hand
|
||
* left, forehand at −X). A left shot runs the same functions and then
|
||
* `mirrorStickwork` swaps the arms and flips the coil across the midline.
|
||
*/
|
||
|
||
/**
|
||
* Bones the stickwork layer *replaces*. The arms belong to the stick whenever
|
||
* it is being used — there is no meaningful blend between "swinging with the
|
||
* stride" and "holding a stick", they are different arms.
|
||
*/
|
||
export const STICK_ARMS = [
|
||
'clavicleR', 'upperArmR', 'forearmR', 'handR',
|
||
'clavicleL', 'upperArmL', 'forearmL', 'handL',
|
||
];
|
||
|
||
/**
|
||
* Bones the layer *adds to*. The spine is already carrying the skating lean and
|
||
* the bank; a shot's coil is a twist on top of that, not instead of it.
|
||
* Replacing these was what flattened the forward lean the moment a stick
|
||
* appeared, and stood everybody up.
|
||
*/
|
||
export const STICK_SPINE = ['spine1', 'spine2', 'spine3', 'neck', 'head'];
|
||
|
||
export const STICK_BONES = STICK_ARMS.concat(STICK_SPINE);
|
||
|
||
/** Left/right arm pairs the mirror swaps. */
|
||
const ARM_PAIRS = [
|
||
['clavicleL', 'clavicleR'],
|
||
['upperArmL', 'upperArmR'],
|
||
['forearmL', 'forearmR'],
|
||
['handL', 'handR'],
|
||
];
|
||
|
||
const _mirrorL = new THREE.Euler();
|
||
const _mirrorR = new THREE.Euler();
|
||
|
||
/**
|
||
* Mirror a right-shot stickwork pose onto a left shot.
|
||
*
|
||
* Arms swap sides with yaw/roll flipped; spine coil flips the same way. Call
|
||
* after any stickwork writer when `shotSign < 0`. Idempotent only if you do
|
||
* not call it twice — the animator applies it once per layer write.
|
||
*/
|
||
export function mirrorStickwork(P) {
|
||
for (const [l, r] of ARM_PAIRS) {
|
||
if (!P.q[l] || !P.q[r]) continue;
|
||
_mirrorL.setFromQuaternion(P.q[l], 'XYZ');
|
||
_mirrorR.setFromQuaternion(P.q[r], 'XYZ');
|
||
E(P.q[l], _mirrorR.x, -_mirrorR.y, -_mirrorR.z);
|
||
E(P.q[r], _mirrorL.x, -_mirrorL.y, -_mirrorL.z);
|
||
}
|
||
for (const n of STICK_SPINE) {
|
||
if (!P.q[n]) continue;
|
||
_mirrorL.setFromQuaternion(P.q[n], 'XYZ');
|
||
E(P.q[n], _mirrorL.x, -_mirrorL.y, -_mirrorL.z);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The neutral carry, and the hustle variant.
|
||
*
|
||
* `hustle` 0..1 slides between two-hands-ready and the one-handed dangle a
|
||
* skater falls into when they are just trying to move: the stick goes out in
|
||
* front, the left arm leaves it and swings with the stride.
|
||
*
|
||
* Carry is authored from the motion-reference sheet (ready stance / puck carry):
|
||
* both hands in front of the torso, shaft angled down to the ice, blade a
|
||
* little to the forehand side — not parked out on the hip with the off-hand
|
||
* floating. The right arm has to sit close enough that the left can actually
|
||
* reach the shaft: the arm is only ~0.54 m long, so a top hand 40 cm off
|
||
* centre puts the stick out of reach no matter what the IK does.
|
||
*/
|
||
export function poseCarry(P, { hustle = 0, reach = 0, lateral = 0 }) {
|
||
const h = clamp(hustle, 0, 1);
|
||
// Skill Stick +X is "push right"; bone +X is the skater's left. Negate so
|
||
// the arms lean the same way the blade goes.
|
||
const side = -lateral;
|
||
|
||
// Right arm: top hand. Across the body and out in front at about waist /
|
||
// lower-chest height. Hustle extends it forward and frees the left side.
|
||
// Roll signs are mirrored: right arm tucks on *positive* Z, left on negative.
|
||
E(P.q.clavicleR, 0.03, lerp(-0.04, -0.1, h), 0.06);
|
||
E(
|
||
P.q.upperArmR,
|
||
lerp(-0.42, -0.7, h) + reach * 0.2,
|
||
lerp(0.42, 0.05, h) + side * 0.28,
|
||
lerp(0.68, 0.38, h),
|
||
);
|
||
E(
|
||
P.q.forearmR,
|
||
lerp(-1.35, -0.75, h) - reach * 0.15,
|
||
lerp(0.02, 0.12, h),
|
||
lerp(0.32, 0.16, h),
|
||
);
|
||
E(P.q.handR, lerp(-0.12, -0.08, h), lerp(0.12, 0.04, h), lerp(0.04, -0.12, h));
|
||
|
||
// Left arm: lower hand on the shaft when settled. Seeded near the stick so
|
||
// the IK only has to finish the last few centimetres, not haul it across the
|
||
// body. At full hustle it leaves the stick and opens for the stride swing.
|
||
E(P.q.clavicleL, 0.03, lerp(0.06, 0.02, h), lerp(-0.06, -0.02, h));
|
||
E(
|
||
P.q.upperArmL,
|
||
lerp(-0.38, -0.66, h) + reach * 0.12,
|
||
lerp(0.1, 0.16, h) + side * 0.18,
|
||
lerp(-0.48, -0.2, h),
|
||
);
|
||
E(
|
||
P.q.forearmL,
|
||
lerp(-1.32, -0.72, h),
|
||
lerp(-0.08, 0, h),
|
||
lerp(0.18, 0.08, h),
|
||
);
|
||
E(P.q.handL, -0.1, 0, 0.08 * (1 - h));
|
||
|
||
// Soft coil over the stick when both hands are on it; opens up when hustling.
|
||
E(P.q.spine1, 0.02 * h, lerp(-0.04, 0.02, h) + side * 0.05, 0);
|
||
E(P.q.spine2, 0.02 * h, lerp(-0.05, 0.02, h) + side * 0.05, 0);
|
||
E(P.q.spine3, 0.02, lerp(-0.04, 0, h), 0);
|
||
}
|
||
|
||
/**
|
||
* Wind-up. `phase` 0..1 is how loaded the shot is, and it is *held* — the
|
||
* animator parks here for as long as the Skill Stick is pulled back.
|
||
*
|
||
* Both hands stay on the stick, relatively square, and the whole grip draws
|
||
* back and *up* as a unit from the carry — not a golf swing that parks the
|
||
* stick behind the head. The torso coils open so the downswing has something
|
||
* to spend.
|
||
*/
|
||
export function poseWindup(P, { phase = 0, aim = 0 }) {
|
||
const w = clamp(phase, 0, 1);
|
||
// Aim on the Skill Stick is "push right"; bone +Y twist toward the skater's
|
||
// left is the opposite sign.
|
||
const side = -aim;
|
||
|
||
// Torso coils open on the shot side — enough load, not a full pirouette.
|
||
E(P.q.spine1, -0.03 - 0.04 * w, -0.1 - 0.18 * w + side * 0.08, -0.02 * w);
|
||
E(P.q.spine2, -0.04 - 0.05 * w, -0.12 - 0.22 * w + side * 0.1, -0.03 * w);
|
||
E(P.q.spine3, -0.02 - 0.04 * w, -0.08 - 0.16 * w + side * 0.08, -0.02 * w);
|
||
// Eyes stay on the target while the body turns away from it.
|
||
E(P.q.neck, 0.02, 0.12 + 0.18 * w - side * 0.16, 0);
|
||
E(P.q.head, 0.02, 0.1 + 0.14 * w - side * 0.18, 0);
|
||
|
||
// Carry-like arms that lift and pull back *together* on the forehand side.
|
||
// Keeping the seed close to the two-handed carry means the lower-hand IK only
|
||
// finishes the last few centimetres, and the hands stay square on the shaft.
|
||
E(P.q.clavicleR, -0.02 * w, -0.05 * w, -0.05);
|
||
E(
|
||
P.q.upperArmR,
|
||
lerp(-0.42, -0.3, w) + side * 0.03,
|
||
lerp(0.42, 0.22, w) + side * 0.1,
|
||
lerp(0.68, 0.62, w),
|
||
);
|
||
E(
|
||
P.q.forearmR,
|
||
lerp(-1.35, -1.2, w),
|
||
lerp(0.02, 0.05, w),
|
||
lerp(0.32, 0.28, w),
|
||
);
|
||
E(P.q.handR, lerp(-0.12, -0.09, w), lerp(0.12, 0.13, w), lerp(0.04, 0.06, w));
|
||
|
||
E(P.q.clavicleL, 0.03, lerp(0.06, 0.07, w), lerp(-0.06, -0.03, w));
|
||
E(
|
||
P.q.upperArmL,
|
||
lerp(-0.38, -0.26, w),
|
||
lerp(0.1, 0.16, w) + side * 0.06,
|
||
lerp(-0.48, -0.4, w),
|
||
);
|
||
E(
|
||
P.q.forearmL,
|
||
lerp(-1.32, -1.2, w),
|
||
lerp(-0.08, -0.09, w),
|
||
lerp(0.18, 0.15, w),
|
||
);
|
||
E(P.q.handL, -0.08, 0, lerp(0.08, 0.02, w));
|
||
}
|
||
|
||
/**
|
||
* Shot swing. `phase` 0..1 runs once, fast.
|
||
*
|
||
* Three beats, matching the grip path: still loaded → square through the puck
|
||
* with the blade on the ice → follow-through high across the body. Front-loaded
|
||
* easing so contact reads early, not in the middle of a slow blend.
|
||
*/
|
||
export function poseShot(P, { phase = 0, power = 1, aim = 0 }) {
|
||
const t = clamp(phase, 0, 1);
|
||
// Fast out of the coil, then settle into the follow.
|
||
const s = 1 - (1 - t) * (1 - t);
|
||
const p = clamp(power, 0.2, 1);
|
||
// 0..1 through the "square on the ice" window, then 0..1 into the follow.
|
||
// Contact lands around t≈0.28 so the blade is flush when the puck leaves.
|
||
const down = clamp(t / 0.28, 0, 1);
|
||
const through = clamp((t - 0.28) / 0.55, 0, 1);
|
||
const square = 1 - (1 - down) * (1 - down);
|
||
|
||
const twist = lerp(-0.28 * p, 0.38 * p, s);
|
||
E(P.q.spine1, -0.04 + 0.1 * s, twist * 0.9, 0.03 * s);
|
||
E(P.q.spine2, -0.05 + 0.12 * s, twist, 0.04 * s);
|
||
E(P.q.spine3, -0.03 + 0.08 * s, twist * 0.8, 0.03 * s);
|
||
E(P.q.neck, 0.02, -twist * 0.5 + aim * 0.2, 0);
|
||
E(P.q.head, 0.02, -twist * 0.4 + aim * 0.25, 0);
|
||
|
||
// Arms: wind-up square → contact square (carry-like) → follow high.
|
||
// The early half is deliberately close to the carry pose so both hands stay
|
||
// on the shaft and the stick reads flat through the ice, not rotating off it.
|
||
E(
|
||
P.q.clavicleR,
|
||
lerp(lerp(-0.02, 0.02, square), 0.04, through),
|
||
lerp(lerp(-0.06, -0.04, square), 0.1, through),
|
||
-0.05,
|
||
);
|
||
E(
|
||
P.q.upperArmR,
|
||
lerp(lerp(-0.3, -0.42, square), -1.0 * p, through),
|
||
lerp(lerp(0.22, 0.4, square), 0.28, through),
|
||
lerp(lerp(0.62, 0.68, square), -0.08, through),
|
||
);
|
||
E(
|
||
P.q.forearmR,
|
||
lerp(lerp(-1.2, -1.34, square), -0.4, through),
|
||
lerp(0.05, 0.12, through),
|
||
lerp(0.28, -0.08, through),
|
||
);
|
||
E(P.q.handR, -0.1, 0.1, 0.12);
|
||
|
||
E(
|
||
P.q.clavicleL,
|
||
0.03,
|
||
lerp(lerp(0.07, 0.06, square), -0.04, through),
|
||
lerp(-0.03, 0.05, through),
|
||
);
|
||
E(
|
||
P.q.upperArmL,
|
||
lerp(lerp(-0.26, -0.38, square), -0.28, through),
|
||
lerp(lerp(0.16, 0.12, square), 0.1, through),
|
||
lerp(lerp(-0.4, -0.48, square), 0.48, through),
|
||
);
|
||
E(
|
||
P.q.forearmL,
|
||
lerp(lerp(-1.2, -1.32, square), -0.55, through),
|
||
lerp(-0.09, -0.2, through),
|
||
lerp(0.15, -0.14, through),
|
||
);
|
||
E(P.q.handL, -0.08, 0, -0.08);
|
||
}
|
||
|
||
/**
|
||
* Pass: a flat sweep, no coil and no lift. Shorter and lower than a shot,
|
||
* because a pass that looks like a shot makes the two impossible to read apart
|
||
* at a glance — which matters more for a teammate watching than for the passer.
|
||
*/
|
||
export function posePass(P, { phase = 0, aim = 0 }) {
|
||
const t = clamp(phase, 0, 1);
|
||
const s = Math.sin(t * Math.PI); // out and back
|
||
const sweep = lerp(-0.18, 0.34, 1 - (1 - t) * (1 - t));
|
||
|
||
E(P.q.spine1, 0.03 * s, sweep * 0.6, 0);
|
||
E(P.q.spine2, 0.04 * s, sweep * 0.7, 0);
|
||
E(P.q.spine3, 0.03 * s, sweep * 0.5, 0);
|
||
E(P.q.neck, 0, -sweep * 0.4 + aim * 0.2, 0);
|
||
E(P.q.head, 0, -sweep * 0.3 + aim * 0.2, 0);
|
||
|
||
E(P.q.clavicleR, 0.02, -0.04, -0.05);
|
||
E(P.q.upperArmR, -0.34 - 0.3 * s, -0.34 + sweep * 0.5, -0.4 - 0.12 * s);
|
||
E(P.q.forearmR, -0.72 - 0.24 * s, 0.12, -0.12);
|
||
E(P.q.handR, -0.12, 0.06, 0.18);
|
||
|
||
E(P.q.clavicleL, 0.02, 0.05, 0.05);
|
||
E(P.q.upperArmL, -0.58 - 0.18 * s, 0.36 + sweep * 0.3, 0.3);
|
||
E(P.q.forearmL, -1.06 - 0.16 * s, -0.24, -0.18);
|
||
E(P.q.handL, -0.1, 0, -0.12);
|
||
}
|
||
|
||
/**
|
||
* Stick spread (RB hold): dominant arm drives the blade on a *frontal* cone —
|
||
* centre is out in front of the chest, Skill Stick X (`waggle` −1..1) sweeps
|
||
* left↔right up to ±90°. Never behind the hips. Non-dominant arm is off the
|
||
* shaft, out to the side in a ready stance. Slight crouch.
|
||
*
|
||
* Authored for a right shot (top hand R). Left shots mirror.
|
||
*/
|
||
export function poseSpread(P, { waggle = 0, phase = 1 } = {}) {
|
||
const w = clamp(phase, 0, 1);
|
||
// Skill Stick +X = screen-right. Match the stick target's sign flip so the
|
||
// arm and blade travel the same way (see skateAnimator SPREAD_ARC).
|
||
const a = -clamp(waggle, -1, 1);
|
||
const ang = a * (Math.PI / 2);
|
||
const fwd = Math.cos(ang); // 1 centre, 0 at full left/right
|
||
const lat = Math.sin(ang); // body-right after the stick-sign flip
|
||
// Bone space: +Y twist that opens the right arm across the body is opposite
|
||
// of body-right, so lateral into the arm uses −lat.
|
||
const side = -lat;
|
||
|
||
// Sit into it — eyes up the ice; torso opens with the lateral sweep only.
|
||
E(P.q.spine1, 0.1 * w, -0.04 * w + side * 0.12, 0);
|
||
E(P.q.spine2, 0.12 * w, -0.05 * w + side * 0.14, 0);
|
||
E(P.q.spine3, 0.08 * w, -0.04 * w + side * 0.12, 0);
|
||
E(P.q.neck, -0.08 * w, 0.06 * w - side * 0.14, 0);
|
||
E(P.q.head, -0.06 * w, 0.06 * w - side * 0.16, 0);
|
||
|
||
// Dominant (right) arm: extended low. Centre = straight out in front;
|
||
// full stick swings the arm across the body front to each side (not
|
||
// forward/back along the midline).
|
||
E(P.q.clavicleR, 0.04 * w, -0.06 * w + side * 0.22, 0.05 * w);
|
||
E(
|
||
P.q.upperArmR,
|
||
// Pitch: reach low toward the ice; a bit more extended when out front.
|
||
lerp(-0.42, -0.28, w) - fwd * 0.12,
|
||
// Yaw: centre ≈ forward of the shoulder; lat swings ±90° in front.
|
||
lerp(0.42, 0.12, w) + side * 1.05,
|
||
lerp(0.68, 0.0, w) + Math.abs(lat) * 0.1,
|
||
);
|
||
E(
|
||
P.q.forearmR,
|
||
lerp(-1.35, -0.2, w) - fwd * 0.08,
|
||
lerp(0.02, 0.04, w) + side * 0.25,
|
||
lerp(0.32, 0.0, w),
|
||
);
|
||
E(
|
||
P.q.handR,
|
||
lerp(-0.12, -0.2, w),
|
||
lerp(0.12, 0.0, w) + side * 0.22,
|
||
lerp(0.04, 0.06, w),
|
||
);
|
||
|
||
// Non-dominant (left) arm: detached, out to the side and slightly forward —
|
||
// ready stance, does not follow the waggle.
|
||
E(P.q.clavicleL, 0.05 * w, 0.12 * w, -0.06);
|
||
E(
|
||
P.q.upperArmL,
|
||
lerp(-0.38, -0.26, w),
|
||
lerp(0.1, 0.68, w), // open wide to the left
|
||
lerp(-0.48, 0.55, w), // rolled out, clear of the shaft
|
||
);
|
||
E(
|
||
P.q.forearmL,
|
||
lerp(-1.32, -0.92, w),
|
||
-0.05,
|
||
lerp(0.18, -0.14, w),
|
||
);
|
||
E(P.q.handL, -0.05, 0.04, 0.02);
|
||
}
|
||
|
||
/**
|
||
* Poke check: a stab. One hand, the whole arm extending forward with the body
|
||
* reaching after it, back almost as fast as it went out.
|
||
*/
|
||
export function posePoke(P, { phase = 0 }) {
|
||
const t = clamp(phase, 0, 1);
|
||
// Out fast, back slower.
|
||
const s = t < 0.35 ? t / 0.35 : 1 - (t - 0.35) / 0.65;
|
||
const jab = clamp(s, 0, 1);
|
||
|
||
E(P.q.spine1, 0.06 * jab, -0.14 * jab, 0);
|
||
E(P.q.spine2, 0.07 * jab, -0.18 * jab, 0);
|
||
E(P.q.spine3, 0.05 * jab, -0.14 * jab, 0);
|
||
E(P.q.neck, -0.04 * jab, 0.1 * jab, 0);
|
||
E(P.q.head, -0.04 * jab, 0.1 * jab, 0);
|
||
|
||
// Right arm thrusts out and down toward the ice.
|
||
E(P.q.clavicleR, 0.02 + 0.06 * jab, -0.06 - 0.16 * jab, -0.05);
|
||
E(P.q.upperArmR, -0.34 - 0.5 * jab, -0.28 - 0.12 * jab, -0.36 + 0.14 * jab);
|
||
E(P.q.forearmR, -0.78 + 0.66 * jab, 0.1, -0.12);
|
||
E(P.q.handR, -0.12, 0.06, 0.18);
|
||
|
||
// Left arm comes off the stick and back for balance.
|
||
E(P.q.clavicleL, 0.02, 0.04, 0.04);
|
||
E(P.q.upperArmL, -0.5 + 0.2 * jab, 0.28 - 0.2 * jab, 0.34 + 0.16 * jab);
|
||
E(P.q.forearmL, -0.9 + 0.3 * jab, -0.16, -0.14);
|
||
E(P.q.handL, -0.1, 0, -0.1);
|
||
}
|