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');