diff --git a/README.md b/README.md
deleted file mode 100644
index 5530d01..0000000
--- a/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# 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 44c5720..362af52 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)} ${String.fromCharCode(0o33)}[A`);
+ console.log(`${String.fromCharCode(0x08)} `);
return result[0];
}
diff --git a/common/utils.ts b/common/utils.ts
index c9b4539..159d4e8 100644
--- a/common/utils.ts
+++ b/common/utils.ts
@@ -195,9 +195,9 @@ export class Vec2 {
return this.add(other.neg());
}
- 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;
+ 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;
return true;
}
@@ -220,10 +220,6 @@ 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) {
@@ -264,8 +260,7 @@ export async function run(cmd: string, ...args: string[]) {
}
export type Prototyped
= { constructor: { prototype: P } };
-// deno-lint-ignore no-explicit-any
-export type Constructible = new (...args: any[]) => I;
+export type Constructible = new (...args: unknown[]) => I;
export class ClassMap {
private inner;
@@ -285,10 +280,6 @@ export class ClassMap {
assertInstanceOf(value, class_);
return value;
}
-
- has>(class_: C) {
- return this.inner.has(class_.prototype);
- }
}
Deno.test("test_classmap", () => {
@@ -329,83 +320,3 @@ 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
deleted file mode 100644
index 8004381..0000000
--- a/server/components/display.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export class CompDisplay {
- display;
- constructor(display: string) {
- this.display = display;
- }
-}
diff --git a/server/components/world.ts b/server/components/world.ts
deleted file mode 100644
index 2c55f57..0000000
--- a/server/components/world.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-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 2d5b3b4..bf13550 100644
--- a/server/engine.ts
+++ b/server/engine.ts
@@ -1,92 +1,123 @@
import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts";
-import { Awaitable, log_from, Prototyped, Vec2 } from "../common/utils.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 { ClassMap } from "../common/utils.ts";
-import { wait } from "../common/utils.ts";
-import { Constructible } from "../common/utils.ts";
-import { first } from "../common/utils.ts";
+import { maybe } from "../common/utils.ts";
const log = log_from(import.meta);
-export class Entity {
+export class WorldEntity {
+ display;
+ world;
identifier;
- components;
- engine;
+ position;
+ compontents;
- constructor(identifier: number, engine: Engine) {
+ constructor(world: World, identifier: number, display: string, position: Vec2, ...components: Prototyped[]) {
+ this.display = display;
+ this.world = world;
this.identifier = identifier;
- this.engine = engine;
- this.components = new ClassMap();
+ 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();
}
- insert(...components: Prototyped[]) {
- for (const c of components) this.components.insert(c);
- return this;
+ spawn_entity(display: string, pos: Vec2) {
+ const entity = new WorldEntity(this, this.next_id++, display, pos);
+ this.entities.set(entity.identifier, entity);
+ return entity;
}
- get(class_: Constructible) {
- return this.components.get(class_);
+ 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_force(class_: Constructible) {
- const result = this.get(class_);
- if (result === null) throw new Error();
+ 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;
+ }
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 {
- next_identifier;
- entities;
+ world;
- constructor() {
- this.next_identifier = 0;
- this.entities = new Map();
+ constructor(world: World) {
+ this.world = world;
}
- start() {
+ async start() {
log("Started engine.");
}
- 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);
+ 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();
}
}
@@ -120,42 +151,3 @@ 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 134b76d..cd7b461 100644
--- a/server/entities/enemy.ts
+++ b/server/entities/enemy.ts
@@ -1,25 +1,33 @@
-import { Vec2 } from "../../common/utils.ts";
-import { CompDisplay } from "../components/display.ts";
-import { CompPos } from "../components/world.ts";
-import { Engine } from "../engine.ts";
+import { World, WorldEntity } from "../engine.ts";
+import { v2, Vec2 } from "../../common/utils.ts";
+import { wait } from "../../common/utils.ts";
-export class CompEnemy {
- target;
- life;
- constructor() {
- this.target = null as number | null;
- this.life = 10;
+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 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 a8ffebb..cd9b66b 100644
--- a/server/entities/player.ts
+++ b/server/entities/player.ts
@@ -1,32 +1,21 @@
import { mts } from "../../common/mod.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 { log_from, Vec2 } from "../../common/utils.ts";
+import { World, WorldEntity } from "../engine.ts";
import { ClientInterface } from "../network.ts";
const log = log_from(import.meta);
export class Session {
client;
- engine;
- entity;
+ world;
+ player_entity;
- constructor(client: ClientInterface, engine: Engine, entity: Entity) {
+ constructor(client: ClientInterface, world: World, player_entity: WorldEntity) {
this.client = client;
- this.engine = engine;
- this.entity = entity;
+ this.world = world;
+ this.player_entity = player_entity;
}
- 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() {
+ async spin() {
try {
for await (const input of this.client.inputs.iter()) {
if (input.kind !== "request_display") log("Received", input);
@@ -34,61 +23,39 @@ 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.engine.delete(this.entity.identifier);
+ if (input.kind === "exit") this.world.kill(this.player_entity.identifier);
}
} catch (error) {
console.error("Session loop failed, ", error);
- this.engine.delete(this.entity.identifier);
+ this.world.kill(this.player_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.engine.run(sys_render_world(this.entity.get_force(CompPos).position, v2(width, height)));
+ const raw = this.world.render(this.player_entity.position, new Vec2(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) {
- 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;
+ return this.entity.world.move_collide(this.entity.identifier, direction);
+ }
+
+ render(res: Vec2) {
+ return this.entity.world.render(this.entity.position, res);
}
}
diff --git a/server/main.ts b/server/main.ts
index 18357df..500c20c 100755
--- a/server/main.ts
+++ b/server/main.ts
@@ -1,35 +1,24 @@
#!/bin/env -S deno run --allow-net --allow-read
-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 { log_from, Vec2 } from "../common/utils.ts";
+import { Engine, World } from "./engine.ts";
+import { Enemy } from "./entities/enemy.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 engine = new Engine();
+ const world = new World();
+ await world.spawn_structure("../data/structures/houses.txt", new Vec2(2, 2));
+ const engine = new Engine(world);
engine.start();
- engine.run(await sys_spawn_structure("../data/structures/houses.txt", v2(2, 2)));
- engine.run(sys_spawn_enemy(v2(1, 1)));
- (async () => {
- await wait(5_000);
- for (const pos of spiral(v2(-8, -8))) {
- await wait(500);
- engine.run(sys_spawn_obstacle(pos, "@@"));
- }
- })();
+ Enemy.spawn(world, new Vec2(-3, -3));
const port = 9999;
const gateway = new Gateway(port);
log("Awaiting connection.");
- for await (const client of gateway.accept()) {
- Session.init(client, engine).start();
- }
+ for await (const session of gateway.accept()) engine.add_session(session);
}
if (import.meta.main) await main();