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();