- Simplify the logic wherever possible to remove overengineering and simplify the netcode - Actually improve the netcode - The new lines are mainly the package locks and tests Reviewed-on: https://home.schmelczer.dev/git/andras/decla-red/pulls/4
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { marchCircle } from '../shared/src/physics/march-circle';
|
|
import { resolveCircleMovement } from '../shared/src/physics/resolve-circle-movement';
|
|
import { finiteVec2, sanitizeName } from '../shared/src/helper/finite';
|
|
import { CommandReceiver } from '../shared/src/commands/command-receiver';
|
|
import type { PhysicsBody, Sdf } from '../shared/src/physics/sdf';
|
|
|
|
const bodyAt = (x: number, y: number, vx: number, vy: number): PhysicsBody => ({
|
|
center: [x, y],
|
|
radius: 1,
|
|
velocity: [vx, vy],
|
|
lastNormal: [0, 1],
|
|
restitution: 1,
|
|
});
|
|
|
|
const rightWall: Sdf = { canCollide: true, distance: ([x]) => 10 - x };
|
|
|
|
describe('circle collision movement', () => {
|
|
it('spends only the remaining step time after a bounce', () => {
|
|
const body = bodyAt(0, 0, 100, 0);
|
|
expect(resolveCircleMovement(body, 0.2, [rightWall])).toBe(rightWall);
|
|
// 9 units to the wall, then 11 units back: 20 units of total travel.
|
|
expect(body.center[0]).toBeCloseTo(-2, 6);
|
|
expect(body.velocity[0]).toBeCloseTo(-100, 6);
|
|
});
|
|
|
|
it('resolves velocity at the second contact instead of leaving it aimed into a wall', () => {
|
|
const body = bodyAt(0, 0, 100, 0);
|
|
const leftWall: Sdf = { canCollide: true, distance: ([x]) => x + 5 };
|
|
resolveCircleMovement(body, 0.3, [rightWall, leftWall]);
|
|
expect(body.center[0]).toBeCloseTo(-4, 6);
|
|
expect(body.velocity[0]).toBeCloseTo(100, 6);
|
|
});
|
|
|
|
it('allows slow tangential sliding along a surface', () => {
|
|
const body = { ...bodyAt(9, 0, 100, 10), restitution: 0 };
|
|
resolveCircleMovement(body, 0.1, [rightWall]);
|
|
expect(body.center[0]).toBeCloseTo(9, 6);
|
|
expect(body.center[1]).toBeCloseTo(1, 6);
|
|
expect(body.velocity[0]).toBeCloseTo(0, 6);
|
|
});
|
|
|
|
it('ignores disabled objects when identifying the contacted surface', () => {
|
|
const inactive: Sdf = { canCollide: false, distance: () => -100 };
|
|
const onHit = vi.fn();
|
|
const body = bodyAt(0, 0, 100, 0);
|
|
const result = marchCircle(body, [20, 0], [inactive, rightWall], onHit);
|
|
expect(result.hitObject).toBe(rightWall);
|
|
expect(onHit).toHaveBeenCalledExactlyOnceWith(rightWall);
|
|
expect(result.normal).toEqual([-1, 0]);
|
|
});
|
|
|
|
it('samples the collision normal before the callback removes the obstacle', () => {
|
|
const wall = { ...rightWall };
|
|
const body = bodyAt(0, 0, 100, 0);
|
|
resolveCircleMovement(body, 0.2, [wall], () => {
|
|
wall.canCollide = false;
|
|
});
|
|
expect(body.center.every(Number.isFinite)).toBe(true);
|
|
expect(body.velocity[0]).toBeCloseTo(-100, 6);
|
|
});
|
|
});
|
|
|
|
describe('network input helpers', () => {
|
|
it('clamps huge finite vectors before allocating a float vector', () => {
|
|
const direction = finiteVec2([1e300, 1e300], 1)!;
|
|
expect(direction.every(Number.isFinite)).toBe(true);
|
|
expect(Math.hypot(...direction)).toBeCloseTo(1, 6);
|
|
expect(direction[0]).toBeGreaterThan(0.7);
|
|
});
|
|
|
|
it('never calls methods on an untrusted player name', () => {
|
|
expect(sanitizeName({ toString: {} }, 10)).toBe('');
|
|
expect(sanitizeName('abcdefghijk', 10)).toBe('abcdefghij');
|
|
});
|
|
|
|
it('does not dispatch inherited object methods as commands', () => {
|
|
class Receiver extends CommandReceiver {
|
|
public fallback = vi.fn();
|
|
protected defaultCommandExecutor(command: object) {
|
|
this.fallback(command);
|
|
}
|
|
}
|
|
const receiver = new Receiver();
|
|
const command = { constructor: { name: 'constructor' } };
|
|
receiver.handleCommand(command);
|
|
expect(receiver.fallback).toHaveBeenCalledExactlyOnceWith(command);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('random world seeds', () => {
|
|
it('uses the random seed bits instead of truncating every initial seed to zero', async () => {
|
|
const random = vi.spyOn(Math, 'random');
|
|
const firstValues: number[] = [];
|
|
for (const seed of [0.1, 0.5, 0.9]) {
|
|
random.mockReturnValue(seed);
|
|
vi.resetModules();
|
|
const { Random } = await import('../shared/src/helper/random');
|
|
firstValues.push(Random.getRandom());
|
|
Random.seed = 42;
|
|
const expected = [Random.getRandom(), Random.getRandom()];
|
|
Random.seed = 42;
|
|
expect([Random.getRandom(), Random.getRandom()]).toEqual(expected);
|
|
}
|
|
expect(new Set(firstValues).size).toBe(3);
|
|
});
|
|
});
|