woaj
continuous-integration/drone/pr Build is passing
Details
continuous-integration/drone/pr Build is passing
Details
This commit is contained in:
parent
e4e31978ba
commit
e47d451426
|
@ -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",
|
||||||
|
|
|
@ -18,6 +18,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,30 @@
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ComponentType,
|
||||||
|
RenderableComponent,
|
||||||
|
TrailingPositionComponent,
|
||||||
|
} from "./component";
|
||||||
|
|
||||||
|
export enum EntityType {
|
||||||
|
LASER = "LASER",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Entity {
|
||||||
|
entity_type: EntityType;
|
||||||
|
id: string;
|
||||||
|
components: Record<ComponentType, Component>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const create_laser = (base: Entity) => {
|
||||||
|
const trailing_position: TrailingPositionComponent = {
|
||||||
|
name: ComponentType.TRAILING_POSITION,
|
||||||
|
trails: [],
|
||||||
|
};
|
||||||
|
base.components[ComponentType.TRAILING_POSITION] = trailing_position;
|
||||||
|
|
||||||
|
const renderable: RenderableComponent = {
|
||||||
|
name: ComponentType.RENDERABLE,
|
||||||
|
};
|
||||||
|
base.components[ComponentType.RENDERABLE] = renderable;
|
||||||
|
return base;
|
||||||
|
};
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Entity } from "./entity";
|
||||||
export enum EventType {
|
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,135 @@
|
||||||
|
import {
|
||||||
|
ComponentType,
|
||||||
|
PositionComponent,
|
||||||
|
TrailingPositionComponent,
|
||||||
|
} from "./component";
|
||||||
|
import { create_laser, Entity, EntityType } from "./entity";
|
||||||
|
import {
|
||||||
|
EntityBornEvent,
|
||||||
|
EntityDeathEvent,
|
||||||
|
EntityPositionUpdateEvent,
|
||||||
|
EventPublisher,
|
||||||
|
EventQueue,
|
||||||
|
EventType,
|
||||||
|
InitialStateEvent,
|
||||||
|
SetControllableEvent,
|
||||||
|
} from "./events";
|
||||||
|
import { Game } from "./game";
|
||||||
|
import { InputSystem } from "./input";
|
||||||
|
import { RenderSystem } from "./render";
|
||||||
|
import { System, SystemType } from "./system";
|
||||||
|
|
||||||
|
export class NetworkSystem extends System {
|
||||||
|
constructor(
|
||||||
|
private readonly event_queue: EventQueue,
|
||||||
|
private readonly event_publisher: EventPublisher,
|
||||||
|
) {
|
||||||
|
super(SystemType.NETWORK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(_dt: number, game: Game) {
|
||||||
|
const events = this.event_queue.peek();
|
||||||
|
for (const event of events) {
|
||||||
|
switch (event.event_type) {
|
||||||
|
case EventType.INITIAL_STATE:
|
||||||
|
this.process_initial_state_event(event as InitialStateEvent, game);
|
||||||
|
break;
|
||||||
|
case EventType.SET_CONTROLLABLE:
|
||||||
|
this.process_set_controllable_event(
|
||||||
|
event as SetControllableEvent,
|
||||||
|
game,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EventType.ENTITY_BORN:
|
||||||
|
this.process_entity_born_event(event as EntityBornEvent, game);
|
||||||
|
break;
|
||||||
|
case EventType.ENTITY_POSITION_UPDATE:
|
||||||
|
this.process_entity_position_update_event(
|
||||||
|
event as EntityPositionUpdateEvent,
|
||||||
|
game,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EventType.ENTITY_DEATH:
|
||||||
|
this.process_entity_death_event(event as EntityDeathEvent, game);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.event_queue.clear();
|
||||||
|
this.event_publisher.publish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private process_initial_state_event(event: InitialStateEvent, game: Game) {
|
||||||
|
console.log("received initial state", event);
|
||||||
|
const { world, entities } = event.data;
|
||||||
|
const render_system = game.get_system<RenderSystem>(SystemType.RENDER);
|
||||||
|
if (!render_system) {
|
||||||
|
console.error("render system not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
render_system.set_world_dimensions(world.width, world.height);
|
||||||
|
Object.values(entities).forEach((entity) =>
|
||||||
|
game.put_entity(this.process_new_entity(entity)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private process_entity_position_update_event(
|
||||||
|
event: EntityPositionUpdateEvent,
|
||||||
|
game: Game,
|
||||||
|
) {
|
||||||
|
console.log("received entity position update", event);
|
||||||
|
const { position, id } = event.data;
|
||||||
|
const entity = game.get_entity(id);
|
||||||
|
if (typeof entity === "undefined") return;
|
||||||
|
|
||||||
|
const position_component = entity.components[
|
||||||
|
ComponentType.POSITION
|
||||||
|
] as PositionComponent;
|
||||||
|
position_component.x = position.x;
|
||||||
|
position_component.y = position.y;
|
||||||
|
|
||||||
|
if (ComponentType.TRAILING_POSITION in entity.components) {
|
||||||
|
const trailing_position = entity.components[
|
||||||
|
ComponentType.TRAILING_POSITION
|
||||||
|
] as TrailingPositionComponent;
|
||||||
|
trailing_position.trails.push({
|
||||||
|
x: position_component.x,
|
||||||
|
y: position_component.y,
|
||||||
|
time: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private process_entity_born_event(event: EntityBornEvent, game: Game) {
|
||||||
|
console.log("received a new entity", event);
|
||||||
|
const { entity } = event.data;
|
||||||
|
game.put_entity(this.process_new_entity(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
private process_new_entity(entity: Entity): Entity {
|
||||||
|
if (entity.entity_type === EntityType.LASER) {
|
||||||
|
return create_laser(entity);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private process_entity_death_event(event: EntityDeathEvent, game: Game) {
|
||||||
|
console.log("an entity died D:", event);
|
||||||
|
const { id } = event.data;
|
||||||
|
game.remove_entity(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private process_set_controllable_event(
|
||||||
|
event: SetControllableEvent,
|
||||||
|
game: Game,
|
||||||
|
) {
|
||||||
|
console.log("got a controllable event", event);
|
||||||
|
if (event.data.client_id !== game.client_id) {
|
||||||
|
console.warn("got controllable event for client that is not us");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input_system = game.get_system<InputSystem>(SystemType.INPUT)!;
|
||||||
|
input_system.add_controllable_entity(event.data.id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { ComponentType, TrailingPositionComponent } from "./component";
|
||||||
|
import { Game } from "./game";
|
||||||
|
import { System, SystemType } from "./system";
|
||||||
|
import { drawLaserPen } from "laser-pen";
|
||||||
|
|
||||||
|
export class RenderSystem extends System {
|
||||||
|
constructor(private readonly canvas: HTMLCanvasElement) {
|
||||||
|
super(SystemType.RENDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public set_world_dimensions(width: number, height: number) {
|
||||||
|
this.canvas.width = width;
|
||||||
|
this.canvas.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(_dt: number, game: Game) {
|
||||||
|
const ctx = this.canvas.getContext("2d");
|
||||||
|
if (ctx === null) return;
|
||||||
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
game.for_each_entity_with_component(ComponentType.RENDERABLE, (entity) => {
|
||||||
|
if (ComponentType.TRAILING_POSITION in entity.components) {
|
||||||
|
const trailing_position = entity.components[
|
||||||
|
ComponentType.TRAILING_POSITION
|
||||||
|
] as TrailingPositionComponent;
|
||||||
|
if (trailing_position.trails.length < 3) return;
|
||||||
|
drawLaserPen(ctx, trailing_position.trails);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Game } from "./game";
|
||||||
|
|
||||||
|
export enum SystemType {
|
||||||
|
INPUT = "INPUT",
|
||||||
|
NETWORK = "NETWORK",
|
||||||
|
RENDER = "RENDER",
|
||||||
|
TRAILING_POSITION = "TRAILING_POSITION",
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class System {
|
||||||
|
constructor(public readonly system_type: SystemType) {}
|
||||||
|
|
||||||
|
abstract update(dt: number, game: Game): void;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { ComponentType, TrailingPositionComponent } from "./component";
|
||||||
|
import { Game } from "./game";
|
||||||
|
import { System, SystemType } from "./system";
|
||||||
|
|
||||||
|
export class TrailingPositionSystem extends System {
|
||||||
|
constructor(
|
||||||
|
private readonly point_filter: (
|
||||||
|
trail_point: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
time: number;
|
||||||
|
}[],
|
||||||
|
) => {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
time: number;
|
||||||
|
}[],
|
||||||
|
) {
|
||||||
|
super(SystemType.TRAILING_POSITION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(_dt: number, game: Game) {
|
||||||
|
game.for_each_entity_with_component(
|
||||||
|
ComponentType.TRAILING_POSITION,
|
||||||
|
(entity) => {
|
||||||
|
const trailing_position = entity.components[
|
||||||
|
ComponentType.TRAILING_POSITION
|
||||||
|
] as TrailingPositionComponent;
|
||||||
|
trailing_position.trails = this.point_filter(trailing_position.trails);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
import type { Component } from "./component";
|
|
||||||
|
|
||||||
export enum EntityType {
|
|
||||||
LASER = "LASER",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Entity {
|
|
||||||
entity_type: EntityType;
|
|
||||||
id: string;
|
|
||||||
components: Record<string, Component>;
|
|
||||||
}
|
|
|
@ -1,95 +1,16 @@
|
||||||
import $ from "jquery";
|
import $ 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)!;
|
||||||
|
const input_system = new InputSystem(publisher, gamecanvas);
|
||||||
|
|
||||||
|
setDelay(1_000);
|
||||||
|
const render_system = new RenderSystem(gamecanvas as HTMLCanvasElement);
|
||||||
|
|
||||||
|
const trailing_position = new TrailingPositionSystem(drainPoints);
|
||||||
|
|
||||||
|
const systems = [
|
||||||
|
network_system,
|
||||||
|
trailing_position,
|
||||||
|
input_system,
|
||||||
|
render_system,
|
||||||
|
];
|
||||||
|
|
||||||
|
const game = new Game(client_id, systems);
|
||||||
|
ws.onclose = () => game.stop();
|
||||||
|
|
||||||
|
game.start();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
export class Vec2 {
|
|
||||||
constructor(
|
|
||||||
public readonly x: number,
|
|
||||||
public readonly y: number,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public distance_to(that: Vec2): number {
|
|
||||||
return Math.sqrt((this.x - that.x) ** 2 + (this.y - that.y) ** 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public copy(): Vec2 {
|
|
||||||
return new Vec2(this.x, this.y);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue