aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth@simponic.xyz>2024-09-07 20:20:07 -0700
committerElizabeth Hunt <elizabeth@simponic.xyz>2024-09-12 17:23:30 -0700
commit8ec7f5368232d59f344e1067e1bad5e48dbcb7ae (patch)
tree1ad2df4dc00773f2307d1525cc80ac7410ea8fba
parente4e31978bae7e45be57b376415a4b925ac8cbc03 (diff)
downloadkennel.hatecomputers.club-8ec7f5368232d59f344e1067e1bad5e48dbcb7ae.tar.gz
kennel.hatecomputers.club-8ec7f5368232d59f344e1067e1bad5e48dbcb7ae.zip
get "cats" up there
-rw-r--r--kennel/config.py2
-rw-r--r--kennel/engine/components/component.py1
-rw-r--r--kennel/engine/components/markov_transition_state.py24
-rw-r--r--kennel/engine/entities/cat.py14
-rw-r--r--kennel/engine/entities/entity.py1
-rw-r--r--kennel/engine/systems/markov_transition_state_system.py13
-rw-r--r--kennel/engine/systems/system.py3
-rw-r--r--kennel/kennel.py81
-rw-r--r--kennel/kennelcats.py6
-rw-r--r--kennel/main.py3
-rw-r--r--static/index.html9
-rw-r--r--static/package-lock.json9
-rw-r--r--static/package.json6
-rw-r--r--static/src/component.ts8
-rw-r--r--static/src/engine/component.ts24
-rw-r--r--static/src/engine/debounce_publisher.ts (renamed from static/src/mouse_controller.ts)25
-rw-r--r--static/src/engine/entity.ts45
-rw-r--r--static/src/engine/events.ts (renamed from static/src/network.ts)17
-rw-r--r--static/src/engine/game.ts112
-rw-r--r--static/src/engine/input.ts49
-rw-r--r--static/src/engine/network.ts138
-rw-r--r--static/src/engine/render.ts46
-rw-r--r--static/src/engine/system.ts14
-rw-r--r--static/src/engine/trailing_position.ts33
-rw-r--r--static/src/entity.ts11
-rw-r--r--static/src/main.ts116
-rw-r--r--static/src/vector.ts14
27 files changed, 674 insertions, 150 deletions
diff --git a/kennel/config.py b/kennel/config.py
index a731c84..8a09500 100644
--- a/kennel/config.py
+++ b/kennel/config.py
@@ -17,5 +17,7 @@ class Config:
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()
diff --git a/kennel/engine/components/component.py b/kennel/engine/components/component.py
index 9f56407..90506b9 100644
--- a/kennel/engine/components/component.py
+++ b/kennel/engine/components/component.py
@@ -5,6 +5,7 @@ from enum import Enum
class ComponentType(str, Enum):
POSITION = "POSITION"
CONTROLLABLE = "CONTROLLABLE"
+ MARKOV = "MARKOV"
class Component:
diff --git a/kennel/engine/components/markov_transition_state.py b/kennel/engine/components/markov_transition_state.py
new file mode 100644
index 0000000..cec8223
--- /dev/null
+++ b/kennel/engine/components/markov_transition_state.py
@@ -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)}
diff --git a/kennel/engine/entities/cat.py b/kennel/engine/entities/cat.py
index 02fdb33..edcde37 100644
--- a/kennel/engine/entities/cat.py
+++ b/kennel/engine/entities/cat.py
@@ -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
-# state_stochastic_matrix = [
+# state_stochastic_matrix = [ [1, 0]
# # IDLE
# [0.5, 0.1, 0.1, 0.1, 0.1, 0.05, 0.05, 0],
# # FROLICKING
diff --git a/kennel/engine/entities/entity.py b/kennel/engine/entities/entity.py
index d40ab19..30e325a 100644
--- a/kennel/engine/entities/entity.py
+++ b/kennel/engine/entities/entity.py
@@ -6,6 +6,7 @@ from kennel.engine.components.component import Component, ComponentType
class EntityType(str, Enum):
LASER = "LASER"
+ CAT = "CAT"
class Entity:
diff --git a/kennel/engine/systems/markov_transition_state_system.py b/kennel/engine/systems/markov_transition_state_system.py
new file mode 100644
index 0000000..ad3de65
--- /dev/null
+++ b/kennel/engine/systems/markov_transition_state_system.py
@@ -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
diff --git a/kennel/engine/systems/system.py b/kennel/engine/systems/system.py
index e3ddb27..8c577a6 100644
--- a/kennel/engine/systems/system.py
+++ b/kennel/engine/systems/system.py
@@ -7,6 +7,7 @@ from kennel.engine.entities.entity import EntityManager
class SystemType(str, Enum):
NETWORK = "NETWORK"
WORLD = "WORLD"
+ MARKOV = "MARKOV"
class System:
@@ -14,7 +15,7 @@ class System:
self.system_type = system_type
@abstractmethod
- async def update(self, entity_manager: EntityManager, delta_time: float):
+ async def update(self, entity_manager: EntityManager, delta_time: float) -> None:
pass
diff --git a/kennel/kennel.py b/kennel/kennel.py
index b4d7b7b..7123b7f 100644
--- a/kennel/kennel.py
+++ b/kennel/kennel.py
@@ -1,18 +1,27 @@
+import asyncio
import uuid
+import time
from typing import List, Optional
from kennel.config import config
from kennel.engine.components.component import ComponentType
from kennel.engine.entities.entity import Entity, EntityManager
from kennel.engine.entities.laser import Laser
+from kennel.engine.entities.cat import Cat
from kennel.engine.game import Game
+from kennel.engine.systems.markov_transition_state_system import (
+ MarkovTransitionStateSystem,
+)
from kennel.engine.systems.network import (
EntityPositionUpdateEvent,
+ EntityBornEvent,
+ EntityDeathEvent,
Event,
EventType,
NetworkSystem,
UpstreamEventProcessor,
)
+from kennel.kennelcats import KennelCatService, KennelCat
from kennel.engine.systems.system import SystemManager
from kennel.engine.systems.world import WorldSystem
@@ -58,12 +67,76 @@ class KennelEventProcessor(UpstreamEventProcessor):
return event
-system_manager.add_system(WorldSystem(config.WORLD_WIDTH, config.WORLD_HEIGHT))
-system_manager.add_system(
- NetworkSystem(KennelEventProcessor()),
-)
+class KennelCatsManager:
+ kennel_cat_service: KennelCatService
+ 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_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]:
diff --git a/kennel/kennelcats.py b/kennel/kennelcats.py
index 970e44f..2cb2f29 100644
--- a/kennel/kennelcats.py
+++ b/kennel/kennelcats.py
@@ -37,10 +37,10 @@ class KennelCat:
class KennelCatService:
- def __init__(self, endpoint: str):
- self.endpoint = endpoint
+ def __init__(self, hc_endpoint: str):
+ self.hc_endpoint = hc_endpoint
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()
return [KennelCat.from_dict(cat) for cat in response.json()]
diff --git a/kennel/main.py b/kennel/main.py
index 22d3d39..49d62d4 100644
--- a/kennel/main.py
+++ b/kennel/main.py
@@ -30,6 +30,7 @@ from kennel.kennel import (
create_session_controllable_entities,
entity_manager,
kennel,
+ kennel_cats_manager,
system_manager,
)
@@ -43,12 +44,14 @@ loop = asyncio.get_event_loop()
async def startup_event():
logger.info("Starting Kennel...")
loop.create_task(kennel.run())
+ loop.create_task(kennel_cats_manager.start())
@app.on_event("shutdown")
async def shutdown_event():
logger.info("Stopping Kennel...")
kennel.stop()
+ kennel_cats_manager.stop()
loop.stop()
logger.info("Kennel stopped")
diff --git a/static/index.html b/static/index.html
index 3770eaa..ca9ed9c 100644
--- a/static/index.html
+++ b/static/index.html
@@ -9,7 +9,14 @@
<title>the kennel.</title>
</head>
<body>
- <div id="app"></div>
+ <noscript>
+ <div style="text-align: center">
+ <h1>yeah, unfortunately you need javascript ;3</h1>
+ </div>
+ </noscript>
+ <div id="app">
+ <canvas id="gamecanvas"></canvas>
+ </div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
diff --git a/static/package-lock.json b/static/package-lock.json
index e85f7a7..b62cb6d 100644
--- a/static/package-lock.json
+++ b/static/package-lock.json
@@ -8,7 +8,8 @@
"name": "kennel",
"version": "0.1.0",
"dependencies": {
- "jquery": "^3.7.1"
+ "jquery": "^3.7.1",
+ "laser-pen": "^1.0.1"
},
"devDependencies": {
"@rollup/plugin-inject": "^5.0.5",
@@ -1230,6 +1231,12 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
},
+ "node_modules/laser-pen": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/laser-pen/-/laser-pen-1.0.1.tgz",
+ "integrity": "sha512-+K3CQK5ryLDDjX0pEdSIRXh89F6KSpm225DEymWMheg2/umhUO+na/4l8MGs2gee7VO2Mx3kyG288WGrofasoA==",
+ "license": "MIT"
+ },
"node_modules/magic-string": {
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
diff --git a/static/package.json b/static/package.json
index 1c698e0..b27d3de 100644
--- a/static/package.json
+++ b/static/package.json
@@ -4,10 +4,9 @@
"version": "0.1.0",
"type": "module",
"scripts": {
- "dev": "vite",
"build": "tsc && vite build",
"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": {
"@rollup/plugin-inject": "^5.0.5",
@@ -18,6 +17,7 @@
"vite-plugin-dynamic-base": "^1.1.0"
},
"dependencies": {
- "jquery": "^3.7.1"
+ "jquery": "^3.7.1",
+ "laser-pen": "^1.0.1"
}
}
diff --git a/static/src/component.ts b/static/src/component.ts
deleted file mode 100644
index d338cad..0000000
--- a/static/src/component.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export enum ComponentType {
- POSITION = "POSITION",
- RENDERABLE = "RENDERABLE",
-}
-
-export interface Component {
- name: string;
-}
diff --git a/static/src/engine/component.ts b/static/src/engine/component.ts
new file mode 100644
index 0000000..9607fec
--- /dev/null
+++ b/static/src/engine/component.ts
@@ -0,0 +1,24 @@
+export enum ComponentType {
+ POSITION = "POSITION",
+ RENDERABLE = "RENDERABLE",
+ TRAILING_POSITION = "TRAILING_POSITION",
+}
+
+export interface Component {
+ name: ComponentType;
+}
+
+export interface PositionComponent extends Component {
+ name: ComponentType.POSITION;
+ x: number;
+ y: number;
+}
+
+export interface TrailingPositionComponent extends Component {
+ name: ComponentType.TRAILING_POSITION;
+ trails: Array<{ x: number; y: number; time: number }>;
+}
+
+export interface RenderableComponent extends Component {
+ name: ComponentType.RENDERABLE;
+}
diff --git a/static/src/mouse_controller.ts b/static/src/engine/debounce_publisher.ts
index bd8d51a..8ee4bb0 100644
--- a/static/src/mouse_controller.ts
+++ b/static/src/engine/debounce_publisher.ts
@@ -1,12 +1,11 @@
-import { Vec2 } from "./vector";
-export class MouseController {
+export class DebouncePublisher<T> {
private last_event_time = Date.now();
- private last_movement: Vec2 | undefined;
+ private unpublished_data: T | undefined;
private interval_id: number | undefined;
constructor(
- private readonly publisher: (new_movement: Vec2) => void | Promise<void>,
- private readonly debounce_ms = 200,
+ private readonly publisher: (data: T) => void | Promise<void>,
+ private readonly debounce_ms = 100,
) {}
public start() {
@@ -14,7 +13,7 @@ export class MouseController {
return;
}
this.interval_id = setInterval(
- () => this.publish_movement(),
+ () => this.debounce_publish(),
this.debounce_ms,
);
}
@@ -27,21 +26,21 @@ export class MouseController {
delete this.interval_id;
}
- public move(x: number, y: number) {
- this.last_movement = new Vec2(x, y);
- this.publish_movement();
+ public update(data: T) {
+ this.unpublished_data = data;
+ this.debounce_publish();
}
- private publish_movement() {
+ private debounce_publish() {
if (
Date.now() - this.last_event_time < this.debounce_ms ||
- typeof this.last_movement === "undefined"
+ typeof this.unpublished_data === "undefined"
) {
return;
}
this.last_event_time = Date.now();
- this.publisher(this.last_movement.copy());
- delete this.last_movement;
+ this.publisher(this.unpublished_data);
+ this.unpublished_data = undefined;
}
}
diff --git a/static/src/engine/entity.ts b/static/src/engine/entity.ts
new file mode 100644
index 0000000..2c8d38e
--- /dev/null
+++ b/static/src/engine/entity.ts
@@ -0,0 +1,45 @@
+import {
+ Component,
+ ComponentType,
+ PositionComponent,
+ RenderableComponent,
+ TrailingPositionComponent,
+} from "./component";
+
+export enum EntityType {
+ LASER = "LASER",
+ CAT = "CAT",
+}
+
+export interface Entity {
+ entity_type: EntityType;
+ id: string;
+ components: Record<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;
+};
diff --git a/static/src/network.ts b/static/src/engine/events.ts
index fac96df..60632f5 100644
--- a/static/src/network.ts
+++ b/static/src/engine/events.ts
@@ -1,3 +1,4 @@
+import { Entity } from "./entity";
export enum EventType {
INITIAL_STATE = "INITIAL_STATE",
SET_CONTROLLABLE = "SET_CONTROLLABLE",
@@ -26,7 +27,7 @@ export interface InitialStateEvent extends Event {
event_type: EventType.INITIAL_STATE;
data: {
world: { width: number; height: number };
- entities: any[];
+ entities: Record<string, Entity>;
};
}
@@ -38,6 +39,20 @@ export interface SetControllableEvent extends Event {
};
}
+export interface EntityBornEvent extends Event {
+ event_type: EventType.ENTITY_BORN;
+ data: {
+ entity: Entity;
+ };
+}
+
+export interface EntityDeathEvent extends Event {
+ event_type: EventType.ENTITY_DEATH;
+ data: {
+ id: string;
+ };
+}
+
export interface EventQueue {
peek(): Event[];
clear(): void;
diff --git a/static/src/engine/game.ts b/static/src/engine/game.ts
new file mode 100644
index 0000000..64f63b5
--- /dev/null
+++ b/static/src/engine/game.ts
@@ -0,0 +1,112 @@
+import { System, SystemType } from "./system";
+import { Entity } from "./entity";
+import { ComponentType } from "./component";
+
+export class Game {
+ private running: boolean;
+ private last_update: number;
+
+ private readonly entities: Map<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;
+ }
+}
diff --git a/static/src/engine/input.ts b/static/src/engine/input.ts
new file mode 100644
index 0000000..c1728e9
--- /dev/null
+++ b/static/src/engine/input.ts
@@ -0,0 +1,49 @@
+import { DebouncePublisher } from "./debounce_publisher";
+import { EntityPositionUpdateEvent, EventPublisher, EventType } from "./events";
+import { Game } from "./game";
+import { System, SystemType } from "./system";
+
+export class InputSystem extends System {
+ private readonly controllable_entities: Set<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) {}
+}
diff --git a/static/src/engine/network.ts b/static/src/engine/network.ts
new file mode 100644
index 0000000..850da65
--- /dev/null
+++ b/static/src/engine/network.ts
@@ -0,0 +1,138 @@
+import {
+ ComponentType,
+ PositionComponent,
+ TrailingPositionComponent,
+} from "./component";
+import { create_cat, create_laser, Entity, EntityType } from "./entity";
+import {
+ EntityBornEvent,
+ EntityDeathEvent,
+ EntityPositionUpdateEvent,
+ EventPublisher,
+ EventQueue,
+ EventType,
+ InitialStateEvent,
+ SetControllableEvent,
+} from "./events";
+import { Game } from "./game";
+import { InputSystem } from "./input";
+import { RenderSystem } from "./render";
+import { System, SystemType } from "./system";
+
+export class NetworkSystem extends System {
+ constructor(
+ private readonly event_queue: EventQueue,
+ private readonly event_publisher: EventPublisher,
+ ) {
+ super(SystemType.NETWORK);
+ }
+
+ public update(_dt: number, game: Game) {
+ const events = this.event_queue.peek();
+ for (const event of events) {
+ switch (event.event_type) {
+ case EventType.INITIAL_STATE:
+ this.process_initial_state_event(event as InitialStateEvent, game);
+ break;
+ case EventType.SET_CONTROLLABLE:
+ this.process_set_controllable_event(
+ event as SetControllableEvent,
+ game,
+ );
+ break;
+ case EventType.ENTITY_BORN:
+ this.process_entity_born_event(event as EntityBornEvent, game);
+ break;
+ case EventType.ENTITY_POSITION_UPDATE:
+ this.process_entity_position_update_event(
+ event as EntityPositionUpdateEvent,
+ game,
+ );
+ break;
+ case EventType.ENTITY_DEATH:
+ this.process_entity_death_event(event as EntityDeathEvent, game);
+ break;
+ }
+ }
+
+ this.event_queue.clear();
+ this.event_publisher.publish();
+ }
+
+ private process_initial_state_event(event: InitialStateEvent, game: Game) {
+ console.log("received initial state", event);
+ const { world, entities } = event.data;
+ const render_system = game.get_system<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);
+ }
+}
diff --git a/static/src/engine/render.ts b/static/src/engine/render.ts
new file mode 100644
index 0000000..8f0343a
--- /dev/null
+++ b/static/src/engine/render.ts
@@ -0,0 +1,46 @@
+import {
+ ComponentType,
+ PositionComponent,
+ TrailingPositionComponent,
+} from "./component";
+import { Game } from "./game";
+import { System, SystemType } from "./system";
+import { drawLaserPen } from "laser-pen";
+
+export class RenderSystem extends System {
+ constructor(private readonly canvas: HTMLCanvasElement) {
+ super(SystemType.RENDER);
+ }
+
+ public set_world_dimensions(width: number, height: number) {
+ this.canvas.width = width;
+ this.canvas.height = height;
+ }
+
+ public update(_dt: number, game: Game) {
+ const ctx = this.canvas.getContext("2d");
+ if (ctx === null) return;
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+
+ game.for_each_entity_with_component(ComponentType.RENDERABLE, (entity) => {
+ if (ComponentType.TRAILING_POSITION in entity.components) {
+ const trailing_position = entity.components[
+ ComponentType.TRAILING_POSITION
+ ] as TrailingPositionComponent;
+ if (trailing_position.trails.length < 3) return;
+ drawLaserPen(ctx, trailing_position.trails);
+ return;
+ }
+
+ if (ComponentType.POSITION in entity.components) {
+ const position = entity.components[
+ ComponentType.POSITION
+ ] as PositionComponent;
+ ctx.beginPath();
+ ctx.arc(position.x, position.y, 50, 0, 2 * Math.PI);
+ ctx.stroke();
+ return;
+ }
+ });
+ }
+}
diff --git a/static/src/engine/system.ts b/static/src/engine/system.ts
new file mode 100644
index 0000000..e19cf5a
--- /dev/null
+++ b/static/src/engine/system.ts
@@ -0,0 +1,14 @@
+import { Game } from "./game";
+
+export enum SystemType {
+ INPUT = "INPUT",
+ NETWORK = "NETWORK",
+ RENDER = "RENDER",
+ TRAILING_POSITION = "TRAILING_POSITION",
+}
+
+export abstract class System {
+ constructor(public readonly system_type: SystemType) {}
+
+ abstract update(dt: number, game: Game): void;
+}
diff --git a/static/src/engine/trailing_position.ts b/static/src/engine/trailing_position.ts
new file mode 100644
index 0000000..1ea22d3
--- /dev/null
+++ b/static/src/engine/trailing_position.ts
@@ -0,0 +1,33 @@
+import { ComponentType, TrailingPositionComponent } from "./component";
+import { Game } from "./game";
+import { System, SystemType } from "./system";
+
+export class TrailingPositionSystem extends System {
+ constructor(
+ private readonly point_filter: (
+ trail_point: {
+ x: number;
+ y: number;
+ time: number;
+ }[],
+ ) => {
+ x: number;
+ y: number;
+ time: number;
+ }[],
+ ) {
+ super(SystemType.TRAILING_POSITION);
+ }
+
+ public update(_dt: number, game: Game) {
+ game.for_each_entity_with_component(
+ ComponentType.TRAILING_POSITION,
+ (entity) => {
+ const trailing_position = entity.components[
+ ComponentType.TRAILING_POSITION
+ ] as TrailingPositionComponent;
+ trailing_position.trails = this.point_filter(trailing_position.trails);
+ },
+ );
+ }
+}
diff --git a/static/src/entity.ts b/static/src/entity.ts
deleted file mode 100644
index 811d05a..0000000
--- a/static/src/entity.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Component } from "./component";
-
-export enum EntityType {
- LASER = "LASER",
-}
-
-export interface Entity {
- entity_type: EntityType;
- id: string;
- components: Record<string, Component>;
-}
diff --git a/static/src/main.ts b/static/src/main.ts
index c3588de..c11e41e 100644
--- a/static/src/main.ts
+++ b/static/src/main.ts
@@ -1,95 +1,16 @@
import $ from "jquery";
-import { Vec2 } from "./vector";
-import { MouseController } from "./mouse_controller";
import {
- EntityPositionUpdateEvent,
EventPublisher,
EventQueue,
- EventType,
- SetControllableEvent,
WebsocketEventPublisher,
WebSocketEventQueue,
-} from "./network";
-
-class KennelClient {
- private running: boolean;
- private last_update: number;
-
- private controllable_entities: Set<string> = new Set();
- private mouse_controller: MouseController;
-
- constructor(
- private readonly client_id: string,
- private readonly event_queue: EventQueue,
- private readonly event_publisher: EventPublisher,
- ) {
- this.last_update = 0;
- this.running = false;
-
- this.mouse_controller = new MouseController((position: Vec2) =>
- this.on_mouse_move(position),
- );
- }
-
- public start() {
- this.running = true;
- this.last_update = performance.now();
- this.mouse_controller.start();
-
- const loop = (timestamp: number) => {
- if (!this.running) return;
- const dt = timestamp - this.last_update;
- this.propogate_state_after(dt);
- requestAnimationFrame(loop); // tail call recursion! /s
- };
- requestAnimationFrame(loop);
-
- $(document).on("mousemove", (event) => {
- this.mouse_controller.move(event.clientX, event.clientY);
- });
- }
-
- public close() {
- this.running = false;
- this.mouse_controller.stop();
- this.controllable_entities.clear();
- $(document).off("mousemove");
- }
-
- private propogate_state_after(dt: number) {
- const events = this.event_queue.peek();
- for (const event of events) {
- switch (event.event_type) {
- case EventType.SET_CONTROLLABLE:
- this.process_set_controllable_event(event as SetControllableEvent);
- break;
- }
- }
- if (events.length > 0) {
- console.log(events, dt);
- }
- this.event_queue.clear();
- this.event_publisher.publish();
- }
-
- private process_set_controllable_event(event: SetControllableEvent) {
- if (event.data.client_id !== this.client_id) {
- console.warn("got controllable event for client that is not us");
- return;
- }
- this.controllable_entities.add(event.data.id);
- }
-
- private on_mouse_move(position: Vec2) {
- for (const id of this.controllable_entities) {
- const event: EntityPositionUpdateEvent = {
- event_type: EventType.ENTITY_POSITION_UPDATE,
- data: { id, position: { x: position.x, y: position.y } },
- };
- this.event_publisher.add(event);
- }
- }
-}
+} from "./engine/events";
+import { Game } from "./engine/game";
+import { NetworkSystem } from "./engine/network";
+import { RenderSystem } from "./engine/render";
+import { InputSystem } from "./engine/input";
+import { TrailingPositionSystem } from "./engine/trailing_position";
+import { drainPoints, setDelay } from "laser-pen";
$(async () => {
const client_id = await fetch("/assign", {
@@ -105,8 +26,25 @@ $(async () => {
const queue: EventQueue = new WebSocketEventQueue(ws);
const publisher: EventPublisher = new WebsocketEventPublisher(ws);
- const kennel_client = new KennelClient(client_id, queue, publisher);
- ws.onclose = () => kennel_client.close();
+ const network_system = new NetworkSystem(queue, publisher);
+
+ const gamecanvas = $("#gamecanvas").get(0)! as HTMLCanvasElement;
+ const input_system = new InputSystem(publisher, gamecanvas);
+
+ setDelay(500);
+ const render_system = new RenderSystem(gamecanvas);
+
+ const trailing_position = new TrailingPositionSystem(drainPoints);
+
+ const systems = [
+ network_system,
+ trailing_position,
+ input_system,
+ render_system,
+ ];
+
+ const game = new Game(client_id, systems);
+ ws.onclose = () => game.stop();
- kennel_client.start();
+ game.start();
});
diff --git a/static/src/vector.ts b/static/src/vector.ts
deleted file mode 100644
index 29e62df..0000000
--- a/static/src/vector.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export class Vec2 {
- constructor(
- public readonly x: number,
- public readonly y: number,
- ) {}
-
- public distance_to(that: Vec2): number {
- return Math.sqrt((this.x - that.x) ** 2 + (this.y - that.y) ** 2);
- }
-
- public copy(): Vec2 {
- return new Vec2(this.x, this.y);
- }
-}