diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5530d01
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# D3
+
+## Usage
+
+### Client
+
+```sh
+cd client
+./main.ts
+```
+
+### Sever
+
+```sh
+cd server
+./main.ts
+```
\ No newline at end of file
diff --git a/client/main.ts b/client/main.ts
index 362af52..44c5720 100755
--- a/client/main.ts
+++ b/client/main.ts
@@ -55,7 +55,7 @@ class InputHandler {
const command = new Deno.Command("stdbuf", { args, stdin: "inherit", stdout: "piped" });
const output = await command.output();
const result = new TextDecoder().decode(output.stdout);
- console.log(`${String.fromCharCode(0x08)} `);
+ console.log(`${String.fromCharCode(0x08)} ${String.fromCharCode(0o33)}[A`);
return result[0];
}
diff --git a/common/utils.ts b/common/utils.ts
index 159d4e8..c9b4539 100644
--- a/common/utils.ts
+++ b/common/utils.ts
@@ -195,9 +195,9 @@ export class Vec2 {
return this.add(other.neg());
}
- public inside(corner_tl: Vec2, corner_br: Vec2) {
- if (!range(corner_tl.x(), corner_br.x()).includes(this.x())) return false;
- if (!range(corner_tl.y(), corner_br.y()).includes(this.y())) return false;
+ public inside(min: Vec2, max: Vec2) {
+ if (!range(min.x(), max.x()).includes(this.x())) return false;
+ if (!range(min.y(), max.y()).includes(this.y())) return false;
return true;
}
@@ -220,6 +220,10 @@ export class Vec2 {
public overlaps(other: Vec2) {
return this.distance(other) < 1;
}
+
+ public clone() {
+ return new Vec2(this.x(), this.y());
+ }
}
export function v2(x: number, y: number) {
@@ -260,7 +264,8 @@ export async function run(cmd: string, ...args: string[]) {
}
export type Prototyped
= { constructor: { prototype: P } };
-export type Constructible = new (...args: unknown[]) => I;
+// deno-lint-ignore no-explicit-any
+export type Constructible = new (...args: any[]) => I;
export class ClassMap {
private inner;
@@ -280,6 +285,10 @@ export class ClassMap {
assertInstanceOf(value, class_);
return value;
}
+
+ has>(class_: C) {
+ return this.inner.has(class_.prototype);
+ }
}
Deno.test("test_classmap", () => {
@@ -320,3 +329,83 @@ Deno.test("test_classmap", () => {
export function maybe(item: T) {
return item as T | undefined;
}
+
+export type Awaitable = T | Promise;
+
+export function first(iter: Iterable) {
+ for (const item of iter) return item;
+ return null;
+}
+
+export function take_n(iter: Iterable, n: number) {
+ const result = [] as T[];
+ for (const item of iter) {
+ if (result.length >= n) break;
+ result.push(item);
+ }
+ return result;
+}
+
+export function* line(from: Vec2, to: Vec2) {
+ let current = from.clone();
+ while (current.neq(to)) {
+ yield current;
+ if (current.x() - to.x() > 0.5) current = current.sub(v2(1, 0));
+ if (current.x() - to.x() < -0.5) current = current.add(v2(1, 0));
+ if (current.y() - to.y() > 0.5) current = current.sub(v2(0, 1));
+ if (current.y() - to.y() < -0.5) current = current.add(v2(0, 1));
+ }
+}
+
+export function* spiral(origin: Vec2) {
+ yield origin;
+ for (const i of range(0, 999)) {
+ const size = i * 2 + 1;
+ const corner_A = origin.add(v2(-size / 2, -size / 2));
+ const corner_B = origin.add(v2(size / 2, -size / 2));
+ const corner_C = origin.add(v2(size / 2, size / 2));
+ const corner_D = origin.add(v2(-size / 2, size / 2));
+ yield* line(corner_A, corner_B);
+ yield* line(corner_B, corner_C);
+ yield* line(corner_C, corner_D);
+ yield* line(corner_D, corner_A);
+ }
+}
+
+// Deno.test("test_spiral", () => {
+// // 2 | 10 11 12 13 14
+// // 1 | 25 2 3 4 15
+// // 0 | 24 9 1 5 16
+// // -1 | 23 8 7 6 17
+// // -2 | 22 21 20 19 18
+// // ---+---------------
+// // -2 -1 0 1 2
+// const result = [...take_n(spiral(v2(0, 0)), 25)];
+// const expects = [
+// v2(0, 0),
+// v2(1, 1),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// v2(),
+// ]
+// });
diff --git a/server/components/display.ts b/server/components/display.ts
new file mode 100644
index 0000000..8004381
--- /dev/null
+++ b/server/components/display.ts
@@ -0,0 +1,6 @@
+export class CompDisplay {
+ display;
+ constructor(display: string) {
+ this.display = display;
+ }
+}
diff --git a/server/components/world.ts b/server/components/world.ts
new file mode 100644
index 0000000..2c55f57
--- /dev/null
+++ b/server/components/world.ts
@@ -0,0 +1,101 @@
+import { spiral } from "../../common/utils.ts";
+import { chunks, enumerate, log_from, Vec2 } from "../../common/utils.ts";
+import { Engine, Entity, Query } from "../engine.ts";
+import { CompDisplay } from "./display.ts";
+const log = log_from(import.meta);
+
+export class CompPos {
+ position;
+ entity;
+
+ constructor(entity: Entity, position: Vec2) {
+ this.position = position;
+ this.entity = entity;
+ }
+
+ move(displacement: Vec2) {
+ this.position = this.position.add(displacement);
+ }
+
+ move_collide(engine: Engine, displacement: Vec2) {
+ let result = false;
+ for (const step of displacement_steps(this.position, displacement)) {
+ const collider_exists = engine.query_one(query_at(step)) !== null;
+ if (collider_exists) return result;
+ this.position = step;
+ result = true;
+ }
+ return result;
+ }
+}
+
+function* displacement_steps(position: Vec2, displacement: Vec2) {
+ let pos = position;
+ let left = displacement;
+ while (left.len() >= 1) {
+ if (left.x() >= 1) {
+ pos = pos.add(new Vec2(1, 0));
+ left = left.sub(new Vec2(1, 0));
+ }
+ if (left.x() <= -1) {
+ pos = pos.add(new Vec2(-1, 0));
+ left = left.sub(new Vec2(-1, 0));
+ }
+ if (left.y() >= 1) {
+ pos = pos.add(new Vec2(0, 1));
+ left = left.sub(new Vec2(0, 1));
+ }
+ if (left.y() <= -1) {
+ pos = pos.add(new Vec2(0, -1));
+ left = left.sub(new Vec2(0, -1));
+ }
+ yield pos;
+ }
+}
+
+export function query_at(pos: Vec2) {
+ return Query.filter(CompPos, (c) => c.position.overlaps(pos));
+}
+
+export function query_in_rect(min: Vec2, max: Vec2) {
+ return Query.filter(CompPos, (c) => c.position.inside(min, max));
+}
+
+class CompStructure {}
+
+export function sys_spawn_obstacle(pos: Vec2, display: string) {
+ return (engine: Engine) =>
+ engine.spawn((entity) =>
+ entity.insert(
+ new CompStructure(),
+ new CompDisplay(display),
+ new CompPos(entity, pos),
+ )
+ );
+}
+
+export async function sys_spawn_structure(file_path: string, origin: Vec2) {
+ const content = await Deno.readTextFile(file_path);
+ return (engine: Engine) => {
+ let count = 0;
+ for (const [y, line] of enumerate(content.split("\n"))) {
+ for (const [x, chars] of enumerate(chunks(line, 2))) {
+ const display = chars[0] + chars[1];
+ if (display === " ") continue;
+ engine.run(sys_spawn_obstacle(origin.add(new Vec2(x, y)), display));
+ count += 1;
+ }
+ }
+ log("Spawned", count, "tiles from", file_path);
+ };
+}
+
+export function sys_find_free_pos(target: Vec2) {
+ return (engine: Engine) => {
+ for (const pos of spiral(target)) {
+ const found = engine.query_one(query_at(pos));
+ if (found === null) return pos;
+ }
+ throw new Error("Unreachable.");
+ };
+}
diff --git a/server/engine.ts b/server/engine.ts
index bf13550..2d5b3b4 100644
--- a/server/engine.ts
+++ b/server/engine.ts
@@ -1,123 +1,92 @@
import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts";
-import { chunks, enumerate, log_from, Prototyped, range, Vec2 } from "../common/utils.ts";
-import { ClientInterface } from "./network.ts";
-import { Session } from "./entities/player.ts";
+import { Awaitable, log_from, Prototyped, Vec2 } from "../common/utils.ts";
import { ClassMap } from "../common/utils.ts";
-import { maybe } from "../common/utils.ts";
+import { wait } from "../common/utils.ts";
+import { Constructible } from "../common/utils.ts";
+import { first } from "../common/utils.ts";
const log = log_from(import.meta);
-export class WorldEntity {
- display;
- world;
+export class Entity {
identifier;
- position;
- compontents;
+ components;
+ engine;
- constructor(world: World, identifier: number, display: string, position: Vec2, ...components: Prototyped[]) {
- this.display = display;
- this.world = world;
+ constructor(identifier: number, engine: Engine) {
this.identifier = identifier;
- this.position = position;
- this.compontents = new ClassMap();
- for (const c of components) this.compontents.insert(c);
- }
-}
-
-export class World {
- next_id;
- entities;
-
- constructor() {
- this.next_id = 0;
- this.entities = new Map();
+ this.engine = engine;
+ this.components = new ClassMap();
}
- spawn_entity(display: string, pos: Vec2) {
- const entity = new WorldEntity(this, this.next_id++, display, pos);
- this.entities.set(entity.identifier, entity);
- return entity;
+ insert(...components: Prototyped[]) {
+ for (const c of components) this.components.insert(c);
+ return this;
}
- async spawn_structure(file_path: string, origin: Vec2) {
- const content = await Deno.readTextFile(file_path);
- let count = 0;
- for (const [y, line] of enumerate(content.split("\n"))) {
- for (const [x, chars] of enumerate(chunks(line, 2))) {
- const display = chars[0] + chars[1];
- if (display === " ") continue;
- this.spawn_entity(display, origin.add(new Vec2(x, y)));
- count += 1;
- }
- }
- log("Spawned", count, "tiles from", file_path);
+ get(class_: Constructible) {
+ return this.components.get(class_);
}
- render(center: Vec2, size: Vec2) {
- const radius = size.scale(0.5);
- const result = Array.from(range(0, size.y())).map(() => Array.from(range(0, size.x())).map(() => " "));
- const top_left = center.sub(radius);
- const bottom_right = center.add(radius);
- for (const entity of this.entities.values()) {
- if (!entity.position.inside(top_left, bottom_right)) continue;
- const local_pos = entity.position.sub(top_left);
- result[local_pos.y()][local_pos.x()] = entity.display;
- }
- return result.map((line) => line.join("")).toReversed().join("\n");
- }
-
- move_collide(entity_id: number, displacement: Vec2) {
- let result = false;
- const entity = this.get_entity(entity_id);
- if (entity === undefined) return result;
- for (const step of displacement_steps(entity.position, displacement)) {
- const collision = this.get_entity_at(step);
- if (collision !== undefined) return result;
- entity.position = step;
- result = true;
- }
+ get_force(class_: Constructible) {
+ const result = this.get(class_);
+ if (result === null) throw new Error();
return result;
}
-
- get_entity(entity_id: number) {
- return this.entities.get(entity_id);
- }
-
- *get_entities_at(pos: Vec2) {
- for (const entity of this.entities.values()) if (entity.position.overlaps(pos)) yield entity;
- }
-
- get_entity_at(pos: Vec2) {
- return maybe([...this.get_entities_at(pos)][0]);
- }
-
- *get_entities_in_range(min: Vec2, max: Vec2) {
- for (const entity of this.entities.values()) if (entity.position.inside(min, max)) yield entity;
- }
-
- get_entity_in_range(min: Vec2, max: Vec2) {
- return maybe([...this.get_entities_in_range(min, max)][0]);
- }
-
- kill(entity_id: number) {
- this.entities.delete(entity_id);
- }
}
export class Engine {
- world;
+ next_identifier;
+ entities;
- constructor(world: World) {
- this.world = world;
+ constructor() {
+ this.next_identifier = 0;
+ this.entities = new Map();
}
- async start() {
+ start() {
log("Started engine.");
}
- add_session(client: ClientInterface) {
- const player_entity = this.world.spawn_entity("`/", new Vec2(0, 0));
- const session = new Session(client, this.world, player_entity);
- session.spin();
+ spawn(op: (entity: Entity) => unknown) {
+ const identifier = this.next_identifier++;
+ const entity = new Entity(identifier, this);
+ op(entity);
+ this.entities.set(identifier, entity);
+ return entity;
+ }
+
+ run(sys: (engine: Engine) => T) {
+ return sys(this);
+ }
+
+ async global_system_loop(sys: (engine: Engine) => Awaitable, interval: number) {
+ while (true) {
+ await sys(this);
+ await wait(interval);
+ }
+ }
+
+ *query_all(query: Query) {
+ yield* query.run(this.entities.values());
+ }
+
+ query_one(query: Query) {
+ return first(this.query_all(query));
+ }
+
+ get(identifier: number) {
+ const value = this.entities.get(identifier);
+ if (value === undefined) return null;
+ return value;
+ }
+
+ get_force(identifier: number) {
+ const entity = this.get(identifier);
+ if (entity === null) throw new Error();
+ return entity;
+ }
+
+ delete(identifier: number) {
+ return this.entities.delete(identifier);
}
}
@@ -151,3 +120,42 @@ Deno.test("test_displacement", () => {
[new Vec2(2, 2), new Vec2(3, 3), new Vec2(4, 4), new Vec2(5, 5), new Vec2(5, 6), new Vec2(5, 7)],
);
});
+
+export class Query {
+ private test;
+
+ constructor(test: (entity: Entity) => boolean) {
+ this.test = test;
+ }
+
+ and(query: Query) {
+ return new Query((entity) => this.test(entity) && query.test(entity));
+ }
+
+ static with(...components: Constructible[]) {
+ return new Query((entity) => {
+ for (const comp of components) if (!entity.components.has(comp)) return false;
+ return true;
+ });
+ }
+
+ with(...components: Constructible[]) {
+ return this.and(Query.with(...components));
+ }
+
+ static filter(component: Constructible, filter: (comp: I, entity: Entity) => boolean) {
+ return new Query((entity) => {
+ const comp = entity.get(component);
+ if (comp === null) return false;
+ return filter(comp, entity);
+ });
+ }
+
+ filter(component: Constructible, filter: (comp: I, entity: Entity) => boolean) {
+ return this.and(Query.filter(component, filter));
+ }
+
+ *run(entities: Iterable) {
+ for (const entity of entities) if (this.test(entity)) yield entity;
+ }
+}
diff --git a/server/entities/enemy.ts b/server/entities/enemy.ts
index cd7b461..134b76d 100644
--- a/server/entities/enemy.ts
+++ b/server/entities/enemy.ts
@@ -1,33 +1,25 @@
-import { World, WorldEntity } from "../engine.ts";
-import { v2, Vec2 } from "../../common/utils.ts";
-import { wait } from "../../common/utils.ts";
+import { Vec2 } from "../../common/utils.ts";
+import { CompDisplay } from "../components/display.ts";
+import { CompPos } from "../components/world.ts";
+import { Engine } from "../engine.ts";
-export class Enemy {
- entity;
- alive;
-
- constructor(entity: WorldEntity) {
- this.entity = entity;
- this.alive = true;
- }
-
- static spawn(world: World, pos: Vec2) {
- const entity = world.spawn_entity("èé", pos);
- }
-
- async spin() {
- while (this.alive) {
- await wait(500);
- const target = this.find_target();
- }
- }
-
- find_target() {
- const world = this.entity.world;
- const local_origin = this.entity.position;
- const found = world.get_entities_in_range(local_origin.sub(v2(3, 3)), local_origin.add(v2(3, 3)));
- for (const entity of found) {
- // if (entity.compontents.get())
- }
+export class CompEnemy {
+ target;
+ life;
+ constructor() {
+ this.target = null as number | null;
+ this.life = 10;
}
}
+
+export function sys_spawn_enemy(pos: Vec2) {
+ return (engine: Engine) => {
+ return engine.spawn((entity) =>
+ entity.insert(
+ new CompEnemy(),
+ new CompPos(entity, pos),
+ new CompDisplay("èé"),
+ )
+ );
+ };
+}
diff --git a/server/entities/player.ts b/server/entities/player.ts
index cd9b66b..a8ffebb 100644
--- a/server/entities/player.ts
+++ b/server/entities/player.ts
@@ -1,21 +1,32 @@
import { mts } from "../../common/mod.ts";
-import { log_from, Vec2 } from "../../common/utils.ts";
-import { World, WorldEntity } from "../engine.ts";
+import { range } from "../../common/utils.ts";
+import { log_from, v2, Vec2 } from "../../common/utils.ts";
+import { CompDisplay } from "../components/display.ts";
+import { sys_find_free_pos } from "../components/world.ts";
+import { CompPos, query_in_rect } from "../components/world.ts";
+import { Engine, Entity } from "../engine.ts";
import { ClientInterface } from "../network.ts";
const log = log_from(import.meta);
export class Session {
client;
- world;
- player_entity;
+ engine;
+ entity;
- constructor(client: ClientInterface, world: World, player_entity: WorldEntity) {
+ constructor(client: ClientInterface, engine: Engine, entity: Entity) {
this.client = client;
- this.world = world;
- this.player_entity = player_entity;
+ this.engine = engine;
+ this.entity = entity;
}
- async spin() {
+ static init(client: ClientInterface, engine: Engine) {
+ const spawn_pos = engine.run(sys_find_free_pos(v2(0, 0)));
+ const entity = engine.run(sys_spawn_player(spawn_pos));
+ const self = new Session(client, engine, entity);
+ return self;
+ }
+
+ async start() {
try {
for await (const input of this.client.inputs.iter()) {
if (input.kind !== "request_display") log("Received", input);
@@ -23,39 +34,61 @@ export class Session {
if (input.kind === "ping") this.client.outputs.send({ kind: "ping_response", content: input.content });
if (input.kind === "request_display") this.send_display(input.content.width, input.content.height);
if (input.kind === "input") this.handle_input(input);
- if (input.kind === "exit") this.world.kill(this.player_entity.identifier);
+ if (input.kind === "exit") this.engine.delete(this.entity.identifier);
}
} catch (error) {
console.error("Session loop failed, ", error);
- this.world.kill(this.player_entity.identifier);
+ this.engine.delete(this.entity.identifier);
}
}
+ handle_input(input: mts.MsgInput) {
+ if (input.content.control === "up") this.move(v2(0, 1));
+ if (input.content.control === "down") this.move(v2(0, -1));
+ if (input.content.control === "left") this.move(v2(-1, 0));
+ if (input.content.control === "right") this.move(v2(1, 0));
+ }
+
send_display(width: number, height: number) {
- const raw = this.world.render(this.player_entity.position, new Vec2(width, height));
+ const raw = this.engine.run(sys_render_world(this.entity.get_force(CompPos).position, v2(width, height)));
this.client.outputs.send({ kind: "display", content: { raw } });
}
- handle_input(input: mts.MsgInput) {
- if (input.content.control === "up") this.world.move_collide(this.player_entity.identifier, new Vec2(0, 1));
- if (input.content.control === "down") this.world.move_collide(this.player_entity.identifier, new Vec2(0, -1));
- if (input.content.control === "left") this.world.move_collide(this.player_entity.identifier, new Vec2(-1, 0));
- if (input.content.control === "right") this.world.move_collide(this.player_entity.identifier, new Vec2(1, 0));
- }
-}
-
-export class PlayerComponent {
- entity;
-
- constructor(entity: WorldEntity) {
- this.entity = entity;
- }
-
move(direction: Vec2) {
- return this.entity.world.move_collide(this.entity.identifier, direction);
- }
-
- render(res: Vec2) {
- return this.entity.world.render(this.entity.position, res);
+ const pos = this.entity.get_force(CompPos);
+ pos.move_collide(this.engine, direction);
+ }
+}
+
+export function sys_render_world(center: Vec2, size: Vec2) {
+ return (engine: Engine) => {
+ const radius = size.scale(0.5);
+ const result = Array.from(range(0, size.y())).map(() => Array.from(range(0, size.x())).map(() => " "));
+ const min = center.sub(radius);
+ const max = center.add(radius);
+ for (const entity of engine.query_all(query_in_rect(min, max).with(CompDisplay))) {
+ const local_pos = entity.get_force(CompPos).position.sub(min);
+ result[local_pos.y()][local_pos.x()] = entity.get_force(CompDisplay).display;
+ }
+ return result.map((line) => line.join("")).toReversed().join("\n");
+ };
+}
+
+export function sys_spawn_player(position: Vec2) {
+ return (engine: Engine) => {
+ return engine.spawn((entity) =>
+ entity.insert(
+ new CompPlayer(),
+ new CompPos(entity, position),
+ new CompDisplay("`/"),
+ )
+ );
+ };
+}
+
+class CompPlayer {
+ life;
+ constructor() {
+ this.life = 10;
}
}
diff --git a/server/main.ts b/server/main.ts
index 500c20c..18357df 100755
--- a/server/main.ts
+++ b/server/main.ts
@@ -1,24 +1,35 @@
#!/bin/env -S deno run --allow-net --allow-read
-import { log_from, Vec2 } from "../common/utils.ts";
-import { Engine, World } from "./engine.ts";
-import { Enemy } from "./entities/enemy.ts";
+import { log_from, v2, wait } from "../common/utils.ts";
+import { Engine } from "./engine.ts";
+import { Session } from "./entities/player.ts";
+import { sys_spawn_obstacle, sys_spawn_structure } from "./components/world.ts";
import { Gateway } from "./network.ts";
+import { sys_spawn_enemy } from "./entities/enemy.ts";
+import { spiral } from "../common/utils.ts";
const log = log_from(import.meta);
async function main() {
log("Starting.");
- const world = new World();
- await world.spawn_structure("../data/structures/houses.txt", new Vec2(2, 2));
- const engine = new Engine(world);
+ const engine = new Engine();
engine.start();
+ engine.run(await sys_spawn_structure("../data/structures/houses.txt", v2(2, 2)));
+ engine.run(sys_spawn_enemy(v2(1, 1)));
- Enemy.spawn(world, new Vec2(-3, -3));
+ (async () => {
+ await wait(5_000);
+ for (const pos of spiral(v2(-8, -8))) {
+ await wait(500);
+ engine.run(sys_spawn_obstacle(pos, "@@"));
+ }
+ })();
const port = 9999;
const gateway = new Gateway(port);
log("Awaiting connection.");
- for await (const session of gateway.accept()) engine.add_session(session);
+ for await (const client of gateway.accept()) {
+ Session.init(client, engine).start();
+ }
}
if (import.meta.main) await main();