ecs refactor
This commit is contained in:
parent
28b026a614
commit
f2e5d13000
8 changed files with 410 additions and 170 deletions
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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<P = unknown> = { constructor: { prototype: P } };
|
||||
export type Constructible<I = unknown> = new (...args: unknown[]) => I;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
export type Constructible<I = unknown> = new (...args: any[]) => I;
|
||||
export class ClassMap<B = unknown> {
|
||||
private inner;
|
||||
|
||||
|
@ -280,6 +285,10 @@ export class ClassMap<B = unknown> {
|
|||
assertInstanceOf(value, class_);
|
||||
return value;
|
||||
}
|
||||
|
||||
has<I extends B, C extends Constructible<I>>(class_: C) {
|
||||
return this.inner.has(class_.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("test_classmap", () => {
|
||||
|
@ -320,3 +329,83 @@ Deno.test("test_classmap", () => {
|
|||
export function maybe<T>(item: T) {
|
||||
return item as T | undefined;
|
||||
}
|
||||
|
||||
export type Awaitable<T = unknown> = T | Promise<T>;
|
||||
|
||||
export function first<T>(iter: Iterable<T>) {
|
||||
for (const item of iter) return item;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function take_n<T>(iter: Iterable<T>, 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(),
|
||||
// ]
|
||||
// });
|
||||
|
|
6
server/components/display.ts
Normal file
6
server/components/display.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export class CompDisplay {
|
||||
display;
|
||||
constructor(display: string) {
|
||||
this.display = display;
|
||||
}
|
||||
}
|
101
server/components/world.ts
Normal file
101
server/components/world.ts
Normal file
|
@ -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.");
|
||||
};
|
||||
}
|
200
server/engine.ts
200
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<number, WorldEntity>();
|
||||
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<I>(class_: Constructible<I>) {
|
||||
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<I>(class_: Constructible<I>) {
|
||||
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<number, Entity>();
|
||||
}
|
||||
|
||||
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<T>(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<I>(component: Constructible<I>, 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<I>(component: Constructible<I>, filter: (comp: I, entity: Entity) => boolean) {
|
||||
return this.and(Query.filter(component, filter));
|
||||
}
|
||||
|
||||
*run(entities: Iterable<Entity>) {
|
||||
for (const entity of entities) if (this.test(entity)) yield entity;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("èé"),
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue