From 8ec7f5368232d59f344e1067e1bad5e48dbcb7ae Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sat, 7 Sep 2024 20:20:07 -0700 Subject: get "cats" up there --- static/src/component.ts | 8 -- static/src/engine/component.ts | 24 ++++++ static/src/engine/debounce_publisher.ts | 46 +++++++++++ static/src/engine/entity.ts | 45 +++++++++++ static/src/engine/events.ts | 107 +++++++++++++++++++++++++ static/src/engine/game.ts | 112 ++++++++++++++++++++++++++ static/src/engine/input.ts | 49 ++++++++++++ static/src/engine/network.ts | 138 ++++++++++++++++++++++++++++++++ static/src/engine/render.ts | 46 +++++++++++ static/src/engine/system.ts | 14 ++++ static/src/engine/trailing_position.ts | 33 ++++++++ static/src/entity.ts | 11 --- static/src/main.ts | 116 +++++++-------------------- static/src/mouse_controller.ts | 47 ----------- static/src/network.ts | 92 --------------------- static/src/vector.ts | 14 ---- 16 files changed, 641 insertions(+), 261 deletions(-) delete mode 100644 static/src/component.ts create mode 100644 static/src/engine/component.ts create mode 100644 static/src/engine/debounce_publisher.ts create mode 100644 static/src/engine/entity.ts create mode 100644 static/src/engine/events.ts create mode 100644 static/src/engine/game.ts create mode 100644 static/src/engine/input.ts create mode 100644 static/src/engine/network.ts create mode 100644 static/src/engine/render.ts create mode 100644 static/src/engine/system.ts create mode 100644 static/src/engine/trailing_position.ts delete mode 100644 static/src/entity.ts delete mode 100644 static/src/mouse_controller.ts delete mode 100644 static/src/network.ts delete mode 100644 static/src/vector.ts (limited to 'static/src') 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/engine/debounce_publisher.ts b/static/src/engine/debounce_publisher.ts new file mode 100644 index 0000000..8ee4bb0 --- /dev/null +++ b/static/src/engine/debounce_publisher.ts @@ -0,0 +1,46 @@ +export class DebouncePublisher { + private last_event_time = Date.now(); + private unpublished_data: T | undefined; + private interval_id: number | undefined; + + constructor( + private readonly publisher: (data: T) => void | Promise, + private readonly debounce_ms = 100, + ) {} + + public start() { + if (typeof this.interval_id !== "undefined") { + return; + } + this.interval_id = setInterval( + () => this.debounce_publish(), + this.debounce_ms, + ); + } + + public stop() { + if (this.interval_id === null) { + return; + } + clearInterval(this.interval_id); + delete this.interval_id; + } + + public update(data: T) { + this.unpublished_data = data; + this.debounce_publish(); + } + + private debounce_publish() { + if ( + Date.now() - this.last_event_time < this.debounce_ms || + typeof this.unpublished_data === "undefined" + ) { + return; + } + + this.last_event_time = Date.now(); + 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; +} + +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/engine/events.ts b/static/src/engine/events.ts new file mode 100644 index 0000000..60632f5 --- /dev/null +++ b/static/src/engine/events.ts @@ -0,0 +1,107 @@ +import { Entity } from "./entity"; +export enum EventType { + INITIAL_STATE = "INITIAL_STATE", + SET_CONTROLLABLE = "SET_CONTROLLABLE", + ENTITY_BORN = "ENTITY_BORN", + ENTITY_DEATH = "ENTITY_DEATH", + ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE", +} + +export interface Event { + event_type: EventType; + data: any; +} + +export interface EntityPositionUpdateEvent extends Event { + event_type: EventType.ENTITY_POSITION_UPDATE; + data: { + id: string; + position: { + x: number; + y: number; + }; + }; +} + +export interface InitialStateEvent extends Event { + event_type: EventType.INITIAL_STATE; + data: { + world: { width: number; height: number }; + entities: Record; + }; +} + +export interface SetControllableEvent extends Event { + event_type: EventType.SET_CONTROLLABLE; + data: { + id: string; + client_id: string; + }; +} + +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; +} + +export interface EventPublisher { + add(event: Event): void; + publish(): void; +} + +export class WebSocketEventQueue implements EventQueue { + private queue: Event[]; + + constructor(websocket: WebSocket) { + this.queue = []; + this.listen_to(websocket); + } + + public peek() { + return this.queue; + } + + public clear() { + this.queue = []; + } + + private listen_to(websocket: WebSocket) { + websocket.onmessage = ({ data }) => { + this.queue = this.queue.concat(JSON.parse(data)); + }; + } +} + +export class WebsocketEventPublisher implements EventPublisher { + private queue: Event[]; + + constructor(private readonly websocket: WebSocket) { + this.queue = []; + } + + public add(event: Event) { + this.queue.push(event); + } + + public publish() { + if (this.queue.length === 0) { + return; + } + this.websocket.send(JSON.stringify(this.queue)); + this.queue = []; + } +} 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 = new Map(); + private readonly component_entities: Map> = + new Map(); + private readonly systems: Map = 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(); + 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(); + 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(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 = 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(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(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; -} 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 = 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/mouse_controller.ts b/static/src/mouse_controller.ts deleted file mode 100644 index bd8d51a..0000000 --- a/static/src/mouse_controller.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Vec2 } from "./vector"; -export class MouseController { - private last_event_time = Date.now(); - private last_movement: Vec2 | undefined; - private interval_id: number | undefined; - - constructor( - private readonly publisher: (new_movement: Vec2) => void | Promise, - private readonly debounce_ms = 200, - ) {} - - public start() { - if (typeof this.interval_id !== "undefined") { - return; - } - this.interval_id = setInterval( - () => this.publish_movement(), - this.debounce_ms, - ); - } - - public stop() { - if (this.interval_id === null) { - return; - } - clearInterval(this.interval_id); - delete this.interval_id; - } - - public move(x: number, y: number) { - this.last_movement = new Vec2(x, y); - this.publish_movement(); - } - - private publish_movement() { - if ( - Date.now() - this.last_event_time < this.debounce_ms || - typeof this.last_movement === "undefined" - ) { - return; - } - - this.last_event_time = Date.now(); - this.publisher(this.last_movement.copy()); - delete this.last_movement; - } -} diff --git a/static/src/network.ts b/static/src/network.ts deleted file mode 100644 index fac96df..0000000 --- a/static/src/network.ts +++ /dev/null @@ -1,92 +0,0 @@ -export enum EventType { - INITIAL_STATE = "INITIAL_STATE", - SET_CONTROLLABLE = "SET_CONTROLLABLE", - ENTITY_BORN = "ENTITY_BORN", - ENTITY_DEATH = "ENTITY_DEATH", - ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE", -} - -export interface Event { - event_type: EventType; - data: any; -} - -export interface EntityPositionUpdateEvent extends Event { - event_type: EventType.ENTITY_POSITION_UPDATE; - data: { - id: string; - position: { - x: number; - y: number; - }; - }; -} - -export interface InitialStateEvent extends Event { - event_type: EventType.INITIAL_STATE; - data: { - world: { width: number; height: number }; - entities: any[]; - }; -} - -export interface SetControllableEvent extends Event { - event_type: EventType.SET_CONTROLLABLE; - data: { - id: string; - client_id: string; - }; -} - -export interface EventQueue { - peek(): Event[]; - clear(): void; -} - -export interface EventPublisher { - add(event: Event): void; - publish(): void; -} - -export class WebSocketEventQueue implements EventQueue { - private queue: Event[]; - - constructor(websocket: WebSocket) { - this.queue = []; - this.listen_to(websocket); - } - - public peek() { - return this.queue; - } - - public clear() { - this.queue = []; - } - - private listen_to(websocket: WebSocket) { - websocket.onmessage = ({ data }) => { - this.queue = this.queue.concat(JSON.parse(data)); - }; - } -} - -export class WebsocketEventPublisher implements EventPublisher { - private queue: Event[]; - - constructor(private readonly websocket: WebSocket) { - this.queue = []; - } - - public add(event: Event) { - this.queue.push(event); - } - - public publish() { - if (this.queue.length === 0) { - return; - } - this.websocket.send(JSON.stringify(this.queue)); - this.queue = []; - } -} 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); - } -} -- cgit v1.3