get "cats" up there
	
		
			
	
		
	
	
		
			
				
	
				continuous-integration/drone/pr Build is failing
				
					Details
				
			
		
	
				
					
				
			
				
	
				continuous-integration/drone/pr Build is failing
				
					Details
				
			
		
	This commit is contained in:
		
							parent
							
								
									e4e31978ba
								
							
						
					
					
						commit
						8ec7f53682
					
				|  | @ -17,5 +17,7 @@ class Config: | ||||||
| 
 | 
 | ||||||
|     COOKIE_MAX_AGE = int(os.getenv("COOKIE_MAX_AGE", 60 * 60 * 24 * 7))  # 1 week |     COOKIE_MAX_AGE = int(os.getenv("COOKIE_MAX_AGE", 60 * 60 * 24 * 7))  # 1 week | ||||||
| 
 | 
 | ||||||
|  |     KENNEL_CATS_POLL_SEC = int(os.getenv("KENNEL_CATS_POLL_SEC", 10)) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| config = Config() | config = Config() | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ from enum import Enum | ||||||
| class ComponentType(str, Enum): | class ComponentType(str, Enum): | ||||||
|     POSITION = "POSITION" |     POSITION = "POSITION" | ||||||
|     CONTROLLABLE = "CONTROLLABLE" |     CONTROLLABLE = "CONTROLLABLE" | ||||||
|  |     MARKOV = "MARKOV" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Component: | class Component: | ||||||
|  |  | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | from kennel.engine.components.component import Component, ComponentType | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MarkovTransitionState(Component): | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         state_names: dict[int, str], | ||||||
|  |         initial_state_vector: list[float], | ||||||
|  |         transition_matrix: list[list[float]], | ||||||
|  |     ): | ||||||
|  |         # TODO: Poll rate per state? | ||||||
|  |         # TODO: State being an enum instead of a vector, just choose max and map | ||||||
|  |         self.state_names = state_names | ||||||
|  |         self.state = initial_state_vector | ||||||
|  |         self.transition_matrix = transition_matrix | ||||||
|  | 
 | ||||||
|  |         super().__init__(ComponentType.MARKOV) | ||||||
|  | 
 | ||||||
|  |     def get_max_state_name(self, state_vector: list[float]): | ||||||
|  |         max_val = max(state_vector) | ||||||
|  |         return self.state_names[state_vector.index(max_val)] | ||||||
|  | 
 | ||||||
|  |     def to_dict(self): | ||||||
|  |         return {"state": self.get_max_state_name(self.state)} | ||||||
|  | @ -1,6 +1,18 @@ | ||||||
|  | from kennel.engine.components.position import Position | ||||||
|  | 
 | ||||||
|  | from .entity import Entity, EntityType | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Cat(Entity): | ||||||
|  |     def __init__(self, id: str): | ||||||
|  |         components = [Position(0, 0)] | ||||||
|  | 
 | ||||||
|  |         super().__init__(EntityType.CAT, id, components) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| # | # | ||||||
| # # IDLE, FROLICKING, EEPY, ALERT, CHASING_CURSOR, CHASING_CAT, SCRATCHING, ITCHY | # # IDLE, FROLICKING, EEPY, ALERT, CHASING_CURSOR, CHASING_CAT, SCRATCHING, ITCHY | ||||||
| # state_stochastic_matrix = [ | # state_stochastic_matrix = [ [1, 0] | ||||||
| #     # IDLE | #     # IDLE | ||||||
| #     [0.5, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0], | #     [0.5, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0], | ||||||
| #     # FROLICKING | #     # FROLICKING | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ from kennel.engine.components.component import Component, ComponentType | ||||||
| 
 | 
 | ||||||
| class EntityType(str, Enum): | class EntityType(str, Enum): | ||||||
|     LASER = "LASER" |     LASER = "LASER" | ||||||
|  |     CAT = "CAT" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Entity: | class Entity: | ||||||
|  |  | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | from kennel.engine.components.component import ComponentType | ||||||
|  | from kennel.engine.entities.entity import EntityManager | ||||||
|  | from kennel.engine.systems.system import System, SystemType | ||||||
|  | from kennel.engine.systems.network import NetworkSystem | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MarkovTransitionStateSystem(System): | ||||||
|  |     def __init__(self, network_system: NetworkSystem): | ||||||
|  |         super().__init__(SystemType.MARKOV) | ||||||
|  | 
 | ||||||
|  |     def update(self, entity_manager: EntityManager, delta_time: float): | ||||||
|  |         entity_manager.get_entities_with_component(ComponentType.MARKOV) | ||||||
|  |         return | ||||||
|  | @ -7,6 +7,7 @@ from kennel.engine.entities.entity import EntityManager | ||||||
| class SystemType(str, Enum): | class SystemType(str, Enum): | ||||||
|     NETWORK = "NETWORK" |     NETWORK = "NETWORK" | ||||||
|     WORLD = "WORLD" |     WORLD = "WORLD" | ||||||
|  |     MARKOV = "MARKOV" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class System: | class System: | ||||||
|  | @ -14,7 +15,7 @@ class System: | ||||||
|         self.system_type = system_type |         self.system_type = system_type | ||||||
| 
 | 
 | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     async def update(self, entity_manager: EntityManager, delta_time: float): |     async def update(self, entity_manager: EntityManager, delta_time: float) -> None: | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,18 +1,27 @@ | ||||||
|  | import asyncio | ||||||
| import uuid | import uuid | ||||||
|  | import time | ||||||
| from typing import List, Optional | from typing import List, Optional | ||||||
| 
 | 
 | ||||||
| from kennel.config import config | from kennel.config import config | ||||||
| from kennel.engine.components.component import ComponentType | from kennel.engine.components.component import ComponentType | ||||||
| from kennel.engine.entities.entity import Entity, EntityManager | from kennel.engine.entities.entity import Entity, EntityManager | ||||||
| from kennel.engine.entities.laser import Laser | from kennel.engine.entities.laser import Laser | ||||||
|  | from kennel.engine.entities.cat import Cat | ||||||
| from kennel.engine.game import Game | from kennel.engine.game import Game | ||||||
|  | from kennel.engine.systems.markov_transition_state_system import ( | ||||||
|  |     MarkovTransitionStateSystem, | ||||||
|  | ) | ||||||
| from kennel.engine.systems.network import ( | from kennel.engine.systems.network import ( | ||||||
|     EntityPositionUpdateEvent, |     EntityPositionUpdateEvent, | ||||||
|  |     EntityBornEvent, | ||||||
|  |     EntityDeathEvent, | ||||||
|     Event, |     Event, | ||||||
|     EventType, |     EventType, | ||||||
|     NetworkSystem, |     NetworkSystem, | ||||||
|     UpstreamEventProcessor, |     UpstreamEventProcessor, | ||||||
| ) | ) | ||||||
|  | from kennel.kennelcats import KennelCatService, KennelCat | ||||||
| from kennel.engine.systems.system import SystemManager | from kennel.engine.systems.system import SystemManager | ||||||
| from kennel.engine.systems.world import WorldSystem | from kennel.engine.systems.world import WorldSystem | ||||||
| 
 | 
 | ||||||
|  | @ -58,12 +67,76 @@ class KennelEventProcessor(UpstreamEventProcessor): | ||||||
|         return event |         return event | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT)) | class KennelCatsManager: | ||||||
| system_manager.add_system( |     kennel_cat_service: KennelCatService | ||||||
|     NetworkSystem(KennelEventProcessor()), |     entity_manager: EntityManager | ||||||
| ) |     network_system: NetworkSystem | ||||||
|  |     last_seen: set[str] | ||||||
|  |     poll_interval_sec: int | ||||||
|  |     running: bool | ||||||
|  | 
 | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         kennel_cat_service: KennelCatService, | ||||||
|  |         entity_manager: EntityManager, | ||||||
|  |         network_system: NetworkSystem, | ||||||
|  |         poll_interval_sec: int, | ||||||
|  |     ): | ||||||
|  |         self.kennel_cat_service = kennel_cat_service | ||||||
|  |         self.entity_manager = entity_manager | ||||||
|  |         self.network_system = network_system | ||||||
|  |         self.poll_interval_sec = poll_interval_sec | ||||||
|  | 
 | ||||||
|  |         self.last_seen = set() | ||||||
|  |         self.running = False | ||||||
|  | 
 | ||||||
|  |     async def start(self) -> None: | ||||||
|  |         logger.info("starting kennel cats manager") | ||||||
|  |         if self.running: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         self.running = True | ||||||
|  |         while self.running: | ||||||
|  |             logger.info("polling kennel cats service") | ||||||
|  |             cats = self.kennel_cat_service.get_kennel() | ||||||
|  |             cats_table = {cat.id: cat for cat in cats} | ||||||
|  |             cat_ids = set([cat.id for cat in cats]) | ||||||
|  | 
 | ||||||
|  |             removed_cats = [cats_table[id] for id in self.last_seen.difference(cat_ids)] | ||||||
|  |             added_cats = [cats_table[id] for id in cat_ids.difference(self.last_seen)] | ||||||
|  |             logger.info(f"removing {removed_cats}") | ||||||
|  |             logger.info(f"adding {added_cats}") | ||||||
|  | 
 | ||||||
|  |             for removed in removed_cats: | ||||||
|  |                 self.entity_manager.remove_entity(removed) | ||||||
|  |                 entity_death = EntityDeathEvent(removed.id) | ||||||
|  |                 self.network_system.server_global_event(entity_death) | ||||||
|  | 
 | ||||||
|  |             for added in added_cats: | ||||||
|  |                 new_cat = Cat(added.id) | ||||||
|  |                 self.entity_manager.add_entity(new_cat) | ||||||
|  |                 entity_born = EntityBornEvent(new_cat) | ||||||
|  |                 self.network_system.server_global_event(entity_born) | ||||||
|  | 
 | ||||||
|  |             self.last_seen = cat_ids | ||||||
|  |             await asyncio.sleep(self.poll_interval_sec) | ||||||
|  | 
 | ||||||
|  |     def stop(self) -> None: | ||||||
|  |         logger.info("stopping kennel cats manager") | ||||||
|  |         self.running = False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | network_system = NetworkSystem(KennelEventProcessor()) | ||||||
|  | world_system = WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT) | ||||||
|  | markov_transition_state_system = MarkovTransitionStateSystem(network_system) | ||||||
|  | system_manager.add_system(network_system) | ||||||
|  | system_manager.add_system(world_system) | ||||||
| 
 | 
 | ||||||
| kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP) | kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP) | ||||||
|  | kennel_cat_service = KennelCatService(config.HATECOMPUTERS_ENDPOINT) | ||||||
|  | kennel_cats_manager = KennelCatsManager( | ||||||
|  |     kennel_cat_service, entity_manager, network_system, config.KENNEL_CATS_POLL_SEC | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def create_session_controllable_entities(session: str) -> List[Entity]: | def create_session_controllable_entities(session: str) -> List[Entity]: | ||||||
|  |  | ||||||
|  | @ -37,10 +37,10 @@ class KennelCat: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class KennelCatService: | class KennelCatService: | ||||||
|     def __init__(self, endpoint: str): |     def __init__(self, hc_endpoint: str): | ||||||
|         self.endpoint = endpoint |         self.hc_endpoint = hc_endpoint | ||||||
| 
 | 
 | ||||||
|     def get_kennel(self) -> List[KennelCat]: |     def get_kennel(self) -> List[KennelCat]: | ||||||
|         response = requests.get(f"{self.endpoint}/kennel") |         response = requests.get(f"{self.hc_endpoint}/kennel") | ||||||
|         response.raise_for_status() |         response.raise_for_status() | ||||||
|         return [KennelCat.from_dict(cat) for cat in response.json()] |         return [KennelCat.from_dict(cat) for cat in response.json()] | ||||||
|  |  | ||||||
|  | @ -30,6 +30,7 @@ from kennel.kennel import ( | ||||||
|     create_session_controllable_entities, |     create_session_controllable_entities, | ||||||
|     entity_manager, |     entity_manager, | ||||||
|     kennel, |     kennel, | ||||||
|  |     kennel_cats_manager, | ||||||
|     system_manager, |     system_manager, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -43,12 +44,14 @@ loop = asyncio.get_event_loop() | ||||||
| async def startup_event(): | async def startup_event(): | ||||||
|     logger.info("Starting Kennel...") |     logger.info("Starting Kennel...") | ||||||
|     loop.create_task(kennel.run()) |     loop.create_task(kennel.run()) | ||||||
|  |     loop.create_task(kennel_cats_manager.start()) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.on_event("shutdown") | @app.on_event("shutdown") | ||||||
| async def shutdown_event(): | async def shutdown_event(): | ||||||
|     logger.info("Stopping Kennel...") |     logger.info("Stopping Kennel...") | ||||||
|     kennel.stop() |     kennel.stop() | ||||||
|  |     kennel_cats_manager.stop() | ||||||
|     loop.stop() |     loop.stop() | ||||||
|     logger.info("Kennel stopped") |     logger.info("Kennel stopped") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,7 +9,14 @@ | ||||||
|   <title>the kennel.</title> |   <title>the kennel.</title> | ||||||
| </head> | </head> | ||||||
| <body> | <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> |   <script type="module" src="/src/main.ts"></script> | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -8,7 +8,8 @@ | ||||||
|       "name": "kennel", |       "name": "kennel", | ||||||
|       "version": "0.1.0", |       "version": "0.1.0", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "jquery": "^3.7.1" |         "jquery": "^3.7.1", | ||||||
|  |         "laser-pen": "^1.0.1" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@rollup/plugin-inject": "^5.0.5", |         "@rollup/plugin-inject": "^5.0.5", | ||||||
|  | @ -1230,6 +1231,12 @@ | ||||||
|       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", |       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", | ||||||
|       "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" |       "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": { |     "node_modules/magic-string": { | ||||||
|       "version": "0.30.11", |       "version": "0.30.11", | ||||||
|       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", |       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", | ||||||
|  |  | ||||||
|  | @ -4,10 +4,9 @@ | ||||||
|   "version": "0.1.0", |   "version": "0.1.0", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "vite", |  | ||||||
|     "build": "tsc && vite build", |     "build": "tsc && vite build", | ||||||
|     "preview": "vite preview", |     "preview": "vite preview", | ||||||
|     "watch": "./node_modules/nodemon/bin/nodemon.js --watch './src/**/*' -e ts,html --exec \"npm run build\"" |     "dev": "./node_modules/nodemon/bin/nodemon.js --watch './src/**/*' -e ts,html --exec \"npm run build\"" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@rollup/plugin-inject": "^5.0.5", |     "@rollup/plugin-inject": "^5.0.5", | ||||||
|  | @ -18,6 +17,7 @@ | ||||||
|     "vite-plugin-dynamic-base": "^1.1.0" |     "vite-plugin-dynamic-base": "^1.1.0" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "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 DebouncePublisher<T> { | ||||||
| export class MouseController { |  | ||||||
|   private last_event_time = Date.now(); |   private last_event_time = Date.now(); | ||||||
|   private last_movement: Vec2 | undefined; |   private unpublished_data: T | undefined; | ||||||
|   private interval_id: number | undefined; |   private interval_id: number | undefined; | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     private readonly publisher: (new_movement: Vec2) => void | Promise<void>, |     private readonly publisher: (data: T) => void | Promise<void>, | ||||||
|     private readonly debounce_ms = 200, |     private readonly debounce_ms = 100, | ||||||
|   ) {} |   ) {} | ||||||
| 
 | 
 | ||||||
|   public start() { |   public start() { | ||||||
|  | @ -14,7 +13,7 @@ export class MouseController { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     this.interval_id = setInterval( |     this.interval_id = setInterval( | ||||||
|       () => this.publish_movement(), |       () => this.debounce_publish(), | ||||||
|       this.debounce_ms, |       this.debounce_ms, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | @ -27,21 +26,21 @@ export class MouseController { | ||||||
|     delete this.interval_id; |     delete this.interval_id; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public move(x: number, y: number) { |   public update(data: T) { | ||||||
|     this.last_movement = new Vec2(x, y); |     this.unpublished_data = data; | ||||||
|     this.publish_movement(); |     this.debounce_publish(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private publish_movement() { |   private debounce_publish() { | ||||||
|     if ( |     if ( | ||||||
|       Date.now() - this.last_event_time < this.debounce_ms || |       Date.now() - this.last_event_time < this.debounce_ms || | ||||||
|       typeof this.last_movement === "undefined" |       typeof this.unpublished_data === "undefined" | ||||||
|     ) { |     ) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.last_event_time = Date.now(); |     this.last_event_time = Date.now(); | ||||||
|     this.publisher(this.last_movement.copy()); |     this.publisher(this.unpublished_data); | ||||||
|     delete this.last_movement; |     this.unpublished_data = undefined; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | @ -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; | ||||||
|  | }; | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | import { Entity } from "./entity"; | ||||||
| export enum EventType { | export enum EventType { | ||||||
|   INITIAL_STATE = "INITIAL_STATE", |   INITIAL_STATE = "INITIAL_STATE", | ||||||
|   SET_CONTROLLABLE = "SET_CONTROLLABLE", |   SET_CONTROLLABLE = "SET_CONTROLLABLE", | ||||||
|  | @ -26,7 +27,7 @@ export interface InitialStateEvent extends Event { | ||||||
|   event_type: EventType.INITIAL_STATE; |   event_type: EventType.INITIAL_STATE; | ||||||
|   data: { |   data: { | ||||||
|     world: { width: number; height: number }; |     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 { | export interface EventQueue { | ||||||
|   peek(): Event[]; |   peek(): Event[]; | ||||||
|   clear(): void; |   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,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); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -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; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -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 $ from "jquery"; | ||||||
| import { Vec2 } from "./vector"; |  | ||||||
| import { MouseController } from "./mouse_controller"; |  | ||||||
| import { | import { | ||||||
|   EntityPositionUpdateEvent, |  | ||||||
|   EventPublisher, |   EventPublisher, | ||||||
|   EventQueue, |   EventQueue, | ||||||
|   EventType, |  | ||||||
|   SetControllableEvent, |  | ||||||
|   WebsocketEventPublisher, |   WebsocketEventPublisher, | ||||||
|   WebSocketEventQueue, |   WebSocketEventQueue, | ||||||
| } from "./network"; | } from "./engine/events"; | ||||||
| 
 | import { Game } from "./engine/game"; | ||||||
| class KennelClient { | import { NetworkSystem } from "./engine/network"; | ||||||
|   private running: boolean; | import { RenderSystem } from "./engine/render"; | ||||||
|   private last_update: number; | import { InputSystem } from "./engine/input"; | ||||||
| 
 | import { TrailingPositionSystem } from "./engine/trailing_position"; | ||||||
|   private controllable_entities: Set<string> = new Set(); | import { drainPoints, setDelay } from "laser-pen"; | ||||||
|   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); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| $(async () => { | $(async () => { | ||||||
|   const client_id = await fetch("/assign", { |   const client_id = await fetch("/assign", { | ||||||
|  | @ -105,8 +26,25 @@ $(async () => { | ||||||
| 
 | 
 | ||||||
|   const queue: EventQueue = new WebSocketEventQueue(ws); |   const queue: EventQueue = new WebSocketEventQueue(ws); | ||||||
|   const publisher: EventPublisher = new WebsocketEventPublisher(ws); |   const publisher: EventPublisher = new WebsocketEventPublisher(ws); | ||||||
|   const kennel_client = new KennelClient(client_id, queue, publisher); |   const network_system = new NetworkSystem(queue, publisher); | ||||||
|   ws.onclose = () => kennel_client.close(); |  | ||||||
| 
 | 
 | ||||||
|   kennel_client.start(); |   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(); | ||||||
|  | 
 | ||||||
|  |   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