import test from "node:test"; import assert from "node:assert/strict"; import { updateRemotePlayer, smoothRemotePlayer, } from "../player-interpolation.js"; const radians = (degrees) => (degrees * Math.PI) / 180; const near = (actual, expected, tolerance = 1e-11) => assert.ok( Math.abs(actual - expected) < tolerance, `Expected ${actual} to be within ${tolerance} of ${expected}`, ); test("network updates change targets without snapping the rendered pose", () => { const previous = updateRemotePlayer(null, { id: "remote", position: [1, 2, 3], yaw: 0.2, name: "Before", }); const packet = { id: "remote", position: [5, 6, 7], yaw: 1.2, name: "After", }; const player = updateRemotePlayer(previous, packet); assert.deepEqual(player.position, [1, 2, 3]); assert.equal(player.yaw, 0.2); assert.deepEqual(player.target, [5, 6, 7]); assert.equal(player.targetYaw, 1.2); assert.equal(player.name, "After"); smoothRemotePlayer(player, 0.25); assert.deepEqual(player.position, [2, 3, 4]); near(player.yaw, 0.45); assert.deepEqual(player.target, [5, 6, 7]); assert.equal(player.targetYaw, 1.2); assert.deepEqual(packet.position, [5, 6, 7]); }); test("a packet arriving mid-turn continues from the current rendered angle", () => { let player = updateRemotePlayer(null, { position: [0, 0, 0], yaw: 0 }); player = updateRemotePlayer(player, { position: [4, 0, 0], yaw: 1 }); smoothRemotePlayer(player, 0.5); player = updateRemotePlayer(player, { position: [8, 0, 0], yaw: 2 }); assert.deepEqual(player.position, [2, 0, 0]); near(player.yaw, 0.5); smoothRemotePlayer(player, 0.5); assert.deepEqual(player.position, [5, 0, 0]); near(player.yaw, 1.25); }); test("turns across the angle boundary follow the short arc in both directions", () => { for (const direction of [1, -1]) { let player = updateRemotePlayer(null, { position: [0, 0, 0], yaw: radians(179 * direction), }); player = updateRemotePlayer(player, { position: [0, 0, 0], yaw: radians(-179 * direction), }); smoothRemotePlayer(player, 0.5); near(player.yaw, radians(180 * direction)); smoothRemotePlayer(player, 0.5); near(player.yaw, radians(180.5 * direction)); } }); test("unbounded yaw values and equivalent full turns do not cause long spins", () => { let player = updateRemotePlayer(null, { position: [0, 0, 0], yaw: 30 * Math.PI + 0.2, }); player = updateRemotePlayer(player, { position: [0, 0, 0], yaw: -18 * Math.PI + 0.6, }); smoothRemotePlayer(player, 0.5); near(player.yaw, 30 * Math.PI + 0.4); player = updateRemotePlayer(player, { position: [0, 0, 0], yaw: player.yaw + 8 * Math.PI, }); const before = player.yaw; smoothRemotePlayer(player, 0.5); near(player.yaw, before); }); test("initial and instant updates initialize both the rendered pose and targets", () => { const first = updateRemotePlayer(undefined, { position: [3, 4, 5] }); assert.deepEqual(first.position, [3, 4, 5]); assert.deepEqual(first.target, [3, 4, 5]); assert.equal(first.yaw, 0); assert.equal(first.targetYaw, 0); const instant = updateRemotePlayer( first, { position: [20, 30, 40], yaw: -2.5 }, true, ); assert.deepEqual(instant.position, [20, 30, 40]); assert.deepEqual(instant.target, [20, 30, 40]); assert.equal(instant.yaw, -2.5); assert.equal(instant.targetYaw, -2.5); smoothRemotePlayer(instant, 0.75); assert.deepEqual(instant.position, [20, 30, 40]); assert.equal(instant.yaw, -2.5); }); test("exponential smoothing has the same result at 30, 60, and 144 fps", () => { const elapsed = 0.5; const remaining = Math.exp(-elapsed * 19); for (const fps of [30, 60, 144]) { let player = updateRemotePlayer(null, { position: [1, 2, 3], yaw: radians(179), }); player = updateRemotePlayer(player, { position: [11, 22, 33], yaw: radians(-179), }); const blend = 1 - Math.exp(-19 / fps); for (let frame = 0; frame < elapsed * fps; frame++) { smoothRemotePlayer(player, blend); } near(player.yaw, radians(181) - radians(2) * remaining); for (let axis = 0; axis < 3; axis++) { const start = axis + 1; const target = (axis + 1) * 11; near(player.position[axis], target - (target - start) * remaining); } } });