Skip to content

Commit

Permalink
Add tests for camera
Browse files Browse the repository at this point in the history
  • Loading branch information
Half-Shot committed Dec 11, 2024
1 parent 5a9f93c commit 57e9395
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 26 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"lint:eslint": "eslint src/",
"test": "jest",
"assets": "node scripts/generateAssetManifest.mjs > src/assets/manifest.ts",
"lint:prettier": "prettier 'src/**/*.(ts|tsx|md)' '*.md' -w",
"lint:prettier": "prettier 'src/**/*.(ts|tsx|md)' '*.md' -c",
"lint:prettier-fix": "prettier 'src/**/*.(ts|tsx|md)' '*.md' -w",
"lint": "yarn lint:eslint && yarn lint:prettier"
},
"dependencies": {
Expand Down
26 changes: 26 additions & 0 deletions spec/test-utils/physent-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { jest } from "@jest/globals";
import { PhysicsEntity } from "../../src/entities/phys/physicsEntity";
import { GameWorld, RapierPhysicsObject } from "../../src/world";
import { Collider, Cuboid } from "@dimforge/rapier2d-compat";
import { Point, Sprite } from "pixi.js";

export class MockPhysicsEntity extends PhysicsEntity {

public mockSprite: Sprite;

constructor(world: GameWorld, position?: Point) {
const mockSprite = {
position: position ?? new Point(Math.ceil(Math.random() * 100), Math.ceil(Math.random() * 100))
} as Sprite;
super(mockSprite, jest.mocked<Partial<RapierPhysicsObject>>({
collider: {
shape: new Cuboid(5,5)
} as Partial<Collider> as Collider,
}) as RapierPhysicsObject, world);
this.mockSprite = mockSprite;
}

public toString() {
return "MockPhysObject";
}
}
10 changes: 10 additions & 0 deletions spec/test-utils/viewport-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { jest } from "@jest/globals";
import { EventEmitter } from "events";

export class MockViewport extends EventEmitter {
constructor() {
super();
}

public moveCenter = jest.fn();
}
94 changes: 94 additions & 0 deletions spec/unit/camera.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, jest, test } from "@jest/globals";
import { CameraLockPriority, ViewportCamera } from "../../src/camera";
import { Viewport } from "pixi-viewport";
import { GameWorld } from "../../src/world";
import { Ticker } from "pixi.js";
import { MockViewport } from "../test-utils/viewport-mock";
import { MockPhysicsEntity } from "../test-utils/physent-mock";

function createTestEnv() {
const viewport = new MockViewport();
const gameWorld = jest.mocked<Partial<GameWorld>>({
entities: new Map(),
});
const camera = new ViewportCamera(viewport as unknown as Viewport, gameWorld as GameWorld);
return { viewport, camera, gameWorld };
}

