even more
This commit is contained in:
parent
82e092ba55
commit
8f50ebdb97
1 changed files with 1842 additions and 3095 deletions
|
|
@ -1,185 +1,150 @@
|
|||
// Regression net for backend defects — drives the real classes.
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { PhysicalContainer } from '../backend/src/physics/containers/physical-container';
|
||||
import { BoundingBox } from '../backend/src/physics/bounding-boxes/bounding-box';
|
||||
import { PhysicalContainer } from '../backend/src/physics/physical-container';
|
||||
import { BoundingBox } from '../backend/src/physics/bounding-box';
|
||||
import { ProjectilePhysical } from '../backend/src/objects/projectile-physical';
|
||||
import { CharacterPhysical } from '../backend/src/objects/character-physical';
|
||||
import { StepCommand, GeneratePointsCommand } from '../backend/src/commands/commands';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const shared = require('../shared/lib/main.js');
|
||||
const { vec2 } = require('../shared/node_modules/gl-matrix');
|
||||
const { applyArrayPlugins, Random, settings, CharacterTeam } = shared;
|
||||
|
||||
applyArrayPlugins();
|
||||
const { Random, settings, CharacterTeam } = shared;
|
||||
|
||||
const step = settings.targetPhysicsDeltaTimeInSeconds;
|
||||
|
||||
class PointsSpy {
|
||||
public blue = 0;
|
||||
public red = 0;
|
||||
public handleCommand(command: unknown) {
|
||||
if (command instanceof GeneratePointsCommand) {
|
||||
this.blue += command.blue;
|
||||
this.red += command.red;
|
||||
}
|
||||
public addPoints(blue: number, red: number) {
|
||||
this.blue += blue;
|
||||
this.red += red;
|
||||
}
|
||||
public announce() {}
|
||||
}
|
||||
|
||||
const boxAround = (center: number[], radius: number) => {
|
||||
const box = new BoundingBox();
|
||||
box.xMin = center[0] - radius;
|
||||
box.xMax = center[0] + radius;
|
||||
box.yMin = center[1] - radius;
|
||||
box.yMax = center[1] + radius;
|
||||
return box;
|
||||
const makeWorld = () => {
|
||||
const game = new PointsSpy();
|
||||
return { game, container: new PhysicalContainer(game) };
|
||||
};
|
||||
|
||||
const spawnProjectile = (container: PhysicalContainer) =>
|
||||
new ProjectilePhysical(
|
||||
vec2.fromValues(0, 0),
|
||||
20,
|
||||
40,
|
||||
CharacterTeam.blue,
|
||||
vec2.fromValues(3000, 0),
|
||||
{ team: CharacterTeam.blue } as never,
|
||||
container,
|
||||
0,
|
||||
);
|
||||
|
||||
const spawnCharacter = (container: PhysicalContainer) => {
|
||||
const character = new CharacterPhysical(
|
||||
'tester',
|
||||
0,
|
||||
0,
|
||||
CharacterTeam.blue,
|
||||
container,
|
||||
vec2.fromValues(0, 0),
|
||||
);
|
||||
container.addObject(character);
|
||||
return character;
|
||||
};
|
||||
|
||||
const projectilesIn = (container: PhysicalContainer) =>
|
||||
container
|
||||
.findIntersecting(BoundingBox.ofCircle([0, 0], 5000))
|
||||
.filter((o) => o.gameObject instanceof ProjectilePhysical)
|
||||
.map((o) => o.gameObject as ProjectilePhysical);
|
||||
|
||||
beforeEach(() => {
|
||||
Random.seed = 1;
|
||||
});
|
||||
|
||||
describe('projectile broadphase registration', () => {
|
||||
it('follows the projectile instead of staying at the muzzle', () => {
|
||||
const container = new PhysicalContainer();
|
||||
container.initialize();
|
||||
|
||||
const projectile = new ProjectilePhysical(
|
||||
vec2.fromValues(0, 0),
|
||||
20,
|
||||
40,
|
||||
CharacterTeam.blue,
|
||||
vec2.fromValues(3000, 0),
|
||||
{ team: CharacterTeam.blue } as never,
|
||||
container,
|
||||
0,
|
||||
);
|
||||
const { container } = makeWorld();
|
||||
const projectile = spawnProjectile(container);
|
||||
container.addObject(projectile);
|
||||
|
||||
for (let i = 0; i < 40; i++) {
|
||||
container.handleCommand(new StepCommand(step, new PointsSpy() as never));
|
||||
container.step(step);
|
||||
}
|
||||
|
||||
expect(projectile.center[0]).toBeGreaterThan(400);
|
||||
expect(container.findIntersecting(boxAround(projectile.center, 100))).toContain(
|
||||
expect(
|
||||
container.findIntersecting(BoundingBox.ofCircle(projectile.center, 100)),
|
||||
).toContain(projectile);
|
||||
expect(container.findIntersecting(BoundingBox.ofCircle([0, 0], 100))).not.toContain(
|
||||
projectile,
|
||||
);
|
||||
expect(container.findIntersecting(boxAround([0, 0], 100))).not.toContain(projectile);
|
||||
});
|
||||
|
||||
it('spawns a shot just clear of the shooter, not a jump downrange', () => {
|
||||
const container = new PhysicalContainer();
|
||||
container.initialize();
|
||||
const projectile = new ProjectilePhysical(
|
||||
vec2.fromValues(0, 0),
|
||||
20,
|
||||
40,
|
||||
CharacterTeam.blue,
|
||||
vec2.fromValues(3000, 0),
|
||||
{ team: CharacterTeam.blue } as never,
|
||||
container,
|
||||
0,
|
||||
);
|
||||
const { container } = makeWorld();
|
||||
const projectile = spawnProjectile(container);
|
||||
|
||||
expect(projectile.center[0]).toBeLessThanOrEqual(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scoring', () => {
|
||||
const spawnCharacter = (container: PhysicalContainer) => {
|
||||
const character = new CharacterPhysical(
|
||||
'tester',
|
||||
0,
|
||||
0,
|
||||
CharacterTeam.blue,
|
||||
container,
|
||||
vec2.fromValues(0, 0),
|
||||
);
|
||||
container.addObject(character);
|
||||
return character;
|
||||
};
|
||||
|
||||
it('awards nothing for an administrative death', () => {
|
||||
const container = new PhysicalContainer();
|
||||
container.initialize();
|
||||
const { game, container } = makeWorld();
|
||||
const character = spawnCharacter(container);
|
||||
const game = new PointsSpy();
|
||||
|
||||
character.onDie(); // what PlayerBase.destroy() does on a socket drop
|
||||
container.handleCommand(new StepCommand(step, game as never));
|
||||
character.onDie();
|
||||
container.step(step);
|
||||
|
||||
expect(game.blue).toBe(0);
|
||||
expect(game.red).toBe(0);
|
||||
});
|
||||
|
||||
it('still awards a kill for a combat death', () => {
|
||||
const container = new PhysicalContainer();
|
||||
container.initialize();
|
||||
const { game, container } = makeWorld();
|
||||
const character = spawnCharacter(container);
|
||||
const game = new PointsSpy();
|
||||
|
||||
character.onDie(true);
|
||||
container.handleCommand(new StepCommand(step, game as never));
|
||||
container.step(step);
|
||||
|
||||
expect(game.red).toBe(settings.playerKillPoint);
|
||||
expect(game.blue).toBe(0);
|
||||
});
|
||||
|
||||
it('awards a combat death only once', () => {
|
||||
const { game, container } = makeWorld();
|
||||
const character = spawnCharacter(container);
|
||||
|
||||
character.onDie(true);
|
||||
character.onDie(true);
|
||||
|
||||
expect(game.red).toBe(settings.playerKillPoint);
|
||||
});
|
||||
});
|
||||
|
||||
describe('charged shots', () => {
|
||||
it('scales the shot by the strength actually paid, not the requested charge', () => {
|
||||
const container = new PhysicalContainer();
|
||||
container.initialize();
|
||||
const character = new CharacterPhysical(
|
||||
'tester',
|
||||
0,
|
||||
0,
|
||||
CharacterTeam.blue,
|
||||
container,
|
||||
vec2.fromValues(0, 0),
|
||||
);
|
||||
container.addObject(character);
|
||||
|
||||
const { container } = makeWorld();
|
||||
const character = spawnCharacter(container);
|
||||
(character as unknown as { projectileStrength: number }).projectileStrength =
|
||||
settings.chargeShotStrengthMin;
|
||||
|
||||
const before = container.findIntersecting(boxAround([0, 0], 5000)).length;
|
||||
character.shootTowards(vec2.fromValues(1000, 0), 1);
|
||||
const spawned = container
|
||||
.findIntersecting(boxAround([0, 0], 5000))
|
||||
.filter((o) => o.gameObject instanceof ProjectilePhysical)
|
||||
.map((o) => o.gameObject as ProjectilePhysical);
|
||||
const [spawned] = projectilesIn(container);
|
||||
|
||||
expect(container.findIntersecting(boxAround([0, 0], 5000)).length).toBeGreaterThan(
|
||||
before,
|
||||
);
|
||||
expect(spawned[0].charge).toBeCloseTo(0, 5);
|
||||
expect(spawned[0].radius).toBeCloseTo(settings.chargeShotRadiusMin, 5);
|
||||
expect(spawned).toBeDefined();
|
||||
expect(spawned.charge).toBeCloseTo(0, 5);
|
||||
expect(spawned.radius).toBeCloseTo(settings.chargeShotRadiusMin, 5);
|
||||
});
|
||||
|
||||
it('ignores a shot aimed at the shooter itself', () => {
|
||||
const container = new PhysicalContainer();
|
||||
container.initialize();
|
||||
const character = new CharacterPhysical(
|
||||
'tester',
|
||||
0,
|
||||
0,
|
||||
CharacterTeam.blue,
|
||||
container,
|
||||
vec2.fromValues(0, 0),
|
||||
);
|
||||
container.addObject(character);
|
||||
const { container } = makeWorld();
|
||||
const character = spawnCharacter(container);
|
||||
|
||||
const projectilesBefore = container
|
||||
.findIntersecting(boxAround([0, 0], 5000))
|
||||
.filter((o) => o.gameObject instanceof ProjectilePhysical).length;
|
||||
character.shootTowards(vec2.clone(character.center), 1);
|
||||
const projectilesAfter = container
|
||||
.findIntersecting(boxAround([0, 0], 5000))
|
||||
.filter((o) => o.gameObject instanceof ProjectilePhysical).length;
|
||||
|
||||
expect(projectilesAfter).toBe(projectilesBefore);
|
||||
expect(projectilesIn(container)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue