improve event parsing and begin sending events from the client
	
		
			
	
		
	
	
		
			
				
	
				continuous-integration/drone/pr Build is failing
				
					Details
				
			
		
	
				
					
				
			
				
	
				continuous-integration/drone/pr Build is failing
				
					Details
				
			
		
	This commit is contained in:
		
							parent
							
								
									b144ad5df7
								
							
						
					
					
						commit
						a0a2068b66
					
				|  | @ -12,5 +12,5 @@ class Component: | ||||||
|         self.component_type = component_type |         self.component_type = component_type | ||||||
| 
 | 
 | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     def dict(self) -> dict: |     def to_dict(self) -> dict: | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  | @ -9,6 +9,6 @@ class Controllable(Component): | ||||||
|     def __repr__(self) -> str: |     def __repr__(self) -> str: | ||||||
|         return f"Controllable(by={self.by})" |         return f"Controllable(by={self.by})" | ||||||
| 
 | 
 | ||||||
|     def dict(self) -> dict: |     def to_dict(self) -> dict: | ||||||
|         # don't serialize who owns this |         # don't serialize who owns this | ||||||
|         return {} |         return {} | ||||||
|  |  | ||||||
|  | @ -10,5 +10,5 @@ class Position(Component): | ||||||
|     def __repr__(self) -> str: |     def __repr__(self) -> str: | ||||||
|         return f"Position(x={self.x}, y={self.y})" |         return f"Position(x={self.x}, y={self.y})" | ||||||
| 
 | 
 | ||||||
|     def dict(self) -> dict: |     def to_dict(self) -> dict: | ||||||
|         return {"x": self.x, "y": self.y} |         return {"x": self.x, "y": self.y} | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ class Entity: | ||||||
|         return { |         return { | ||||||
|             "entity_type": self.entity_type, |             "entity_type": self.entity_type, | ||||||
|             "id": self.id, |             "id": self.id, | ||||||
|             "components": {k: v.dict() for k, v in self.components.items()}, |             "components": {k: v.to_dict() for k, v in self.components.items()}, | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,17 @@ | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from kennel.engine.entities.entity import EntityManager | from kennel.engine.entities.entity import Entity, EntityType, EntityManager | ||||||
| from .system import System, SystemType | from .system import System, SystemType | ||||||
| from abc import abstractmethod | from abc import abstractmethod | ||||||
|  | from typing import Optional, List | ||||||
| import asyncio | import asyncio | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class EventType(str, Enum): | class EventType(str, Enum): | ||||||
|     INITIAL_STATE = "INITIAL_STATE" |     INITIAL_STATE = "INITIAL_STATE" | ||||||
|  |     SET_CONTROLLABLE = "SET_CONTROLLABLE" | ||||||
|     ENTITY_BORN = "ENTITY_BORN" |     ENTITY_BORN = "ENTITY_BORN" | ||||||
|     ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE" |  | ||||||
|     ENTITY_DEATH = "ENTITY_DEATH" |     ENTITY_DEATH = "ENTITY_DEATH" | ||||||
|  |     ENTITY_POSITION_UPDATE = "ENTITY_POSITION_UPDATE" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Event: | class Event: | ||||||
|  | @ -17,29 +19,86 @@ class Event: | ||||||
|         self.event_type = event_type |         self.event_type = event_type | ||||||
|         self.data = data |         self.data = data | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self) -> str: | ||||||
|         return f"Event({self.event_type}, {self.data})" |         return f"Event({self.event_type}, {self.data})" | ||||||
| 
 | 
 | ||||||
|     def dict(self): |     def to_dict(self) -> dict: | ||||||
|         return {"event_type": self.event_type, "data": self.data} |         return {"event_type": self.event_type, "data": self.data} | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def from_dict(data: dict): |     def from_dict(data: dict) -> Optional["Event"]: | ||||||
|  |         if "event_type" not in data or "data" not in data: | ||||||
|  |             return | ||||||
|  |         event_type = data["event_type"] | ||||||
|  |         if event_type == EventType.INITIAL_STATE: | ||||||
|  |             return InitialStateEvent( | ||||||
|  |                 data["data"]["world"]["width"], | ||||||
|  |                 data["data"]["world"]["height"], | ||||||
|  |                 data["data"]["entities"], | ||||||
|  |             ) | ||||||
|  |         if event_type == EventType.SET_CONTROLLABLE: | ||||||
|  |             return SetControllableEvent(data["data"]["id"], data["data"]["client_id"]) | ||||||
|  |         if event_type == EventType.ENTITY_BORN: | ||||||
|  |             return EntityBornEvent(data["data"]) | ||||||
|  |         if event_type == EventType.ENTITY_POSITION_UPDATE: | ||||||
|  |             return EntityPositionUpdateEvent( | ||||||
|  |                 data["data"]["id"], data["data"]["position"] | ||||||
|  |             ) | ||||||
|  |         if event_type == EventType.ENTITY_DEATH: | ||||||
|  |             return EntityDeathEvent(data["data"]["id"]) | ||||||
|  | 
 | ||||||
|  |         logger.warn(f"Unknown event type: {data['event_type']}") | ||||||
|         return Event(EventType(data["event_type"]), data["data"]) |         return Event(EventType(data["event_type"]), data["data"]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class InitialStateEvent(Event): | ||||||
|  |     def __init__(self, world_width: int, world_height: int, entities: List[Entity]): | ||||||
|  |         self.world_width = world_width | ||||||
|  |         self.world_height = world_height | ||||||
|  |         self.entities = entities | ||||||
|  |         super().__init__( | ||||||
|  |             EventType.INITIAL_STATE, | ||||||
|  |             { | ||||||
|  |                 "world": {"width": world_width, "height": world_height}, | ||||||
|  |                 "entities": [entity.to_dict() for entity in entities], | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SetControllableEvent(Event): | ||||||
|  |     def __init__(self, entity_id: str, client_id: str): | ||||||
|  |         self.entity_id = entity_id | ||||||
|  |         self.client_id = client_id | ||||||
|  |         super().__init__( | ||||||
|  |             EventType.SET_CONTROLLABLE, {"id": entity_id, "client_id": client_id} | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EntityBornEvent(Event): | ||||||
|  |     def __init__(self, entity: Entity): | ||||||
|  |         self.entity = entity | ||||||
|  |         super().__init__(EventType.ENTITY_BORN, {"entity": entity.to_dict()}) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EntityPositionUpdateEvent(Event): | ||||||
|  |     def __init__(self, entity_id: str, position: dict): | ||||||
|  |         super().__init__( | ||||||
|  |             EventType.ENTITY_POSITION_UPDATE, | ||||||
|  |             {"id": entity_id, "position": position}, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EntityDeathEvent(Event): | ||||||
|  |     def __init__(self, entity_id: str): | ||||||
|  |         super().__init__(EventType.ENTITY_DEATH, {"id": entity_id}) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class Publishable: | class Publishable: | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     async def publish(self, event: Event): |     async def publish(self, event: Event): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ClientEventTransformer: |  | ||||||
|     @abstractmethod |  | ||||||
|     def apply(self, event: Event) -> Event: |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class EventProcessor: | class EventProcessor: | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     def accept(self, entity_manager: EntityManager, event: Event) -> None: |     def accept(self, entity_manager: EntityManager, event: Event) -> None: | ||||||
|  | @ -47,37 +106,32 @@ class EventProcessor: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class NetworkSystem(System): | class NetworkSystem(System): | ||||||
|     def __init__( |     def __init__(self, event_processor: EventProcessor): | ||||||
|         self, |  | ||||||
|         event_processor: EventProcessor, |  | ||||||
|         client_event_transformer: ClientEventTransformer, |  | ||||||
|     ): |  | ||||||
|         super().__init__(SystemType.NETWORK) |         super().__init__(SystemType.NETWORK) | ||||||
|         self.event_processor = event_processor |         self.event_processor = event_processor | ||||||
|         self.client_event_transformer = client_event_transformer |  | ||||||
| 
 | 
 | ||||||
|         self.events = [] |         self.events = [] | ||||||
|  |         self.client_events = [] | ||||||
|         self.clients = {} |         self.clients = {} | ||||||
| 
 | 
 | ||||||
|     async def update(self, entity_manager: EntityManager, delta_time: float) -> None: |     async def update(self, entity_manager: EntityManager, delta_time: float) -> None: | ||||||
|         for event in self.events: |         if len(self.events) + len(self.client_events) == 0: | ||||||
|  |             return | ||||||
|  |         for event in self.events + self.client_events: | ||||||
|             self.event_processor.accept(entity_manager, event) |             self.event_processor.accept(entity_manager, event) | ||||||
|         client_events = [ |         client_sendable = self.events + [event for event, _ in self.client_events] | ||||||
|             self.client_event_transformer.apply(entity_manager, event) |  | ||||||
|             for event in self.events |  | ||||||
|         ] |  | ||||||
|         await asyncio.gather( |         await asyncio.gather( | ||||||
|             *[ |             *[ | ||||||
|                 client.publish(event) |                 client.publish(event) | ||||||
|                 for client in self.clients.values() |                 for client in self.clients.values() | ||||||
|                 for event in client_events |                 for event in client_sendable | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|         self.events = [] |         self.events = [] | ||||||
|  |         self.client_events = [] | ||||||
| 
 | 
 | ||||||
|     def client_event(self, client_id: str, event: Event) -> None: |     def client_event(self, client_id: str, event: Event) -> None: | ||||||
|         event.data["client_id"] = client_id |         self.client_events.append((event, client_id)) | ||||||
|         self.events.append(event) |  | ||||||
| 
 | 
 | ||||||
|     def add_event(self, event: Event) -> None: |     def add_event(self, event: Event) -> None: | ||||||
|         self.events.append(event) |         self.events.append(event) | ||||||
|  |  | ||||||
|  | @ -6,7 +6,6 @@ from kennel.engine.systems.system import SystemManager | ||||||
| from kennel.engine.systems.network import ( | from kennel.engine.systems.network import ( | ||||||
|     NetworkSystem, |     NetworkSystem, | ||||||
|     EventProcessor, |     EventProcessor, | ||||||
|     ClientEventTransformer, |  | ||||||
|     Event, |     Event, | ||||||
|     EventType, |     EventType, | ||||||
| ) | ) | ||||||
|  | @ -21,28 +20,33 @@ system_manager = SystemManager() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class KennelEventProcessor(EventProcessor): | class KennelEventProcessor(EventProcessor): | ||||||
|     def accept(self, entity_manager: EntityManager, event: Event) -> None: |     def accept( | ||||||
|  |         self, entity_manager: EntityManager, event: type[Event | tuple[Event, str]] | ||||||
|  |     ) -> None: | ||||||
|  |         if isinstance(event, tuple): | ||||||
|  |             client_event, client_id = event | ||||||
|  |             self._process_client_event(entity_manager, client_event, client_id) | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |     def _process_client_event( | ||||||
|  |         self, entity_manager: EntityManager, event: Event, client_id: str | ||||||
|  |     ) -> None: | ||||||
|         if event.event_type == EventType.ENTITY_POSITION_UPDATE: |         if event.event_type == EventType.ENTITY_POSITION_UPDATE: | ||||||
|             self._process_entity_position_update(entity_manager, event) |             self._process_entity_position_update(entity_manager, event, client_id) | ||||||
| 
 | 
 | ||||||
|     def _process_entity_position_update( |     def _process_entity_position_update( | ||||||
|         self, entity_manager: EntityManager, event: Event |         self, entity_manager: EntityManager, event: Event, client_id: str | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         entity = entity_manager.get_entity(event.data["entity_id"]) |         entity = entity_manager.get_entity(event.data["id"]) | ||||||
|         if entity is None: |         if entity is None: | ||||||
|             logger.error(f"Entity {event.data['entity_id']} does not exist") |             logger.error(f"Entity(id={event.data['id']}) does not exist") | ||||||
|             return |  | ||||||
|         if event.data["client_id"] is None: |  | ||||||
|             logger.error("Client ID is required for position updates") |  | ||||||
|             return |             return | ||||||
|         controllable = entity.get_component(ComponentType.CONTROLLABLE) |         controllable = entity.get_component(ComponentType.CONTROLLABLE) | ||||||
|         if controllable is None: |         if controllable is None: | ||||||
|             logger.error(f"Entity {entity} is not controllable") |             logger.error(f"Entity {entity} is not controllable") | ||||||
|             return |             return | ||||||
|         if controllable.by != event.data["client_id"]: |         if controllable.by != client_id: | ||||||
|             logger.error( |             logger.error(f"Entity {entity} is not controllable by client {client_id}") | ||||||
|                 f"Entity {entity} is not controllable by client {event.data['client_id']}" |  | ||||||
|             ) |  | ||||||
|             return |             return | ||||||
|         position = entity.get_component(ComponentType.POSITION) |         position = entity.get_component(ComponentType.POSITION) | ||||||
|         if position is None: |         if position is None: | ||||||
|  | @ -51,30 +55,14 @@ class KennelEventProcessor(EventProcessor): | ||||||
|         entity.add_component(position) |         entity.add_component(position) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class KennelClientEventTransformer(ClientEventTransformer): |  | ||||||
|     def apply(self, entity_manager: EntityManager, event: Event) -> Event: |  | ||||||
|         if event.event_type == EventType.ENTITY_BORN: |  | ||||||
|             id = event.data["id"] |  | ||||||
|             entity = entity_manager.get_entity(id) |  | ||||||
|             if entity is None: |  | ||||||
|                 logger.error(f"Entity {id} does not exist") |  | ||||||
|                 return event |  | ||||||
|             for component_type in entity.components: |  | ||||||
|                 component = entity.get_component(component_type) |  | ||||||
|                 if component is not None: |  | ||||||
|                     event.data[component_type] = component.dict() |  | ||||||
| 
 |  | ||||||
|         return event |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT)) | system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT)) | ||||||
| system_manager.add_system( | system_manager.add_system( | ||||||
|     NetworkSystem(EventProcessor(), KennelClientEventTransformer()), |     NetworkSystem(KennelEventProcessor()), | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP) | kennel = Game(entity_manager, system_manager, config.MIN_TIME_STEP) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def create_session_controllable_entities(session: str) -> List[Entity]: | def create_session_controllable_entities(session: str) -> List[Entity]: | ||||||
|     laser = Laser(uuid.uuid4().hex, session) |     laser = Laser(uuid.uuid4().hex[:10], session) | ||||||
|     return [laser] |     return [laser] | ||||||
|  |  | ||||||
|  | @ -11,7 +11,14 @@ from fastapi import ( | ||||||
| from fastapi.responses import FileResponse | from fastapi.responses import FileResponse | ||||||
| from fastapi.staticfiles import StaticFiles | from fastapi.staticfiles import StaticFiles | ||||||
| from kennel.engine.systems.system import SystemType | from kennel.engine.systems.system import SystemType | ||||||
| from kennel.engine.systems.network import Event, Publishable, EventType | from kennel.engine.systems.network import ( | ||||||
|  |     Event, | ||||||
|  |     Publishable, | ||||||
|  |     EventType, | ||||||
|  |     InitialStateEvent, | ||||||
|  |     SetControllableEvent, | ||||||
|  |     EntityBornEvent, | ||||||
|  | ) | ||||||
| from typing import Annotated, Optional | from typing import Annotated, Optional | ||||||
| from kennel.kennel import ( | from kennel.kennel import ( | ||||||
|     kennel, |     kennel, | ||||||
|  | @ -76,7 +83,7 @@ class WebSocketClient(Publishable): | ||||||
|         self.websocket = websocket |         self.websocket = websocket | ||||||
| 
 | 
 | ||||||
|     async def publish(self, event: Event): |     async def publish(self, event: Event): | ||||||
|         await self.websocket.send_json(event.dict()) |         await self.websocket.send_json([event.event_type, event.data]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.websocket("/ws") | @app.websocket("/ws") | ||||||
|  | @ -85,19 +92,16 @@ async def websocket_endpoint( | ||||||
|     session: Annotated[str, Depends(get_cookie_or_token)], |     session: Annotated[str, Depends(get_cookie_or_token)], | ||||||
| ): | ): | ||||||
|     await websocket.accept() |     await websocket.accept() | ||||||
|  |     client = WebSocketClient(websocket) | ||||||
|     logger.info(f"Websocket connection established for session {session}") |     logger.info(f"Websocket connection established for session {session}") | ||||||
| 
 | 
 | ||||||
|     await websocket.send_json( |     initial_state = InitialStateEvent( | ||||||
|         { |         config.WORLD_WIDTH, config.WORLD_HEIGHT, kennel.entity_manager.to_dict() | ||||||
|             "event_type": EventType.INITIAL_STATE, |  | ||||||
|             "data": { |  | ||||||
|                 "world": {"width": config.WORLD_WIDTH, "height": config.WORLD_HEIGHT}, |  | ||||||
|                 "entities": kennel.entity_manager.to_dict(), |  | ||||||
|             }, |  | ||||||
|         } |  | ||||||
|     ) |     ) | ||||||
|  |     await client.publish(initial_state) | ||||||
| 
 | 
 | ||||||
|     session_entities = create_session_controllable_entities(session) |     session_entities = create_session_controllable_entities(session) | ||||||
|  |     logger.info(f"Creating {len(session_entities)} entities for session {session}") | ||||||
|     try: |     try: | ||||||
|         network_system = system_manager.get_system(SystemType.NETWORK) |         network_system = system_manager.get_system(SystemType.NETWORK) | ||||||
|         if network_system is None: |         if network_system is None: | ||||||
|  | @ -106,14 +110,26 @@ async def websocket_endpoint( | ||||||
|         for entity in session_entities: |         for entity in session_entities: | ||||||
|             logger.info(f"Adding entity {entity.id} for session {session}") |             logger.info(f"Adding entity {entity.id} for session {session}") | ||||||
|             entity_manager.add_entity(entity) |             entity_manager.add_entity(entity) | ||||||
|             network_system.add_event(Event(EventType.ENTITY_BORN, {"id": entity.id})) |             network_system.add_event(EntityBornEvent(entity)) | ||||||
|  | 
 | ||||||
|  |             set_controllable_event = SetControllableEvent(entity.id, session) | ||||||
|  |             await client.publish(set_controllable_event) | ||||||
| 
 | 
 | ||||||
|         network_system.add_client(session, WebSocketClient(websocket)) |         network_system.add_client(session, WebSocketClient(websocket)) | ||||||
|         while True: |         while True: | ||||||
|             message = await websocket.receive_json() |             message = await websocket.receive_json() | ||||||
|             network_system.client_event(session, Event.from_dict(message)) |             if type(message) is not list: | ||||||
|  |                 message = [message] | ||||||
|  |             events = [Event.from_dict(event) for event in message] | ||||||
|  |             if not all([event is not None for event in events]): | ||||||
|  |                 logger.info(f"Invalid events in: {message}"[:100]) | ||||||
|  |                 continue | ||||||
|  |             for event in events: | ||||||
|  |                 network_system.client_event(session, event) | ||||||
|  |     except WebSocketDisconnect as e: | ||||||
|  |         logger.info(f"Websocket connection closed by client: {session}") | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         logger.error(f"WebSocket exception {e}") |         logger.error("Exception occurred", exc_info=e) | ||||||
|     finally: |     finally: | ||||||
|         logger.info("Websocket connection closed") |         logger.info("Websocket connection closed") | ||||||
|         for entity in session_entities: |         for entity in session_entities: | ||||||
|  |  | ||||||
|  | @ -0,0 +1,8 @@ | ||||||
|  | export enum ComponentType { | ||||||
|  |   POSITION = "POSITION", | ||||||
|  |   RENDERABLE = "RENDERABLE", | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface Component { | ||||||
|  |   name: string; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | import type { Component } from "./component"; | ||||||
|  | 
 | ||||||
|  | export enum EntityType { | ||||||
|  |   LASER = "LASER", | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface Entity { | ||||||
|  |   entity_type: EntityType; | ||||||
|  |   id: string; | ||||||
|  |   components: Record<string, Component>; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | 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 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; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | @ -1,18 +1,51 @@ | ||||||
| import $ from "jquery"; | import $ from "jquery"; | ||||||
|  | import { Vec2 } from "./vector"; | ||||||
|  | import { EventType, type SetControllableEvent, type Event } from "./event"; | ||||||
|  | import { MouseController } from "./mouse_controller"; | ||||||
| 
 | 
 | ||||||
| $(document).ready(async () => { | $(document).ready(async () => { | ||||||
|   await fetch("/assign", { |   const session_id = await fetch("/assign", { | ||||||
|     credentials: "include", |     credentials: "include", | ||||||
|  |   }) | ||||||
|  |     .then((res) => res.json()) | ||||||
|  |     .then(({ session }) => session); | ||||||
|  | 
 | ||||||
|  |   const controllable_entities = new Set<string>(); | ||||||
|  |   const control_callback = (movement: Vec2) => { | ||||||
|  |     for (const id of controllable_entities) { | ||||||
|  |       const message = JSON.stringify({ | ||||||
|  |         event_type: EventType.ENTITY_POSITION_UPDATE, | ||||||
|  |         data: { id, position: movement }, | ||||||
|  |       }); | ||||||
|  |       ws.send(message); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   const mouse_controller = new MouseController(control_callback); | ||||||
|  |   $(document).on("mousemove", (event) => { | ||||||
|  |     mouse_controller.move(event.clientX, event.clientY); | ||||||
|   }); |   }); | ||||||
|  |   mouse_controller.start(); | ||||||
| 
 | 
 | ||||||
|   const ws = new WebSocket("/ws"); |   const ws = new WebSocket("/ws"); | ||||||
|   ws.onopen = () => { |   await new Promise<void>((resolve) => { | ||||||
|     console.log("connected"); |     ws.onopen = () => { | ||||||
|   }; |       console.log("connected"); | ||||||
|  |       resolve(); | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|   ws.onmessage = ({ data }) => { |   ws.onmessage = ({ data }) => { | ||||||
|     console.log(JSON.parse(data)); |     const [event_type, event_data] = JSON.parse(data); | ||||||
|  |     const message = { event_type, data: event_data } as Event; | ||||||
|  | 
 | ||||||
|  |     console.log("Received message", message); | ||||||
|  |     if (message.event_type === EventType.SET_CONTROLLABLE) { | ||||||
|  |       const event = message as SetControllableEvent; | ||||||
|  |       if (event.data.client_id === session_id) { | ||||||
|  |         controllable_entities.add(event.data.id); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   }; |   }; | ||||||
|   ws.onclose = (e) => { |   ws.onclose = () => { | ||||||
|     console.log("disconnected", e); |     controllable_entities.clear(); | ||||||
|   }; |   }; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,54 @@ | ||||||
|  | import { Vec2 } from "./vector"; | ||||||
|  | 
 | ||||||
|  | export class MouseController { | ||||||
|  |   private readonly debounce_ms = 400; | ||||||
|  |   private readonly movement_threshold = 40; | ||||||
|  |   private last_event_time = Date.now(); | ||||||
|  |   private movement_queue: Vec2[] = []; | ||||||
|  |   private interval_id: number | null = null; | ||||||
|  | 
 | ||||||
|  |   constructor(private readonly callback: (new_movement: Vec2) => void) {} | ||||||
|  | 
 | ||||||
|  |   public start() { | ||||||
|  |     if (this.interval_id !== null) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.interval_id = setInterval(() => { | ||||||
|  |       this.publish_movement(); | ||||||
|  |     }, this.debounce_ms); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public stop() { | ||||||
|  |     if (this.interval_id === null) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     clearInterval(this.interval_id); | ||||||
|  |     this.interval_id = null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public move(x: number, y: number) { | ||||||
|  |     const new_movement = new Vec2(x, y); | ||||||
|  |     const last_movement = this.movement_queue.at(-1); | ||||||
|  |     this.movement_queue.push(new_movement); | ||||||
|  |     if ( | ||||||
|  |       typeof last_movement === "undefined" || | ||||||
|  |       new_movement.distance_to(last_movement) < this.movement_threshold | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.publish_movement(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private publish_movement() { | ||||||
|  |     if ( | ||||||
|  |       Date.now() - this.last_event_time < this.debounce_ms || | ||||||
|  |       this.movement_queue.length === 0 | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.last_event_time = Date.now(); | ||||||
|  |     this.callback(this.movement_queue.at(-1)!); | ||||||
|  |     this.movement_queue = []; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | export class Vec2 { | ||||||
|  |   constructor( | ||||||
|  |     private readonly x: number, | ||||||
|  |     private readonly y: number, | ||||||
|  |   ) {} | ||||||
|  | 
 | ||||||
|  |   public distance_to(that: Vec2): number { | ||||||
|  |     return Math.sqrt((this.x - that.x) ** 2 + (this.y - that.y) ** 2); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| { | { | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "target": "ES2020", |     "target": "ES2022", | ||||||
|     "useDefineForClassFields": true, |     "useDefineForClassFields": true, | ||||||
|     "module": "ESNext", |     "module": "ESNext", | ||||||
|     "lib": ["ES2020", "DOM", "DOM.Iterable"], |     "lib": ["ES2022", "DOM", "DOM.Iterable"], | ||||||
|     "skipLibCheck": true, |     "skipLibCheck": true, | ||||||
| 
 | 
 | ||||||
|     /* Bundler mode */ |     /* Bundler mode */ | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue