Compare commits
	
		
			1 Commits
		
	
	
		
			8ec7f53682
			...
			e47d451426
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | e47d451426 | 
|  | @ -9,7 +9,14 @@ | |||
|   <title>the kennel.</title> | ||||
| </head> | ||||
| <body> | ||||
|   <div id="app"></div> | ||||
|   <noscript> | ||||
|     <div style="text-align: center"> | ||||
|       <h1>yeah, unfortunately you need javascript ;3</h1> | ||||
|     </div> | ||||
|   </noscript> | ||||
|   <div id="app"> | ||||
|     <canvas id="gamecanvas"></canvas> | ||||
|   </div> | ||||
|   <script type="module" src="/src/main.ts"></script> | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -8,7 +8,8 @@ | |||
|       "name": "kennel", | ||||
|       "version": "0.1.0", | ||||
|       "dependencies": { | ||||
|         "jquery": "^3.7.1" | ||||
|         "jquery": "^3.7.1", | ||||
|         "laser-pen": "^1.0.1" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@rollup/plugin-inject": "^5.0.5", | ||||
|  | @ -1230,6 +1231,12 @@ | |||
|       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", | ||||
|       "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" | ||||
|     }, | ||||
|     "node_modules/laser-pen": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/laser-pen/-/laser-pen-1.0.1.tgz", | ||||
|       "integrity": "sha512-+K3CQK5ryLDDjX0pEdSIRXh89F6KSpm225DEymWMheg2/umhUO+na/4l8MGs2gee7VO2Mx3kyG288WGrofasoA==", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/magic-string": { | ||||
|       "version": "0.30.11", | ||||
|       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
|     "vite-plugin-dynamic-base": "^1.1.0" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "jquery": "^3.7.1" | ||||
|     "jquery": "^3.7.1", | ||||
|     "laser-pen": "^1.0.1" | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +0,0 @@ | |||
| export enum ComponentType { | ||||
|   POSITION = "POSITION", | ||||
|   RENDERABLE = "RENDERABLE", | ||||
| } | ||||
| 
 | ||||
| export interface Component { | ||||
|   name: string; | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,30 @@ | |||
| import { | ||||
|   Component, | ||||
|   ComponentType, | ||||
|   RenderableComponent, | ||||
|   TrailingPositionComponent, | ||||
| } from "./component"; | ||||
| 
 | ||||
| export enum EntityType { | ||||
|   LASER = "LASER", | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| }; | ||||
|  | @ -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; | ||||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -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) {} | ||||
| } | ||||
|  | @ -0,0 +1,135 @@ | |||
| import { | ||||
|   ComponentType, | ||||
|   PositionComponent, | ||||
|   TrailingPositionComponent, | ||||
| } from "./component"; | ||||
| import { 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); | ||||
|     } | ||||
|     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); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,31 @@ | |||
| import { ComponentType, 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); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -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); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | @ -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>; | ||||
| } | ||||
|  | @ -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); | ||||
| 
 | ||||
|   kennel_client.start(); | ||||
|   const gamecanvas = $("#gamecanvas").get(0)!; | ||||
|   const input_system = new InputSystem(publisher, gamecanvas); | ||||
| 
 | ||||
|   setDelay(1_000); | ||||
|   const render_system = new RenderSystem(gamecanvas as HTMLCanvasElement); | ||||
| 
 | ||||
|   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(); | ||||
| 
 | ||||
|   game.start(); | ||||
| }); | ||||
|  |  | |||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue