Files
2026-08-03 19:20:00 -05:00

134 lines
4.7 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.
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');