WIP: ECS / Network System #1
|
@ -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