init
This commit is contained in:
commit
28b026a614
17 changed files with 895 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/deno.lock
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"deno.enable": true
|
||||
}
|
125
client/main.ts
Executable file
125
client/main.ts
Executable file
|
@ -0,0 +1,125 @@
|
|||
#!/bin/env -S deno run --allow-net --allow-run
|
||||
import { MsgToClient, MsgToServer, mtc } from "../common/mod.ts";
|
||||
import { run } from "../common/utils.ts";
|
||||
import {
|
||||
channel,
|
||||
launch_caught,
|
||||
log_from,
|
||||
parsed_stream,
|
||||
Receiver,
|
||||
Sender,
|
||||
serialized_stream,
|
||||
wait,
|
||||
} from "../common/utils.ts";
|
||||
const log = log_from(import.meta);
|
||||
|
||||
async function main() {
|
||||
log("Client starting.");
|
||||
const interface_ = await ServerInterface.connect(9999);
|
||||
new InputHandler(interface_.outputs).spin();
|
||||
const display = new DisplayHandler(interface_.outputs);
|
||||
display.spin();
|
||||
new MsgHandler(interface_.inputs, display).spin();
|
||||
log("Connected.");
|
||||
|
||||
interface_.outputs.send({ kind: "ping", content: { message: "Machin." } });
|
||||
log("Sent ping.");
|
||||
}
|
||||
|
||||
class MsgHandler {
|
||||
receiver;
|
||||
display;
|
||||
|
||||
constructor(receiver: Receiver<MsgToClient>, display: DisplayHandler) {
|
||||
this.receiver = receiver;
|
||||
this.display = display;
|
||||
}
|
||||
|
||||
async spin() {
|
||||
for await (const event of this.receiver.iter()) {
|
||||
if (event.kind === "ping_response") log("Received ping response :", event.content);
|
||||
if (event.kind === "display") this.display.refresh(event.content.raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InputHandler {
|
||||
outputs;
|
||||
|
||||
constructor(outputs: Sender<MsgToServer>) {
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
async read_one_char() {
|
||||
const args = ["-i0", "sh", "-c", "read -n 1 OUT; echo -n $OUT"];
|
||||
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)} `);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async spin() {
|
||||
while (true) {
|
||||
const read = await this.read_one_char();
|
||||
if (read === "z") this.outputs.send({ kind: "input", content: { control: "up" } });
|
||||
if (read === "s") this.outputs.send({ kind: "input", content: { control: "down" } });
|
||||
if (read === "q") this.outputs.send({ kind: "input", content: { control: "left" } });
|
||||
if (read === "d") this.outputs.send({ kind: "input", content: { control: "right" } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DisplayHandler {
|
||||
outputs;
|
||||
|
||||
constructor(outputs: Sender<MsgToServer>) {
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
async spin() {
|
||||
while (true) {
|
||||
await wait(100);
|
||||
const [width, height] = await this.get_res();
|
||||
this.outputs.send({ kind: "request_display", content: { width, height } });
|
||||
}
|
||||
}
|
||||
|
||||
async get_res() {
|
||||
const width = parseInt(await run("tput", "cols")) / 2 - 2; // note : tiles are 2 char wide
|
||||
const height = parseInt(await run("tput", "lines")) - 2;
|
||||
return [width, height];
|
||||
}
|
||||
|
||||
refresh(raw: string) {
|
||||
const clear_sequence = String.fromCharCode(0o033) + String.fromCharCode(0o143);
|
||||
console.log(clear_sequence + raw);
|
||||
}
|
||||
}
|
||||
|
||||
class ServerInterface {
|
||||
inputs;
|
||||
outputs;
|
||||
|
||||
constructor(inputs: Receiver<MsgToClient>, outputs: Sender<MsgToServer>) {
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
static async connect(port: number) {
|
||||
const connection = await Deno.connect({ port });
|
||||
return await ServerInterface.init(connection);
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
static async init(connection: Deno.Conn) {
|
||||
// TODO : handshake ?
|
||||
const [input_sender, input_receiver] = channel<MsgToClient>();
|
||||
const [output_sender, output_receiver] = channel<MsgToServer>();
|
||||
launch_caught(() => input_sender.send_all(parsed_stream(mtc.message_to_client_parser())(connection.readable)));
|
||||
launch_caught(() => serialized_stream(output_receiver.iter())(connection.writable));
|
||||
return new ServerInterface(input_receiver, output_sender);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) await main();
|
10
client/watch.sh
Executable file
10
client/watch.sh
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
cd "$(dirname "$(realpath "$0")")"
|
||||
|
||||
nodemon -w ../common -w . -e ts -x '
|
||||
while true;
|
||||
do
|
||||
./main.ts;
|
||||
sleep 1s;
|
||||
done'
|
0
common/message/message.ts
Normal file
0
common/message/message.ts
Normal file
32
common/message/to_client.ts
Normal file
32
common/message/to_client.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts";
|
||||
import { ParserOutput } from "../utils.ts";
|
||||
|
||||
export type MsgToClient = ParserOutput<typeof message_to_client_parser>;
|
||||
export function message_to_client_parser() {
|
||||
return ping_response_parser()
|
||||
.or(display_parser());
|
||||
}
|
||||
|
||||
export type MsgPingResponse = ParserOutput<typeof ping_response_parser>;
|
||||
export function ping_response_parser() {
|
||||
return z.object({
|
||||
kind: z.literal("ping_response"),
|
||||
content: z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export type MsgDisplay = ParserOutput<typeof display_parser>;
|
||||
export function display_parser() {
|
||||
return z.object({
|
||||
kind: z.literal("display"),
|
||||
content: z.object({
|
||||
raw: z.string(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function char_parser() {
|
||||
return z.string().length(1);
|
||||
}
|
58
common/message/to_server.ts
Normal file
58
common/message/to_server.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts";
|
||||
import { ParserOutput } from "../utils.ts";
|
||||
|
||||
export type MsgToServer = ParserOutput<typeof message_to_server_parser>;
|
||||
export function message_to_server_parser() {
|
||||
return ping_parser()
|
||||
.or(request_display_parser())
|
||||
.or(input_parser())
|
||||
.or(exit_parser());
|
||||
}
|
||||
|
||||
export type MsgPing = ParserOutput<typeof ping_parser>;
|
||||
export function ping_parser() {
|
||||
return z.object({
|
||||
kind: z.literal("ping"),
|
||||
content: z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export type MsgReqDisplay = ParserOutput<typeof request_display_parser>;
|
||||
export function request_display_parser() {
|
||||
return z.object({
|
||||
kind: z.literal("request_display"),
|
||||
content: z.object({
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export type MsgInput = ParserOutput<typeof input_parser>;
|
||||
export function input_parser() {
|
||||
return z.object({
|
||||
kind: z.literal("input"),
|
||||
content: z.object({
|
||||
control: control_parser(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export type ClientInput = ParserOutput<typeof control_parser>;
|
||||
function control_parser() {
|
||||
return z.literal("up")
|
||||
.or(z.literal("down"))
|
||||
.or(z.literal("left"))
|
||||
.or(z.literal("right"))
|
||||
.or(z.literal("interact"))
|
||||
.or(z.literal("attack"));
|
||||
}
|
||||
|
||||
export type MsgExit = ParserOutput<typeof exit_parser>;
|
||||
export function exit_parser() {
|
||||
return z.object({
|
||||
kind: z.literal("exit"),
|
||||
});
|
||||
}
|
4
common/mod.ts
Normal file
4
common/mod.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * as mtc from "./message/to_client.ts";
|
||||
export * as mts from "./message/to_server.ts";
|
||||
export type { MsgToClient } from "./message/to_client.ts";
|
||||
export type { MsgToServer } from "./message/to_server.ts";
|
322
common/utils.ts
Normal file
322
common/utils.ts
Normal file
|
@ -0,0 +1,322 @@
|
|||
import { assert, assertEquals, assertInstanceOf } from "https://deno.land/std@0.221.0/assert/mod.ts";
|
||||
import * as path from "https://deno.land/std@0.221.0/path/mod.ts";
|
||||
import { TextLineStream } from "https://deno.land/std@0.221.0/streams/mod.ts";
|
||||
import { z, ZodType } from "https://deno.land/x/zod@v3.22.4/mod.ts";
|
||||
|
||||
export function parsed_stream<T>(parser: ZodType<T>) {
|
||||
return async function* (readable: ReadableStream<Uint8Array>) {
|
||||
const line_stream = readable
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new TextLineStream());
|
||||
for await (const line of line_stream) yield parser.parse(JSON.parse(line));
|
||||
};
|
||||
}
|
||||
|
||||
export function serialized_stream<T>(generator: AsyncIterable<T>) {
|
||||
return async function (writable: WritableStream<Uint8Array>) {
|
||||
const stream = new TextEncoderStream();
|
||||
stream.readable.pipeTo(writable);
|
||||
for await (const item of generator) {
|
||||
const writer = stream.writable.getWriter();
|
||||
await writer.write(JSON.stringify(item) + "\n");
|
||||
writer.releaseLock();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type Consumer<T> = (item: T) => void;
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
export type ParserOutput<T extends () => ZodType<any, any, any>> = z.infer<ReturnType<T>>;
|
||||
|
||||
export type PromiseSplit<T> = ReturnType<typeof split_promise<T>>;
|
||||
export function split_promise<T>() {
|
||||
let resolver_ = null as Consumer<T> | null;
|
||||
const promise = new Promise<T>((r) => {
|
||||
resolver_ = r;
|
||||
});
|
||||
assert(resolver_ !== null);
|
||||
const resolver = resolver_ as Consumer<T>;
|
||||
return { promise, resolver };
|
||||
}
|
||||
|
||||
export class AsyncQueue<T> {
|
||||
private items;
|
||||
private awaiting;
|
||||
|
||||
public constructor() {
|
||||
this.items = [] as T[];
|
||||
this.awaiting = [] as Consumer<void>[];
|
||||
}
|
||||
|
||||
public push(item: T) {
|
||||
this.items.push(item);
|
||||
const [first] = this.awaiting.splice(0, 1);
|
||||
if (first !== undefined) first();
|
||||
}
|
||||
|
||||
public async pull() {
|
||||
let first = this.take_first_item();
|
||||
if (first !== undefined) return first;
|
||||
const { promise, resolver } = split_promise<void>();
|
||||
this.awaiting.push(resolver);
|
||||
await promise;
|
||||
first = this.take_first_item();
|
||||
assert(first !== undefined);
|
||||
return first as T;
|
||||
}
|
||||
|
||||
private take_first_item() {
|
||||
const [first] = this.items.splice(0, 1);
|
||||
return first as T | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function channel<T>() {
|
||||
const queue = new AsyncQueue<T>();
|
||||
return [new Sender(queue), new Receiver(queue)] as const;
|
||||
}
|
||||
|
||||
export function channel_generator<T>(generator: AsyncIterable<T>) {
|
||||
const [sender, receiver] = channel();
|
||||
(async () => {
|
||||
for await (const item of generator) sender.send(item);
|
||||
})();
|
||||
return receiver;
|
||||
}
|
||||
|
||||
export class Sender<T> {
|
||||
queue;
|
||||
|
||||
public constructor(queue: AsyncQueue<T>) {
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
public send(item: T) {
|
||||
this.queue.push(item);
|
||||
}
|
||||
|
||||
public async send_all(iter: AsyncGenerator<T>) {
|
||||
for await (const item of iter) this.send(item);
|
||||
}
|
||||
}
|
||||
|
||||
export class Receiver<T> {
|
||||
queue;
|
||||
|
||||
public constructor(queue: AsyncQueue<T>) {
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
public async receive() {
|
||||
return await this.queue.pull();
|
||||
}
|
||||
|
||||
public async *iter() {
|
||||
while (true) yield this.receive();
|
||||
}
|
||||
}
|
||||
|
||||
export function range(from: number, to: number) {
|
||||
return new Range(from, to);
|
||||
}
|
||||
|
||||
export class Range {
|
||||
from;
|
||||
to;
|
||||
|
||||
constructor(from: number, to: number) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
includes(n: number) {
|
||||
return (n >= this.from) && (n < this.to);
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
let index = this.from;
|
||||
const to = this.to;
|
||||
return (function* () {
|
||||
while (index < to) yield index++;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
export async function launch_caught<T>(
|
||||
operation: () => Promise<T> | T,
|
||||
handler: null | ((error: unknown) => void) = null,
|
||||
) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (handler !== null) handler(error);
|
||||
else console.error("Lanched Uncaught :", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function log_from(meta: ImportMeta) {
|
||||
return function (...args: unknown[]) {
|
||||
const mod = path.basename(new URL(meta.url).pathname);
|
||||
console.log(`[${mod}]`, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
export class Vec2 {
|
||||
private x_;
|
||||
private y_;
|
||||
|
||||
public constructor(x: number, y: number) {
|
||||
this.x_ = Math.round(x);
|
||||
this.y_ = Math.round(y);
|
||||
}
|
||||
|
||||
public x() {
|
||||
return this.x_;
|
||||
}
|
||||
|
||||
public y() {
|
||||
return this.y_;
|
||||
}
|
||||
|
||||
public scale(factor: number) {
|
||||
return new Vec2(this.x_ * factor, this.y_ * factor);
|
||||
}
|
||||
|
||||
public neg() {
|
||||
return this.scale(-1);
|
||||
}
|
||||
|
||||
public add(other: Vec2) {
|
||||
return new Vec2(this.x_ + other.x_, this.y_ + other.y_);
|
||||
}
|
||||
|
||||
public sub(other: 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;
|
||||
return true;
|
||||
}
|
||||
|
||||
public eq(other: Vec2) {
|
||||
return (this.x() === other.x()) && (this.y() === other.y());
|
||||
}
|
||||
|
||||
public neq(other: Vec2) {
|
||||
return !this.eq(other);
|
||||
}
|
||||
|
||||
public len() {
|
||||
return Math.sqrt(this.x() ** 2 + this.y() ** 2);
|
||||
}
|
||||
|
||||
public distance(other: Vec2) {
|
||||
return this.sub(other).len();
|
||||
}
|
||||
|
||||
public overlaps(other: Vec2) {
|
||||
return this.distance(other) < 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function v2(x: number, y: number) {
|
||||
return new Vec2(x, y);
|
||||
}
|
||||
|
||||
export function* enumerate<T>(iterator: Iterable<T>) {
|
||||
let index = 0;
|
||||
for (const item of iterator) yield [index++, item] as [number, T];
|
||||
}
|
||||
|
||||
export function* chunks<T>(iterator: Iterable<T>, size: number) {
|
||||
let current = [];
|
||||
for (const item of iterator) {
|
||||
current.push(item);
|
||||
if (current.length < size) continue;
|
||||
yield current;
|
||||
current = [];
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("test_chunk", () => {
|
||||
assertEquals(
|
||||
[...chunks([1, 2, 3, 4, 5, 6, 7], 3)],
|
||||
[[1, 2, 3], [4, 5, 6]],
|
||||
);
|
||||
});
|
||||
|
||||
export function wait(ms: number) {
|
||||
return new Promise((res) => setTimeout(res, ms));
|
||||
}
|
||||
|
||||
export async function run(cmd: string, ...args: string[]) {
|
||||
const command = new Deno.Command(cmd, { args, stdout: "piped" });
|
||||
const output = await command.output();
|
||||
if (!output.success) throw new Error();
|
||||
return new TextDecoder().decode(output.stdout);
|
||||
}
|
||||
|
||||
export type Prototyped<P = unknown> = { constructor: { prototype: P } };
|
||||
export type Constructible<I = unknown> = new (...args: unknown[]) => I;
|
||||
export class ClassMap<B = unknown> {
|
||||
private inner;
|
||||
|
||||
constructor() {
|
||||
this.inner = new Map();
|
||||
}
|
||||
|
||||
insert<V extends Prototyped & B>(object: V) {
|
||||
const prototype = object.constructor.prototype;
|
||||
if (this.inner.has(prototype)) throw new Error();
|
||||
this.inner.set(prototype, object);
|
||||
}
|
||||
|
||||
get<I extends B, C extends Constructible<I>>(class_: C) {
|
||||
const value = this.inner.get(class_.prototype);
|
||||
if (value === undefined) return null;
|
||||
assertInstanceOf(value, class_);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("test_classmap", () => {
|
||||
{
|
||||
class Truc {}
|
||||
class Machin {}
|
||||
class Chose {}
|
||||
|
||||
const map = new ClassMap();
|
||||
const truc = new Truc();
|
||||
map.insert(truc);
|
||||
const machin = new Machin();
|
||||
map.insert(machin);
|
||||
|
||||
assert(map.get(Truc) === truc);
|
||||
assert(map.get(Machin) !== truc);
|
||||
assert(map.get(Machin) === machin);
|
||||
assert(map.get(Chose) === null);
|
||||
}
|
||||
{
|
||||
interface Machinable {
|
||||
machin(): number;
|
||||
}
|
||||
// deno-lint-ignore no-unused-vars
|
||||
class Truc {}
|
||||
class Machin implements Machinable {
|
||||
machin = () => 35;
|
||||
}
|
||||
const map = new ClassMap<Machinable>();
|
||||
map.insert(new Machin());
|
||||
// map.insert(new Truc()); // note : Fails with improper typing.
|
||||
|
||||
map.get(Machin);
|
||||
// map.get(Truc); // note : Fails with improper typing.
|
||||
}
|
||||
});
|
||||
|
||||
export function maybe<T>(item: T) {
|
||||
return item as T | undefined;
|
||||
}
|
8
data/structures/houses.txt
Normal file
8
data/structures/houses.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
[][][] [][]
|
||||
[] []
|
||||
[] []
|
||||
[] [][][]
|
||||
[][][][][][] []
|
||||
[]
|
||||
[] []
|
||||
[][][][][]
|
6
deno.json
Normal file
6
deno.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"fmt": {
|
||||
"useTabs": true,
|
||||
"lineWidth": 120
|
||||
}
|
||||
}
|
153
server/engine.ts
Normal file
153
server/engine.ts
Normal file
|
@ -0,0 +1,153 @@
|
|||
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 { ClassMap } from "../common/utils.ts";
|
||||
import { maybe } from "../common/utils.ts";
|
||||
const log = log_from(import.meta);
|
||||
|
||||
export class WorldEntity {
|
||||
display;
|
||||
world;
|
||||
identifier;
|
||||
position;
|
||||
compontents;
|
||||
|
||||
constructor(world: World, identifier: number, display: string, position: Vec2, ...components: Prototyped[]) {
|
||||
this.display = display;
|
||||
this.world = world;
|
||||
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>();
|
||||
}
|
||||
|
||||
spawn_entity(display: string, pos: Vec2) {
|
||||
const entity = new WorldEntity(this, this.next_id++, display, pos);
|
||||
this.entities.set(entity.identifier, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
world;
|
||||
|
||||
constructor(world: World) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
async 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();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("test_displacement", () => {
|
||||
assertEquals(
|
||||
[...displacement_steps(new Vec2(1, 1), new Vec2(4, 6))],
|
||||
[new Vec2(2, 2), new Vec2(3, 3), new Vec2(4, 4), new Vec2(5, 5), new Vec2(5, 6), new Vec2(5, 7)],
|
||||
);
|
||||
});
|
33
server/entities/enemy.ts
Normal file
33
server/entities/enemy.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { World, WorldEntity } from "../engine.ts";
|
||||
import { v2, Vec2 } from "../../common/utils.ts";
|
||||
import { wait } from "../../common/utils.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())
|
||||
}
|
||||
}
|
||||
}
|
61
server/entities/player.ts
Normal file
61
server/entities/player.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { mts } from "../../common/mod.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;
|
||||
world;
|
||||
player_entity;
|
||||
|
||||
constructor(client: ClientInterface, world: World, player_entity: WorldEntity) {
|
||||
this.client = client;
|
||||
this.world = world;
|
||||
this.player_entity = player_entity;
|
||||
}
|
||||
|
||||
async spin() {
|
||||
try {
|
||||
for await (const input of this.client.inputs.iter()) {
|
||||
if (input.kind !== "request_display") log("Received", input);
|
||||
|
||||
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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Session loop failed, ", error);
|
||||
this.world.kill(this.player_entity.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
send_display(width: number, height: number) {
|
||||
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) {
|
||||
return this.entity.world.move_collide(this.entity.identifier, direction);
|
||||
}
|
||||
|
||||
render(res: Vec2) {
|
||||
return this.entity.world.render(this.entity.position, res);
|
||||
}
|
||||
}
|
24
server/main.ts
Executable file
24
server/main.ts
Executable file
|
@ -0,0 +1,24 @@
|
|||
#!/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 { Gateway } from "./network.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);
|
||||
engine.start();
|
||||
|
||||
Enemy.spawn(world, new Vec2(-3, -3));
|
||||
|
||||
const port = 9999;
|
||||
const gateway = new Gateway(port);
|
||||
log("Awaiting connection.");
|
||||
for await (const session of gateway.accept()) engine.add_session(session);
|
||||
}
|
||||
|
||||
if (import.meta.main) await main();
|
50
server/network.ts
Normal file
50
server/network.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
#!/bin/env -S deno run --allow-net
|
||||
|
||||
import { MsgToClient, MsgToServer, mts } from "../common/mod.ts";
|
||||
import {
|
||||
channel,
|
||||
launch_caught,
|
||||
log_from,
|
||||
parsed_stream,
|
||||
Receiver,
|
||||
Sender,
|
||||
serialized_stream,
|
||||
} from "../common/utils.ts";
|
||||
const log = log_from(import.meta);
|
||||
|
||||
export class Gateway {
|
||||
server;
|
||||
constructor(port: number) {
|
||||
this.server = Deno.listen({ port });
|
||||
log("Listening on", port);
|
||||
}
|
||||
async *accept() {
|
||||
for await (const connection of this.server) {
|
||||
const session = await ClientInterface.init(connection);
|
||||
log("New session.");
|
||||
yield session;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientInterface {
|
||||
inputs;
|
||||
outputs;
|
||||
|
||||
constructor(inputs: Receiver<MsgToServer>, outputs: Sender<MsgToClient>) {
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
static async init(connection: Deno.Conn) {
|
||||
// TODO : handshake ?
|
||||
const [input_sender, input_receiver] = channel<MsgToServer>();
|
||||
const [output_sender, output_receiver] = channel<MsgToClient>();
|
||||
input_sender.send_all(parsed_stream(mts.message_to_server_parser())(connection.readable))
|
||||
.finally(() => input_sender.send({ kind: "exit" }));
|
||||
serialized_stream(output_receiver.iter())(connection.writable)
|
||||
.finally(() => input_sender.send({ kind: "exit" }));
|
||||
return new ClientInterface(input_receiver, output_sender);
|
||||
}
|
||||
}
|
5
server/watch.sh
Executable file
5
server/watch.sh
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
cd "$(dirname "$(realpath "$0")")"
|
||||
|
||||
nodemon -w ../common -w . -e ts -x 'clear && ./main.ts'
|
Loading…
Add table
Add a link
Reference in a new issue