aboutsummaryrefslogtreecommitdiff
path: root/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'static/src')
-rw-r--r--static/src/component.ts8
-rw-r--r--static/src/engine/component.ts24
-rw-r--r--static/src/engine/debounce_publisher.ts (renamed from static/src/mouse_controller.ts)25
-rw-r--r--static/src/engine/entity.ts45
-rw-r--r--static/src/engine/events.ts (renamed from static/src/network.ts)17
-rw-r--r--static/src/engine/game.ts112
-rw-r--r--static/src/engine/input.ts49
-rw-r--r--static/src/engine/network.ts138
-rw-r--r--static/src/engine/render.ts46
-rw-r--r--static/src/engine/system.ts14
-rw-r--r--static/src/engine/trailing_position.ts33
-rw-r--r--static/src/entity.ts11
-rw-r--r--static/src/main.ts116
-rw-r--r--static/src/vector.ts14
14 files changed, 516 insertions, 136 deletions
diff --git a/static/src/component.ts b/static/src/component.ts
deleted file mode 100644
index d338cad..0000000
--- a/static/src/component.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export enum ComponentType {
- POSITION = "POSITION",
- RENDERABLE = "RENDERABLE",
-}
-
-export interface Component {
- name: string;
-}
diff --git a/static/src/engine/component.ts b/static/src/engine/component.ts
new file mode 100644
index 0000000..9607fec
--- /dev/null
+++ b/static/src/engine/component.ts
@@ -0,0 +1,24 @@
+export enum ComponentType {
+ POSITION = "POSITION",
+ RENDERABLE = "RENDERABLE",
+ TRAILING_POSITION = "TRAILING_POSITION",
+}
+
+export interface Component {
+ name: ComponentType;
+}
+
+export interface PositionComponent extends Component {
+ name: ComponentType.POSITION;
+ x: number;
+ y: number;
+}
+
+export interface TrailingPositionComponent extends Component {
+ name: ComponentType.TRAILING_POSITION;
+ trails: Array<{ x: number; y: number; time: number }>;
+}
+
+export interface RenderableComponent extends Component {
+ name: ComponentType.RENDERABLE;
+}
diff --git a/static/src/mouse_controller.ts b/static/src/engine/debounce_publisher.ts
index bd8d51a..8ee4bb0 100644
--- a/static/src/mouse_controller.ts
+++ b/static/src/engine/debounce_publisher.ts
@@ -1,12 +1,11 @@
-import { Vec2 } from "./vector";
-export class MouseController {
+export class DebouncePublisher<T> {
private last_event_time = Date.now();
- private last_movement: Vec2 | undefined;
+ private unpublished_data: T | undefined;
private interval_id: number | undefined;
constructor(
- private readonly publisher: (new_movement: Vec2) => void | Promise<void>,
- private readonly debounce_ms = 200,
+ private readonly publisher: (data: T) => void | Promise<void>,
+ private readonly debounce_ms = 100,
) {}
public start() {
@@ -14,7 +13,7 @@ export class MouseController {
return;
}
this.interval_id = setInterval(
- () => this.publish_movement(),
+ () => this.debounce_publish(),
this.debounce_ms,
);
}
@@ -27,21 +26,21 @@ export class MouseController {
delete this.interval_id;
}
- public move(x: number, y: number) {
- this.last_movement = new Vec2(x, y);
- this.publish_movement();
+ public update(data: T) {
+ this.unpublished_data = data;
+ this.debounce_publish();
}
- private publish_movement() {
+ private debounce_publish() {
if (
Date.now() - this.last_event_time < this.debounce_ms ||
- typeof this.last_movement === "undefined"
+ typeof this.unpublished_data === "undefined"
) {
return;
}
this.last_event_time = Date.now();
- this.publisher(this.last_movement.copy());
- delete this.last_movement;
+ this.publisher(this.unpublished_data);
+ this.unpublished_data = undefined;
}
}
diff --git a/static/src/engine/entity.ts b/static/src/engine/entity.ts
new file mode 100644
index 0000000..2c8d38e
--- /dev/null
+++ b/static/src/engine/entity.ts
@@ -0,0 +1,45 @@
+import {
+ Component,
+ ComponentType,
+ PositionComponent,
+ RenderableComponent,
+ TrailingPositionComponent,
+} from "./component";
+
+export enum EntityType {
+ LASER = "LASER",
+ CAT = "CAT",
+}
+
+export interface Entity {
+ entity_type: EntityType;
+ id: string;
+ components: Record<ComponentType, Component>;
+}
+
+export const create_laser = (base: Entity) => {
+ const trailing_position: TrailingPositionComponent = {
+ name: ComponentType.TRAILING_POSITION,
+ trails: [],
+ };
+ base.components[ComponentType.TRAILING_POSITION] = trailing_position;
+
+ const renderable: RenderableComponent = {
+ name: ComponentType.RENDERABLE,
+ };
+ base.components[ComponentType.RENDERABLE] = renderable;
+ return base;
+};
+
+export const create_cat = (base: Entity) => {
+ const renderable: RenderableComponent = {
+ name: ComponentType.RENDERABLE,
+ };
+ base.components[ComponentType.RENDERABLE] = renderable;
+ base.components[ComponentType.POSITION] = {
+ component: ComponentType.POSITION,
+ x: Math.random() * 1_000,
+ y: Math.random() * 1_000,
+ } as unknown as PositionComponent; // TODO: hack
+ return base;
+};
diff --git a/static/src/network.ts b/static/src/engine/events.ts
index fac96df..60632f5 100644
--- a/static/src/network.ts
+++ b/static/src/engine/events.ts
@@ -1,3 +1,4 @@
+import { Entity } from "./entity";
export enum EventType {
INITIAL_STATE = "INITIAL_STATE",
SET_CONTROLLABLE = "SET_CONTROLLABLE",
@@ -26,7 +27,7 @@ export interface InitialStateEvent extends Event {
event_type: EventType.INITIAL_STATE;
data: {
world: { width: number; height: number };
- entities: any[];
+ entities: Record<string, Entity>;
};
}
@@ -38,6 +39,20 @@ export interface SetControllableEvent extends Event {
};
}
+export interface EntityBornEvent extends Event {
+ event_type: EventType.ENTITY_BORN;
+ data: {
+ entity: Entity;
+ };
+}
+
+export interface EntityDeathEvent extends Event {
+ event_type: EventType.ENTITY_DEATH;
+ data: {
+ id: string;
+ };
+}
+
export interface EventQueue {
peek(): Event[];
clear(): void;
diff --git a/static/src/engine/game.ts b/static/src/engine/game.ts
new file mode 100644
index 0000000..64f63b5
--- /dev/null
+++ b/static/src/engine/game.ts
@@ -0,0 +1,112 @@
+import { System, SystemType } from "./system";
+import { Entity } from "./entity";
+import { ComponentType } from "./component";
+
+export class Game {
+ private running: boolean;
+ private last_update: number;
+
+ private readonly entities: Map<string, Entity> = new Map();
+ private readonly component_entities: Map<ComponentType, Set<string>> =
+ new Map();
+ private readonly systems: Map<SystemType, System> = new Map();
+ private readonly system_order: SystemType[];
+
+ constructor(
+ public readonly client_id: string,
+ systems: System[],
+ ) {
+ this.last_update = performance.now();
+ this.running = false;
+
+ systems.forEach((system) => this.systems.set(system.system_type, system));
+ this.system_order = systems.map(({ system_type }) => system_type);
+ }
+
+ public start() {
+ if (this.running) return;
+
+ console.log("starting game");
+ this.running = true;
+ this.last_update = performance.now();
+
+ const game_loop = (timestamp: number) => {
+ if (!this.running) return;
+
+ // rebuild component -> { entity } map
+ this.component_entities.clear();
+ Array.from(this.entities.values()).forEach((entity) =>
+ Object.values(entity.components).forEach((component) => {
+ const set =
+ this.component_entities.get(component.name) ?? new Set<string>();
+ set.add(entity.id);
+ this.component_entities.set(component.name, set);
+ }),
+ );
+
+ const dt = timestamp - this.last_update;
+
+ this.system_order.forEach((system_type) =>
+ this.systems.get(system_type)!.update(dt, this),
+ );
+
+ this.last_update = timestamp;
+ requestAnimationFrame(game_loop); // tail call recursion! /s
+ };
+ requestAnimationFrame(game_loop);
+ }
+
+ public stop() {
+ if (!this.running) return;
+ this.running = false;
+ }
+
+ public for_each_entity_with_component(
+ component: ComponentType,
+ callback: (entity: Entity) => void,
+ ) {
+ this.component_entities.get(component)?.forEach((entity_id) => {
+ const entity = this.entities.get(entity_id);
+ if (!entity) return;
+
+ callback(entity);
+ });
+ }
+
+ public get_entity(id: string) {
+ return this.entities.get(id);
+ }
+
+ public put_entity(entity: Entity) {
+ const old_entity = this.entities.get(entity.id);
+ if (old_entity) this.clear_entity_components(old_entity);
+
+ Object.values(entity.components).forEach((component) => {
+ const set =
+ this.component_entities.get(component.name) ?? new Set<string>();
+ set.add(entity.id);
+ this.component_entities.set(component.name, set);
+ });
+ this.entities.set(entity.id, entity);
+ }
+
+ public remove_entity(id: string) {
+ const entity = this.entities.get(id);
+ if (typeof entity === "undefined") return;
+
+ this.clear_entity_components(entity);
+ this.entities.delete(id);
+ }
+
+ private clear_entity_components(entity: Entity) {
+ Object.values(entity.components).forEach((component) => {
+ const set = this.component_entities.get(component.name);
+ if (typeof set === "undefined") return;
+ set.delete(entity.id);
+ });
+ }
+
+ public get_system<T extends System>(system_type: SystemType): T | undefined {
+ return this.systems.get(system_type) as T;
+ }
+}
diff --git a/static/src/engine/input.ts b/static/src/engine/input.ts
new file mode 100644
index 0000000..c1728e9
--- /dev/null
+++ b/static/src/engine/input.ts
@@ -0,0 +1,49 @@
+import { DebouncePublisher } from "./debounce_publisher";
+import { EntityPositionUpdateEvent, EventPublisher, EventType } from "./events";
+import { Game } from "./game";
+import { System, SystemType } from "./system";
+
+export class InputSystem extends System {
+ private readonly controllable_entities: Set<string> = new Set();
+ private readonly mouse_movement_debouncer: DebouncePublisher<{
+ x: number;
+ y: number;
+ }>;
+
+ constructor(
+ private readonly message_publisher: EventPublisher,
+ target: HTMLElement,
+ ) {
+ super(SystemType.INPUT);
+
+ this.mouse_movement_debouncer = new DebouncePublisher((data) =>
+ this.publish_mouse_movement(data),
+ );
+
+ target.addEventListener("mousemove", (event) => {
+ this.mouse_movement_debouncer.update({
+ x: event.clientX,
+ y: event.clientY,
+ });
+ });
+ }
+
+ private publish_mouse_movement({ x, y }: { x: number; y: number }) {
+ console.log(`publishing mouse movement at (${x}, ${y})`);
+ for (const entity_id of this.controllable_entities) {
+ this.message_publisher.add({
+ event_type: EventType.ENTITY_POSITION_UPDATE,
+ data: {
+ id: entity_id,
+ position: { x, y },
+ },
+ } as EntityPositionUpdateEvent);
+ }
+ }
+
+ public add_controllable_entity(entity_id: string) {
+ this.controllable_entities.add(entity_id);
+ }
+
+ public update(_dt: number, _game: Game) {}
+}
diff --git a/static/src/engine/network.ts b/static/src/engine/network.ts
new file mode 100644
index 0000000..850da65
--- /dev/null
+++ b/static/src/engine/network.ts
@@ -0,0 +1,138 @@
+import {
+ ComponentType,
+ PositionComponent,
+ TrailingPositionComponent,
+} from "./component";
+import { create_cat, create_laser, Entity, EntityType } from "./entity";
+import {
+ EntityBornEvent,
+ EntityDeathEvent,
+ EntityPositionUpdateEvent,
+ EventPublisher,
+ EventQueue,
+ EventType,
+ InitialStateEvent,
+ SetControllableEvent,
+} from "./events";
+import { Game } from "./game";
+import { InputSystem } from "./input";
+import { RenderSystem } from "./render";
+import { System, SystemType } from "./system";
+
+export class NetworkSystem extends System {
+ constructor(
+ private readonly event_queue: EventQueue,
+ private readonly event_publisher: EventPublisher,
+ ) {
+ super(SystemType.NETWORK);
+ }
+
+ public update(_dt: number, game: Game) {
+ const events = this.event_queue.peek();
+ for (const event of events) {
+ switch (event.event_type) {
+ case EventType.INITIAL_STATE:
+ this.process_initial_state_event(event as InitialStateEvent, game);
+ break;
+ case EventType.SET_CONTROLLABLE:
+ this.process_set_controllable_event(
+ event as SetControllableEvent,
+ game,
+ );
+ break;
+ case EventType.ENTITY_BORN:
+ this.process_entity_born_event(event as EntityBornEvent, game);
+ break;
+ case EventType.ENTITY_POSITION_UPDATE:
+ this.process_entity_position_update_event(
+ event as EntityPositionUpdateEvent,
+ game,
+ );
+ break;
+ case EventType.ENTITY_DEATH:
+ this.process_entity_death_event(event as EntityDeathEvent, game);
+ break;
+ }
+ }
+
+ this.event_queue.clear();
+ this.event_publisher.publish();
+ }
+
+ private process_initial_state_event(event: InitialStateEvent, game: Game) {
+ console.log("received initial state", event);
+ const { world, entities } = event.data;
+ const render_system = game.get_system<RenderSystem>(SystemType.RENDER);
+ if (!render_system) {
+ console.error("render system not found");
+ return;
+ }
+ render_system.set_world_dimensions(world.width, world.height);
+ Object.values(entities).forEach((entity) =>
+ game.put_entity(this.process_new_entity(entity)),
+ );
+ }
+
+ private process_entity_position_update_event(
+ event: EntityPositionUpdateEvent,
+ game: Game,
+ ) {
+ console.log("received entity position update", event);
+ const { position, id } = event.data;
+ const entity = game.get_entity(id);
+ if (typeof entity === "undefined") return;
+
+ const position_component = entity.components[
+ ComponentType.POSITION
+ ] as PositionComponent;
+ position_component.x = position.x;
+ position_component.y = position.y;
+
+ if (ComponentType.TRAILING_POSITION in entity.components) {
+ const trailing_position = entity.components[
+ ComponentType.TRAILING_POSITION
+ ] as TrailingPositionComponent;
+ trailing_position.trails.push({
+ x: position_component.x,
+ y: position_component.y,
+ time: Date.now(),
+ });
+ }
+ }
+
+ private process_entity_born_event(event: EntityBornEvent, game: Game) {
+ console.log("received a new entity", event);
+ const { entity } = event.data;
+ game.put_entity(this.process_new_entity(entity));
+ }
+
+ private process_new_entity(entity: Entity): Entity {
+ if (entity.entity_type === EntityType.LASER) {
+ return create_laser(entity);
+ }
+ if (entity.entity_type === EntityType.CAT) {
+ return create_cat(entity);
+ }
+ return entity;
+ }
+
+ private process_entity_death_event(event: EntityDeathEvent, game: Game) {
+ console.log("an entity died D:", event);
+ const { id } = event.data;
+ game.remove_entity(id);
+ }
+
+ private process_set_controllable_event(
+ event: SetControllableEvent,
+ game: Game,
+ ) {
+ console.log("got a controllable event", event);
+ if (event.data.client_id !== game.client_id) {
+ console.warn("got controllable event for client that is not us");
+ return;
+ }
+
+ const input_system = game.get_system<InputSystem>(SystemType.INPUT)!;
+ input_system.add_controllable_entity(event.data.id);
+ }
+}
diff --git a/static/src/engine/render.ts b/static/src/engine/render.ts
new file mode 100644
index 0000000..8f0343a
--- /dev/null
+++ b/static/src/engine/render.ts
@@ -0,0 +1,46 @@
+import {
+ ComponentType,
+ PositionComponent,
+ TrailingPositionComponent,
+} from "./component";
+import { Game } from "./game";
+import { System, SystemType } from "./system";
+import { drawLaserPen } from "laser-pen";
+
+export class RenderSystem extends System {
+ constructor(private readonly canvas: HTMLCanvasElement) {
+ super(SystemType.RENDER);
+ }
+
+ public set_world_dimensions(width: number, height: number) {
+ this.canvas.width = width;
+ this.canvas.height = height;
+ }
+
+ public update(_dt: number, game: Game) {
+ const ctx = this.canvas.getContext("2d");
+ if (ctx === null) return;
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+
+ game.for_each_entity_with_component(ComponentType.RENDERABLE, (entity) => {
+ if (ComponentType.TRAILING_POSITION in entity.components) {
+ const trailing_position = entity.components[
+ ComponentType.TRAILING_POSITION
+ ] as TrailingPositionComponent;
+ if (trailing_position.trails.length < 3) return;
+ drawLaserPen(ctx, trailing_position.trails);
+ return;
+ }
+
+ if (ComponentType.POSITION in entity.components) {
+ const position = entity.components[
+ ComponentType.POSITION
+ ] as PositionComponent;
+ ctx.beginPath();
+ ctx.arc(position.x, position.y, 50, 0, 2 * Math.PI);
+ ctx.stroke();
+ return;
+ }
+ });
+ }
+}
diff --git a/static/src/engine/system.ts b/static/src/engine/system.ts
new file mode 100644
index 0000000..e19cf5a
--- /dev/null
+++ b/static/src/engine/system.ts
@@ -0,0 +1,14 @@
+import { Game } from "./game";
+
+export enum SystemType {
+ INPUT = "INPUT",
+ NETWORK = "NETWORK",
+ RENDER = "RENDER",
+ TRAILING_POSITION = "TRAILING_POSITION",
+}
+
+export abstract class System {
+ constructor(public readonly system_type: SystemType) {}
+
+ abstract update(dt: number, game: Game): void;
+}
diff --git a/static/src/engine/trailing_position.ts b/static/src/engine/trailing_position.ts
new file mode 100644
index 0000000..1ea22d3
--- /dev/null
+++ b/static/src/engine/trailing_position.ts
@@ -0,0 +1,33 @@
+import { ComponentType, TrailingPositionComponent } from "./component";
+import { Game } from "./game";
+import { System, SystemType } from "./system";
+
+export class TrailingPositionSystem extends System {
+ constructor(
+ private readonly point_filter: (
+ trail_point: {
+ x: number;
+ y: number;
+ time: number;
+ }[],
+ ) => {
+ x: number;
+ y: number;
+ time: number;
+ }[],
+ ) {
+ super(SystemType.TRAILING_POSITION);
+ }
+
+ public update(_dt: number, game: Game) {
+ game.for_each_entity_with_component(
+ ComponentType.TRAILING_POSITION,
+ (entity) => {
+ const trailing_position = entity.components[
+ ComponentType.TRAILING_POSITION
+ ] as TrailingPositionComponent;
+ trailing_position.trails = this.point_filter(trailing_position.trails);
+ },
+ );
+ }
+}
diff --git a/static/src/entity.ts b/static/src/entity.ts
deleted file mode 100644
index 811d05a..0000000
--- a/static/src/entity.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Component } from "./component";
-
-export enum EntityType {
- LASER = "LASER",
-}
-
-export interface Entity {
- entity_type: EntityType;
- id: string;
- components: Record<string, Component>;
-}
diff --git a/static/src/main.ts b/static/src/main.ts
index c3588de..c11e41e 100644
--- a/static/src/main.ts
+++ b/static/src/main.ts
@@ -1,95 +1,16 @@
import $ from "jquery";
-import { Vec2 } from "./vector";
-import { MouseController } from "./mouse_controller";
import {
- EntityPositionUpdateEvent,
EventPublisher,
EventQueue,
- EventType,
- SetControllableEvent,
WebsocketEventPublisher,
WebSocketEventQueue,
-} from "./network";
-
-class KennelClient {
- private running: boolean;
- private last_update: number;
-
- private controllable_entities: Set<string> = new Set();
- private mouse_controller: MouseController;
-
- constructor(
- private readonly client_id: string,
- private readonly event_queue: EventQueue,
- private readonly event_publisher: EventPublisher,
- ) {
- this.last_update = 0;
- this.running = false;
-
- this.mouse_controller = new MouseController((position: Vec2) =>
- this.on_mouse_move(position),
- );
- }
-
- public start() {
- this.running = true;
- this.last_update = performance.now();
- this.mouse_controller.start();
-
- const loop = (timestamp: number) => {
- if (!this.running) return;
- const dt = timestamp - this.last_update;
- this.propogate_state_after(dt);
- requestAnimationFrame(loop); // tail call recursion! /s
- };
- requestAnimationFrame(loop);
-
- $(document).on("mousemove", (event) => {
- this.mouse_controller.move(event.clientX, event.clientY);
- });
- }
-
- public close() {
- this.running = false;
- this.mouse_controller.stop();
- this.controllable_entities.clear();
- $(document).off("mousemove");
- }
-
- private propogate_state_after(dt: number) {
- const events = this.event_queue.peek();
- for (const event of events) {
- switch (event.event_type) {
- case EventType.SET_CONTROLLABLE:
- this.process_set_controllable_event(event as SetControllableEvent);
- break;
- }
- }
- if (events.length > 0) {
- console.log(events, dt);
- }
- this.event_queue.clear();
- this.event_publisher.publish();
- }
-
- private process_set_controllable_event(event: SetControllableEvent) {
- if (event.data.client_id !== this.client_id) {
- console.warn("got controllable event for client that is not us");
- return;
- }
- this.controllable_entities.add(event.data.id);
- }
-
- private on_mouse_move(position: Vec2) {
- for (const id of this.controllable_entities) {
- const event: EntityPositionUpdateEvent = {
- event_type: EventType.ENTITY_POSITION_UPDATE,
- data: { id, position: { x: position.x, y: position.y } },
- };
- this.event_publisher.add(event);
- }
- }
-}
+} from "./engine/events";
+import { Game } from "./engine/game";
+import { NetworkSystem } from "./engine/network";
+import { RenderSystem } from "./engine/render";
+import { InputSystem } from "./engine/input";
+import { TrailingPositionSystem } from "./engine/trailing_position";
+import { drainPoints, setDelay } from "laser-pen";
$(async () => {
const client_id = await fetch("/assign", {
@@ -105,8 +26,25 @@ $(async () => {
const queue: EventQueue = new WebSocketEventQueue(ws);
const publisher: EventPublisher = new WebsocketEventPublisher(ws);
- const kennel_client = new KennelClient(client_id, queue, publisher);
- ws.onclose = () => kennel_client.close();
+ const network_system = new NetworkSystem(queue, publisher);
+
+ const gamecanvas = $("#gamecanvas").get(0)! as HTMLCanvasElement;
+ const input_system = new InputSystem(publisher, gamecanvas);
+
+ setDelay(500);
+ const render_system = new RenderSystem(gamecanvas);
+
+ const trailing_position = new TrailingPositionSystem(drainPoints);
+
+ const systems = [
+ network_system,
+ trailing_position,
+ input_system,
+ render_system,
+ ];
+
+ const game = new Game(client_id, systems);
+ ws.onclose = () => game.stop();
- kennel_client.start();
+ game.start();
});
diff --git a/static/src/vector.ts b/static/src/vector.ts
deleted file mode 100644
index 29e62df..0000000
--- a/static/src/vector.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export class Vec2 {
- constructor(
- public readonly x: number,
- public readonly y: number,
- ) {}
-
- public distance_to(that: Vec2): number {
- return Math.sqrt((this.x - that.x) ** 2 + (this.y - that.y) ** 2);
- }
-
- public copy(): Vec2 {
- return new Vec2(this.x, this.y);
- }
-}