185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
import { describe, expect, test } from "@suaveplan/testing/runner";
|
|
import {
|
|
calculateVelocity,
|
|
dequantizeVector,
|
|
GameInterpolator,
|
|
GameStateManager,
|
|
PositionPredictor,
|
|
PredictionAlgorithm,
|
|
quantizeVector,
|
|
RotationPredictor,
|
|
StateReconciliation,
|
|
} from "./index.js";
|
|
|
|
describe("GameStateManager", () => {
|
|
test("pushes and retrieves states", () => {
|
|
const manager = new GameStateManager();
|
|
manager.pushState({ version: 1, entities: {} });
|
|
expect(manager.getStateCount()).toBe(1);
|
|
expect(manager.getLatestState()?.version).toBe(1);
|
|
expect(manager.getStateAt(0)?.version).toBe(1);
|
|
});
|
|
|
|
test("getStateAt returns undefined for out of bounds", () => {
|
|
const manager = new GameStateManager();
|
|
expect(manager.getStateAt(0)).toBeUndefined();
|
|
});
|
|
|
|
test("getLatestState returns undefined when empty", () => {
|
|
const manager = new GameStateManager();
|
|
expect(manager.getLatestState()).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("quantize/dequantize", () => {
|
|
test("roundtrips a vector", () => {
|
|
const v = { x: 0.5, y: -0.25, z: 0.75 };
|
|
const q = quantizeVector(v, 16);
|
|
const d = dequantizeVector(q, 16);
|
|
expect(d.x).toBeCloseTo(v.x, 1);
|
|
expect(d.y).toBeCloseTo(v.y, 1);
|
|
expect(d.z).toBeCloseTo(v.z, 1);
|
|
});
|
|
|
|
test("handles vector without z component", () => {
|
|
const v = { x: 0.5, y: -0.25 };
|
|
const q = quantizeVector(v, 8);
|
|
expect(q.z).toBeUndefined();
|
|
const d = dequantizeVector(q, 8);
|
|
expect(d.x).toBeCloseTo(v.x, 1);
|
|
expect(d.y).toBeCloseTo(v.y, 1);
|
|
expect(d.z).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("PositionPredictor", () => {
|
|
test("needs at least 2 points to predict", () => {
|
|
const predictor = new PositionPredictor();
|
|
predictor.addPosition({ x: 0, y: 0, z: 0 }, 0);
|
|
expect(predictor.predictPosition(100)).toBeNull();
|
|
});
|
|
test("linear prediction works", () => {
|
|
const predictor = new PositionPredictor({
|
|
algorithm: PredictionAlgorithm.LINEAR,
|
|
});
|
|
predictor.addPosition({ x: 0, y: 0, z: 0 }, 0);
|
|
predictor.addPosition({ x: 10, y: 0, z: 0 }, 1000);
|
|
const pred = predictor.predictPosition(100);
|
|
expect(pred).not.toBeNull();
|
|
expect(pred!.x).toBeGreaterThan(10);
|
|
});
|
|
test("clear history resets predictor", () => {
|
|
const predictor = new PositionPredictor();
|
|
predictor.addPosition({ x: 0, y: 0, z: 0 }, 0);
|
|
predictor.addPosition({ x: 10, y: 0, z: 0 }, 1000);
|
|
predictor.clearHistory();
|
|
expect(predictor.predictPosition(100)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("RotationPredictor", () => {
|
|
test("predicts rotation", () => {
|
|
const predictor = new RotationPredictor();
|
|
predictor.addRotation({ yaw: 0, pitch: 0 }, 0);
|
|
predictor.addRotation({ yaw: 10, pitch: 5 }, 1000);
|
|
const pred = predictor.predictRotation(100);
|
|
expect(pred).not.toBeNull();
|
|
expect(pred!.yaw).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("GameInterpolator", () => {
|
|
test("linear position interpolation", () => {
|
|
const interp = new GameInterpolator();
|
|
const result = interp.interpolatePosition(
|
|
{ x: 0, y: 0, z: 0 },
|
|
{ x: 10, y: 20, z: 30 },
|
|
0.5
|
|
);
|
|
expect(result.x).toBe(5);
|
|
expect(result.y).toBe(10);
|
|
expect(result.z).toBe(15);
|
|
});
|
|
test("rotation interpolation handles angle wrapping", () => {
|
|
const interp = new GameInterpolator();
|
|
const result = interp.interpolateRotation(
|
|
{ yaw: 170, pitch: 0 },
|
|
{ yaw: -170, pitch: 0 },
|
|
0.5
|
|
);
|
|
expect(Math.abs(result.yaw)).toBeLessThanOrEqual(180);
|
|
});
|
|
test("player state interpolation", () => {
|
|
const interp = new GameInterpolator();
|
|
const start = {
|
|
position: { x: 0, y: 0, z: 0 },
|
|
rotation: { yaw: 0, pitch: 0 },
|
|
stats: {
|
|
health: 100,
|
|
shield: 50,
|
|
speed: 5,
|
|
isMoving: false,
|
|
isSprinting: false,
|
|
},
|
|
};
|
|
const end = {
|
|
position: { x: 10, y: 0, z: 0 },
|
|
rotation: { yaw: 90, pitch: 0 },
|
|
stats: {
|
|
health: 80,
|
|
shield: 40,
|
|
speed: 10,
|
|
isMoving: true,
|
|
isSprinting: true,
|
|
},
|
|
};
|
|
const result = interp.interpolatePlayerState(start, end, 0.5);
|
|
expect(result.position.x).toBe(5);
|
|
expect(result.stats.health).toBe(90);
|
|
});
|
|
});
|
|
|
|
describe("StateReconciliation", () => {
|
|
test("processes inputs", () => {
|
|
const sr = new StateReconciliation();
|
|
const seq = sr.processInput(
|
|
{ moveX: 1, moveY: 0, rotateYaw: 0, rotatePitch: 0, actions: [] },
|
|
0
|
|
);
|
|
expect(seq).toBe(0);
|
|
expect(sr.getClientSequence()).toBe(0);
|
|
});
|
|
test("ping/pong tracking", () => {
|
|
const sr = new StateReconciliation();
|
|
sr.sendPing();
|
|
sr.receivePong();
|
|
expect(sr.getAveragePing()).toBeGreaterThanOrEqual(0);
|
|
});
|
|
test("reset clears state", () => {
|
|
const sr = new StateReconciliation();
|
|
sr.processInput(
|
|
{ moveX: 1, moveY: 0, rotateYaw: 0, rotatePitch: 0, actions: [] },
|
|
0
|
|
);
|
|
sr.reset();
|
|
expect(sr.getClientSequence()).toBe(-1);
|
|
});
|
|
});
|
|
|
|
describe("calculateVelocity", () => {
|
|
test("calculates velocity from history", () => {
|
|
const history = [
|
|
{ value: { x: 0, y: 0, z: 0 }, timestamp: 0 },
|
|
{ value: { x: 10, y: 0, z: 0 }, timestamp: 1000 },
|
|
];
|
|
const vel = calculateVelocity(history);
|
|
expect(vel.x).toBe(10);
|
|
expect(vel.y).toBe(0);
|
|
});
|
|
test("returns zero for single point", () => {
|
|
const history = [{ value: { x: 5, y: 0, z: 0 }, timestamp: 0 }];
|
|
const vel = calculateVelocity(history);
|
|
expect(vel.x).toBe(0);
|
|
});
|
|
});
|