describe('ViewportCamera', () => {
test('camera starts with nolock', () => {
const { camera } = createTestEnv();
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBeNull();
});
test('camera ignores targets with nolock', () => {
const { camera, gameWorld } = createTestEnv();
const ent = new MockPhysicsEntity(gameWorld as GameWorld);
ent.cameraLockPriority = CameraLockPriority.NoLock;
gameWorld.entities?.set('foobar', ent);
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBeNull();
});
test('camera locks onto a single target', () => {
const { viewport, camera, gameWorld } = createTestEnv();
const ent = new MockPhysicsEntity(gameWorld as GameWorld);
ent.cameraLockPriority = CameraLockPriority.SuggestedLockLocal;
gameWorld.entities?.set('foobar', ent);
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBe(ent);
expect(viewport.moveCenter).toHaveBeenCalledWith(ent.sprite.position.x, ent.sprite.position.y);
});
test('camera does not move if non-local lock', () => {
const { viewport, camera, gameWorld } = createTestEnv();
const ent = new MockPhysicsEntity(gameWorld as GameWorld);
ent.cameraLockPriority = CameraLockPriority.SuggestedLockNonLocal;
gameWorld.entities?.set('foobar', ent);
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBe(ent);
expect(viewport.moveCenter).not.toHaveBeenCalled();
});
test('camera ignores lock if user wants to move', () => {
const { viewport, camera, gameWorld } = createTestEnv();
const ent = new MockPhysicsEntity(gameWorld as GameWorld);
ent.cameraLockPriority = CameraLockPriority.SuggestedLockLocal;
gameWorld.entities?.set('foobar', ent);
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBe(ent);
expect(viewport.moveCenter).toHaveBeenCalledWith(ent.sprite.position.x, ent.sprite.position.y);
viewport.emit('moved', { type: "test" });
camera.update(new Ticker(), undefined);
// Ensure not recalled.
expect(viewport.moveCenter).toHaveBeenCalledTimes(1);
});
test('camera moves to a higher priority target', () => {
const { viewport, camera, gameWorld } = createTestEnv();
const entLower = new MockPhysicsEntity(gameWorld as GameWorld);
const entHigher = new MockPhysicsEntity(gameWorld as GameWorld);
entLower.cameraLockPriority = CameraLockPriority.SuggestedLockLocal;
gameWorld.entities?.set('1', entLower);
gameWorld.entities?.set('2', entHigher);
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBe(entLower);
expect(viewport.moveCenter).toHaveBeenCalledWith(entLower.sprite.position.x, entLower.sprite.position.y);
entHigher.cameraLockPriority = CameraLockPriority.LockIfNotLocalPlayer;
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBe(entHigher);
expect(viewport.moveCenter).toHaveBeenCalledWith(entHigher.sprite.position.x, entHigher.sprite.position.y);
});
test('camera moves to a lower priority target when the higher cancels', () => {
const { viewport, camera, gameWorld } = createTestEnv();
const entLower = new MockPhysicsEntity(gameWorld as GameWorld);
const entHigher = new MockPhysicsEntity(gameWorld as GameWorld);
entLower.cameraLockPriority = CameraLockPriority.SuggestedLockLocal
entHigher.cameraLockPriority = CameraLockPriority.LockIfNotLocalPlayer;
gameWorld.entities?.set('1', entLower);
gameWorld.entities?.set('2', entHigher);
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBe(entHigher);
expect(viewport.moveCenter).toHaveBeenCalledWith(entHigher.sprite.position.x, entHigher.sprite.position.y);
entHigher.cameraLockPriority = CameraLockPriority.NoLock;
camera.update(new Ticker(), undefined);
expect(camera.lockTarget).toBe(entLower);
expect(viewport.moveCenter).toHaveBeenCalledWith(entLower.sprite.position.x, entLower.sprite.position.y);
});
});
9 changes: 8 additions & 1 deletion src/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export class ViewportCamera {
private userWantsControl = false;
private lastMoveHash = 0;

public get lockTarget() {
return this.currentLockTarget;
}

constructor(
private readonly viewport: Viewport,
private readonly world: GameWorld,
Expand All @@ -36,6 +40,8 @@ export class ViewportCamera {
return;
}
this.userWantsControl = true;
// Reset move hash, since the camera is under control.
this.lastMoveHash = 0;
logger.debug("Player took control");
});
}
Expand Down Expand Up @@ -63,7 +69,7 @@ export class ViewportCamera {
if (newTarget !== this.currentLockTarget) {
// Reset user control.
this.userWantsControl = false;
logger.debug("New lock target", newTarget);
logger.debug("New lock target", newTarget.toString());
}
this.currentLockTarget = newTarget;

Expand All @@ -76,6 +82,7 @@ export class ViewportCamera {
this.currentLockTarget.sprite.position.x +
this.currentLockTarget.sprite.position.y;
if (this.lastMoveHash === newMoveHash) {
logger.debug("Hash match, not moving");
return;
}
this.lastMoveHash = newMoveHash;
Expand Down
5 changes: 4 additions & 1 deletion src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { Mine } from "./phys/mine";
import { PhysicsEntity } from "./phys/physicsEntity";
import { TestDummy } from "./playable/testDummy";
import { Worm } from "./playable/worm";
import { Water } from "./water";

/**
* Should be called during game startup to load all assets to
* entitires that need them.
* @param assets
*/
export function readAssetsForEntities(assets: AssetPack): void {
export async function readAssetsForEntities(assets: AssetPack): Promise<void> {
const p = Water.readAssets();
BazookaShell.readAssets(assets);
Grenade.readAssets(assets);
Mine.readAssets(assets);
Expand All @@ -22,4 +24,5 @@ export function readAssetsForEntities(assets: AssetPack): void {
Worm.readAssets(assets);
Explosion.readAssets(assets);
PhysicsEntity.readAssets(assets);
await p;
}
16 changes: 13 additions & 3 deletions src/entities/water.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {
UPDATE_PRIORITY,
} from "pixi.js";
import { IPhysicalEntity } from "./entity";
import vertex from "../shaders/water.vert?raw";
import fragment from "../shaders/water.frag?raw";
import {
collisionGroupBitmask,
CollisionGroups,
Expand Down Expand Up @@ -37,6 +35,14 @@ export class Water implements IPhysicalEntity {
return false;
}

private static vertexSrc: string;
private static fragmentSrc: string;

static async readAssets() {
Water.vertexSrc = (await import("../shaders/water.vert?raw")).default;
Water.fragmentSrc = (await import("../shaders/water.frag?raw")).default;
}

private readonly physObject: RapierPhysicsObject;
private readonly shader: Shader;

Expand Down Expand Up @@ -73,7 +79,11 @@ export class Water implements IPhysicalEntity {
indexBuffer,
});
this.shader = Filter.from({
gl: { vertex, fragment, name: "water" },
gl: {
vertex: Water.vertexSrc,
fragment: Water.fragmentSrc,
name: "water",
},
resources: {
waveUniforms: {
iTime: { type: "f32", value: 0 },
Expand Down
3 changes: 2 additions & 1 deletion src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class Flags extends EventEmitter {

constructor() {
super();
const qs = new URLSearchParams(window.location.search);
// Don't assume that window exists (e.g. searching)
const qs = new URLSearchParams(globalThis?.location?.search ?? "");
this.DebugView = qs.get("debug")
? DebugLevel.PhysicsOverlay
: DebugLevel.None;
Expand Down
4 changes: 2 additions & 2 deletions src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class Game {
this.viewport.zoom(8);

// TODO: Bit of a hack?
staticController.bindMouseInput();
staticController.bindInput();
}

public goToMenu(winningTeams?: Team[]) {
Expand All @@ -85,7 +85,7 @@ export class Game {

public async loadResources() {
const assetPack = getAssets();
readAssetsForEntities(assetPack);
await readAssetsForEntities(assetPack);
readAssetsForWeapons(assetPack);
WindDial.loadAssets(assetPack.textures);
}
Expand Down
7 changes: 3 additions & 4 deletions src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,11 @@ class Controller extends EventEmitter {
}
this.sequences.push({ sequence: parts, inputKind });
}
// TODO: Only bind when the game has started.
window.addEventListener("keydown", this.onKeyDown.bind(this));
window.addEventListener("keyup", this.onKeyUp.bind(this));
}

public bindMouseInput() {
public bindInput() {
window.addEventListener("keydown", this.onKeyDown.bind(this));
window.addEventListener("keyup", this.onKeyUp.bind(this));
const overlayElement = document.querySelector<HTMLDivElement>("#overlay");
if (!overlayElement) {
throw Error("Missing overlay element");
Expand Down
13 changes: 0 additions & 13 deletions src/mixins/bodyWireframe..ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,6 @@ import { PIXELS_PER_METER, RapierPhysicsObject } from "../world";
import { Cuboid } from "@dimforge/rapier2d-compat";
import { DefaultTextStyle } from "./styles";

/**
* Render a wireframe in pixi.js around a matter body.
*/

const globalWindow = window as unknown as {
debugPivotModX: number;
debugPivotModY: number;
debugRotation: number;
};

globalWindow.debugPivotModX = 0;
globalWindow.debugPivotModY = 0;
globalWindow.debugRotation = 0;
export class BodyWireframe {
private gfx = new Graphics();
private debugText = new Text({
Expand Down

0 comments on commit 57e9395

Please sign in to comment.