import { Game } from '@engine/Game';
import { Entity } from '@engine/entities';
import { Grid } from '@engine/structures';
import {
WallBounds,
FacingDirection,
Render,
Physics,
Input,
Collision,
NetworkUpdate,
SystemNames
} from '@engine/systems';
import {
type MessageQueueProvider,
type MessagePublisher,
type MessageProcessor,
type Message,
type EntityAddBody,
MessageType,
type EntityUpdateBody
} from '@engine/network';
import { stringify, parse } from '@engine/utils';
import { ComponentNames, Control, NetworkUpdateable } from '@engine/components';
class ClientMessageProcessor implements MessageProcessor {
private game: Game;
constructor(game: Game) {
this.game = game;
}
public process(message: Message) {
switch (message.type) {
case MessageType.NEW_ENTITIES:
const entityAdditions = message.body as unknown as EntityAddBody[];
entityAdditions.forEach((addBody) => {
const entity = Entity.from(
addBody.entityName,
addBody.id,
addBody.args
);
if (entity.hasComponent(ComponentNames.Control)) {
const clientId = this.game.getSystem(
SystemNames.Input
).clientId;
const control = entity.getComponent(
ComponentNames.Control
);
if (control.controllableBy === clientId) {
entity.addComponent(new NetworkUpdateable());
}
}
this.game.addEntity(entity);
});
break;
case MessageType.REMOVE_ENTITIES:
const ids = message.body as unknown as string[];
ids.forEach((id) => this.game.removeEntity(id));
break;
case MessageType.UPDATE_ENTITIES:
const entityUpdates = message.body as unknown as EntityUpdateBody[];
entityUpdates.forEach(({ id, args }) => {
const entity = this.game.getEntity(id);
if (!entity) return;
if (entity && entity.hasComponent(ComponentNames.Control)) {
const clientId = this.game.getSystem(
SystemNames.Input
).clientId;
const control = entity.getComponent(
ComponentNames.Control
);
// don't listen to entities which we control
if (control.controllableBy == clientId) return;
}
entity.setFrom(args);
});
break;
default:
break;
}
}
}
class ClientSocketMessageQueueProvider implements MessageQueueProvider {
private socket: WebSocket;
private messages: Message[];
constructor(socket: WebSocket) {
this.socket = socket;
this.messages = [];
this.socket.addEventListener('message', (e) => {
const messages = parse(e.data);
this.messages = this.messages.concat(messages);
});
}
public getNewMessages() {
return this.messages;
}
public clearMessages() {
this.messages = [];
}
}
class ClientSocketMessagePublisher implements MessagePublisher {
private socket: WebSocket;
private messages: Message[];
constructor(socket: WebSocket) {
this.socket = socket;
this.messages = [];
}
public addMessage(message: Message) {
this.messages.push(message);
}
public publish() {
if (this.socket.readyState == WebSocket.OPEN) {
this.messages.forEach((message: Message) =>
this.socket.send(stringify(message))
);
this.messages = [];
}
}
}
export class JumpStorm {
private game: Game;
private clientId: string;
constructor(game: Game) {
this.game = game;
}
public async init(
ctx: CanvasRenderingContext2D,
httpMethod: string,
wsMethod: string,
host: string
) {
this.clientId = await this.getAssignedCookie(
`${httpMethod}://${host}/assign`
);
const socket = new WebSocket(`${wsMethod}://${host}/game`);
const clientSocketMessageQueueProvider =
new ClientSocketMessageQueueProvider(socket);
const clientSocketMessagePublisher = new ClientSocketMessagePublisher(
socket
);
const clientMessageProcessor = new ClientMessageProcessor(this.game);
const inputSystem = new Input(this.clientId, clientSocketMessagePublisher);
this.addWindowEventListenersToInputSystem(inputSystem);
const grid = new Grid();
[
new Physics(),
new NetworkUpdate(
clientSocketMessageQueueProvider,
clientSocketMessagePublisher,
clientMessageProcessor
),
inputSystem,
new FacingDirection(),
new Collision(grid),
new WallBounds(),
new Render(ctx)
].forEach((system) => this.game.addSystem(system));
}
public play() {
this.game.start();
const loop = (timestamp: number) => {
this.game.doGameLoop(timestamp);
requestAnimationFrame(loop); // tail call recursion! /s
};
requestAnimationFrame(loop);
}
private addWindowEventListenersToInputSystem(input: Input) {
window.addEventListener('keydown', (e) => {
if (!e.repeat) {
input.keyPressed(e.key.toLowerCase());
}
});
window.addEventListener('keyup', (e) =>
input.keyReleased(e.key.toLowerCase())
);
}
private async getAssignedCookie(endpoint: string): Promise {
return fetch(endpoint)
.then((resp) => {
if (resp.ok) {
return resp.text();
}
throw resp;
})
.then((cookie) => cookie);
}
}