animations
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { goalLineX } from '../shared/net.js';
|
||||
import { resolveShotAim, SHOT_AIM } from '../src/game/shotAim.js';
|
||||
import { done, near, ok, section } from './harness.mjs';
|
||||
|
||||
/** Shortest-arc wrap to (−π, π]. */
|
||||
function wrap(a) {
|
||||
let x = a;
|
||||
while (x > Math.PI) x -= Math.PI * 2;
|
||||
while (x <= -Math.PI) x += Math.PI * 2;
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* EA-style shot aim: movement stick + wind-up skating places the puck;
|
||||
* Skill Stick is a light fine-tune.
|
||||
*/
|
||||
|
||||
section('a neutral shot goes where the body points');
|
||||
{
|
||||
const r = resolveShotAim({
|
||||
x: 0, z: 0, yaw: Math.PI / 2, vx: 0, vz: 0,
|
||||
moveX: 0, moveY: 0, skillAim: 0,
|
||||
});
|
||||
near(r.aimYaw, Math.PI / 2, 1e-9, 'facing +X → aim +X');
|
||||
near(r.placement, 0, 1e-6, 'no body lean');
|
||||
ok(r.targetZ === null, 'no net target without a move stick');
|
||||
}
|
||||
|
||||
section('skate velocity yaws the shot off pure facing');
|
||||
{
|
||||
// Facing +X, but skating hard toward +Z (body left when facing +X:
|
||||
// forward = (1,0), left = (−0? wait: right = (cos, -sin) of π/2 = (0,-1) = −Z;
|
||||
// so +Z is body left).
|
||||
const r = resolveShotAim({
|
||||
x: 0, z: 0, yaw: Math.PI / 2, vx: 0, vz: 5,
|
||||
moveX: 0, moveY: 0, skillAim: 0,
|
||||
});
|
||||
// Velocity heading is 0; aim should pull off π/2 toward 0.
|
||||
ok(r.aimYaw < Math.PI / 2 - 0.05, `velocity pulls aim (${r.aimYaw.toFixed(3)})`);
|
||||
ok(r.aimYaw > 0, 'but not all the way to pure velocity');
|
||||
}
|
||||
|
||||
section('move stick opens net corners when facing the goal');
|
||||
{
|
||||
// Attacker on the slot, facing the +X net. Camera behind them looking +X:
|
||||
// look dir = (+1, 0) = −(sin θ, cos θ) ⇒ θ = −π/2.
|
||||
const cameraYaw = -Math.PI / 2;
|
||||
|
||||
const right = resolveShotAim({
|
||||
x: 15, z: 0, yaw: Math.PI / 2, vx: 2, vz: 0,
|
||||
moveX: 1, moveY: 0.15, cameraYaw, skillAim: 0,
|
||||
goalX: goalLineX(1),
|
||||
});
|
||||
const left = resolveShotAim({
|
||||
x: 15, z: 0, yaw: Math.PI / 2, vx: 2, vz: 0,
|
||||
moveX: -1, moveY: 0.15, cameraYaw, skillAim: 0,
|
||||
goalX: goalLineX(1),
|
||||
});
|
||||
const middle = resolveShotAim({
|
||||
x: 15, z: 0, yaw: Math.PI / 2, vx: 2, vz: 0,
|
||||
moveX: 0, moveY: 1, cameraYaw, skillAim: 0,
|
||||
goalX: goalLineX(1),
|
||||
});
|
||||
|
||||
ok(right.targetZ != null, 'right stick engages net aim');
|
||||
ok(left.targetZ != null, 'left stick engages net aim');
|
||||
ok(
|
||||
right.targetZ * left.targetZ < 0,
|
||||
`opposite sticks open opposite posts (${right.targetZ?.toFixed(2)} vs ${left.targetZ?.toFixed(2)})`,
|
||||
);
|
||||
ok(Math.abs(right.targetZ) > 0.4, `opens a real corner (${right.targetZ?.toFixed(2)} m)`);
|
||||
if (middle.targetZ != null) {
|
||||
ok(
|
||||
Math.abs(middle.targetZ) < Math.abs(right.targetZ),
|
||||
'forward stick is closer to the middle than a full cut',
|
||||
);
|
||||
}
|
||||
ok(Math.abs(wrap(right.aimYaw - left.aimYaw)) > 0.08, 'left and right aim yaws diverge');
|
||||
}
|
||||
|
||||
section('Skill Stick is only a light fine-tune');
|
||||
{
|
||||
const base = resolveShotAim({
|
||||
x: 12, z: 0, yaw: Math.PI / 2, vx: 0, vz: 0,
|
||||
moveX: 0, moveY: 0, skillAim: 0,
|
||||
});
|
||||
const tuned = resolveShotAim({
|
||||
x: 12, z: 0, yaw: Math.PI / 2, vx: 0, vz: 0,
|
||||
moveX: 0, moveY: 0, skillAim: 1,
|
||||
});
|
||||
const delta = Math.abs(wrap(tuned.aimYaw - base.aimYaw));
|
||||
near(delta, SHOT_AIM.SKILL_RAD, 1e-9, `full skillAim is ${SHOT_AIM.SKILL_RAD} rad`);
|
||||
ok(delta < 0.25, 'far smaller than the old 0.6 rad primary aim');
|
||||
}
|
||||
|
||||
section('far from the net, stick does not magnetize to the mouth');
|
||||
{
|
||||
// Centre ice, range past the net-aim window — still free to dump wide.
|
||||
const r = resolveShotAim({
|
||||
x: 0, z: 0, yaw: Math.PI / 2, vx: 0, vz: 0,
|
||||
moveX: 1, moveY: 0, cameraYaw: -Math.PI / 2, skillAim: 0,
|
||||
goalX: goalLineX(1),
|
||||
});
|
||||
ok(r.targetZ === null, 'outside shooting range: no net corner lock');
|
||||
}
|
||||
|
||||
section('placement sign matches Skill Stick right for the animator');
|
||||
{
|
||||
// Skill Stick +X only → placement must be positive (poseWindup / poseShot).
|
||||
const r = resolveShotAim({
|
||||
x: 0, z: 0, yaw: 0, vx: 0, vz: 0,
|
||||
moveX: 0, moveY: 0, skillAim: 1,
|
||||
});
|
||||
ok(r.placement > 0.1, `skill right → positive placement (${r.placement.toFixed(3)})`);
|
||||
near(r.aimYaw, SHOT_AIM.SKILL_RAD, 1e-9, 'and yaw adds by the fine-tune amount');
|
||||
}
|
||||
|
||||
section('move stick right of body yields positive placement');
|
||||
{
|
||||
// Facing +Z (yaw 0). Body right is +X. Camera yaw 0: screen right is +X world.
|
||||
// Stick right → world +X → aim turns right of facing → positive placement.
|
||||
const r = resolveShotAim({
|
||||
x: 0, z: 0, yaw: 0, vx: 0, vz: 0,
|
||||
moveX: 1, moveY: 0, cameraYaw: 0, skillAim: 0,
|
||||
// No net: pure world stick pull (out of range / not forced)
|
||||
goalX: goalLineX(1),
|
||||
});
|
||||
// At centre ice net aim is off; stick right still pulls aimYaw toward +X (π/2).
|
||||
ok(r.aimYaw > 0.1, `stick right yaws positive (${r.aimYaw.toFixed(3)})`);
|
||||
ok(r.placement > 0.1, `and placement is positive for the pose (${r.placement.toFixed(3)})`);
|
||||
}
|
||||
|
||||
done('shotAim');
|
||||
Reference in New Issue
Block a user