summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--client/css/main.css21
-rw-r--r--client/img/atlas.pngbin0 -> 6304 bytes
-rw-r--r--client/img/atlas.png.bak (renamed from client/img/pac.png)bin4583 -> 5670 bytes
-rw-r--r--client/img/dot.pngbin4363 -> 0 bytes
-rw-r--r--client/img/pac.gifbin1004 -> 0 bytes
-rw-r--r--client/img/wall_atlas.pngbin1261 -> 0 bytes
-rw-r--r--client/index.html8
-rw-r--r--client/js/game.js111
-rw-r--r--client/js/gfx/graphics.js128
-rw-r--r--client/js/gfx/map.js293
-rw-r--r--client/js/gfx/sprite.js71
-rw-r--r--client/js/input.js84
-rw-r--r--client/js/logic.js335
-rw-r--r--client/js/main.js168
-rw-r--r--client/src/logic/items.ts41
-rw-r--r--client/src/logic/logic.ts80
-rw-r--r--client/src/logic/movement.ts142
-rw-r--r--client/src/logic/players.ts79
-rw-r--r--client/src/logic/ui.ts32
-rw-r--r--client/src/main.ts75
-rw-r--r--client/src/map.ts151
-rw-r--r--client/src/net/game.ts186
-rw-r--r--client/src/net/input.ts70
-rw-r--r--client/src/net/multiplayer.ts (renamed from client/js/multiplayer.js)85
-rw-r--r--client/src/renderer.ts320
-rw-r--r--client/src/types.ts131
-rw-r--r--client/tsconfig.json11
-rw-r--r--server/Cargo.lock (renamed from Cargo.lock)0
-rw-r--r--server/Cargo.toml (renamed from Cargo.toml)0
-rw-r--r--server/src/main.rs (renamed from src/main.rs)0
-rw-r--r--server/src/room/handle.rs (renamed from src/room/handle.rs)0
-rw-r--r--server/src/room/messages.rs (renamed from src/room/messages.rs)0
-rw-r--r--server/src/room/mod.rs (renamed from src/room/mod.rs)0
-rw-r--r--server/src/room/websocket.rs (renamed from src/room/websocket.rs)0
-rw-r--r--server/src/rooms.rs (renamed from src/rooms.rs)0
-rw-r--r--server/src/routes.rs (renamed from src/routes.rs)2
37 files changed, 1416 insertions, 1211 deletions
diff --git a/.gitignore b/.gitignore
index ea8c4bf..f4c9b61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-/target
+server/target
+client/js
diff --git a/client/css/main.css b/client/css/main.css
index 86b8fd8..87c145c 100644
--- a/client/css/main.css
+++ b/client/css/main.css
@@ -71,24 +71,3 @@ input {
padding: .25rem;
margin-bottom: .215rem;
}
-
-#container img {
- image-rendering: pixelated;
-}
-
-#canvas {
- position: absolute;
- width: 100%;
- height: 100%;
-}
-
-#fps {
- position: absolute;
- left: 0;
- top: 0;
- z-index: 99;
- font-size: 20;
- font-style: monospace;
- background-color: black;
- color: white;
-}
diff --git a/client/img/atlas.png b/client/img/atlas.png
new file mode 100644
index 0000000..240705b
--- /dev/null
+++ b/client/img/atlas.png
Binary files differ
diff --git a/client/img/pac.png b/client/img/atlas.png.bak
index 7dc0989..3cb5dfc 100644
--- a/client/img/pac.png
+++ b/client/img/atlas.png.bak
Binary files differ
diff --git a/client/img/dot.png b/client/img/dot.png
deleted file mode 100644
index b911cea..0000000
--- a/client/img/dot.png
+++ /dev/null
Binary files differ
diff --git a/client/img/pac.gif b/client/img/pac.gif
deleted file mode 100644
index 4b0608b..0000000
--- a/client/img/pac.gif
+++ /dev/null
Binary files differ
diff --git a/client/img/wall_atlas.png b/client/img/wall_atlas.png
deleted file mode 100644
index bd3a5df..0000000
--- a/client/img/wall_atlas.png
+++ /dev/null
Binary files differ
diff --git a/client/index.html b/client/index.html
index fb1e66e..af15bbd 100644
--- a/client/index.html
+++ b/client/index.html
@@ -2,15 +2,13 @@
<html>
<head>
<link rel="stylesheet" href="css/main.css"/>
+ <script>var exports = {};</script>
</head>
<body>
- <img src="img/wall_atlas.png" id="atlas" style="display: none;"/>
- <div id="container" style="display: none;">
- <canvas id="canvas" style="display: none;"></canvas>
- </div>
+ <img src="img/atlas.png" id="atlas" style="display: none;"/>
+ <canvas id="canvas" style="display: none;"></canvas>
<style id="style"></style>
<div id="center">
- <div id="fps"></div>
<form id="join" autocomplete="off">
<input type="text" id="room_code" name="room_code" placeholder="Room Code">
<input type="text" id="name" name="name" placeholder="Player Name">
diff --git a/client/js/game.js b/client/js/game.js
deleted file mode 100644
index 3d8f6a7..0000000
--- a/client/js/game.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * @author tint
- * @template Data, Input
- */
-export class Game {
- /**
- * @param {number} history How many frames of history to keep in memory
- * @param {(data: Data, input: Input, frame: number) => Data} advance The function to apply game logic. For rollback to work properly, this must be a pure function, and can't mutate inputs.
- */
- constructor(history, advance) {
- this.historysize = history;
- this.advance = advance;
-
- /** @type {{data: Data, input: Input}[]} */
- this.history = [];
- this.historyStart = 0;
-
- // the game may have inputs from the "future"
- // (local input delay to make sure inputs play at the same time on all machines)
- // so the "present" isn't always the latest frame
- // the game loop should set this every frame
- this.currentFrame = 0;
- }
-
- startHistory(frame, data) {
- this.historyStart = frame;
- this.history = [{ data }];
- this.currentFrame = frame;
- }
-
- getHistory(frame) {
- return this.history[frame - this.historyStart];
- }
-
- getFrame() {
- return this.historyStart + this.history.length - 1;
- }
-
- getCurrentData() {
- const entry = this.history[this.history.length - 1];
- return entry && entry.data;
- }
-
- /**
- * Sets the input at a specific frame. If that frame is in history,
- * the game will be rewound, the input applied, and then fast-forwarded to the current head.
- * If the frame is ahead of the current latest frame, the game will be run until that frame.
- * @param {number} frame The time to apply the input at
- * @param {Input} input The input
- */
- setInput(frame, input) {
- this.editFrame(frame, index => {
- let past = this.history[index - 1];
- if(index === 0) {
- past = { data: undefined };
- }
- this.history[index] = {
- input,
- data: this.advance(past ? past.data : undefined, input, frame),
- };
- });
- }
-
- setData(frame, data) {
- this.editFrame(frame, index => {
- this.history[index] = {
- data,
- input: this.history[index] && this.history[index].input,
- }
- });
- }
-
- /**
- * @param {number} frame
- * @param {(index: number) => void} edit
- */
- editFrame(frame, edit) {
- const head = this.historyStart + this.history.length;
- if(frame < head) {
- if(frame < this.historyStart) {
- throw new Error("Tried to edit a past frame not in history:", frame);
- }
-
- edit(frame - this.historyStart);
- // fast forward back to the present with the new data
- for(let i = frame + 1; i < head; i++) {
- const past = this.history[i - this.historyStart - 1];
- this.history[i - this.historyStart].data = this.advance(
- past ? past.data : undefined,
- this.history[i - this.historyStart].input,
- i
- );
- }
- } else {
- // fast forward the inbetween frames with no input
- for(let i = head; i < frame; i++) {
- const entry = this.history[i - this.historyStart - 1];
- this.history[i - this.historyStart] = {
- input: undefined,
- data: this.advance(entry ? entry.data : undefined, undefined, i),
- };
- }
- edit(frame - this.historyStart);
- }
-
- while(this.history.length > this.historysize) {
- this.history.shift();
- this.historyStart++;
- }
- }
-}
diff --git a/client/js/gfx/graphics.js b/client/js/gfx/graphics.js
deleted file mode 100644
index 8ee4a6f..0000000
--- a/client/js/gfx/graphics.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import { Sprite } from './sprite.js'
-import { ItemType, Rotation } from '../logic.js'
-
-
-const draw_players = (data, players, sprites) => {
- for (let id of players) {
- let pos = data.players[id].pos
- sprites[id].move(pos[0], pos[1])
- switch (data.players[id].move_rot) {
- case Rotation.NORTH:
- sprites[id].rotate(270)
- break
- case Rotation.EAST:
- sprites[id].rotate(0)
- break
- case Rotation.SOUTH:
- sprites[id].rotate(90)
- break
- case Rotation.WEST:
- sprites[id].rotate(180)
- break
- }
-
- if (data.players[id].moving) {
- sprites[id].set_img("img/pac.gif")
- } else {
- sprites[id].set_img("img/pac.png")
- }
- }
-}
-
-const update_player_sprites = (data, players, sprites) => {
- for (const sprite of sprites) {
- if (sprite !== undefined) {
- sprite.destroy()
- }
- }
-
- let new_sprites = Array(players)
- new_sprites.fill(undefined)
-
- for (let id of players) {
- let sprite = new Sprite("img/pac.png", data.map)
- sprite.layer(3)
- sprite.resize(1,1)
- sprite.show()
- new_sprites[id] = sprite
- }
-
- return new_sprites
-}
-
-const create_map_dot = (data, x, y) => {
- let dot = new Sprite("img/dot.png", data.map)
- dot.move(x, y)
- dot.resize(.2,.2)
- dot.show()
- dot.type = ItemType.DOT
- return dot
-}
-
-const draw_sprites = (data, item_sprites) => {
- let items = data.map.items
-
- let to_remove = []
- // remove rendered but non existing items
- for (const item_key in item_sprites) {
-
- let sprite = item_sprites[item_key]
- if (!items[item_key]) {
- sprite.destroy()
- to_remove.push(item_key)
- }
-
- }
-
- for (const id of to_remove) {
- delete item_sprites[id]
- }
-
- // add not rendered sprites
- for (const item_key in items) {
-
- /** @type {import('../logic.js').Item} */
- let item = items[item_key]
- let sprite = item_sprites[item_key]
-
- if (sprite) {
- if (item.type === sprite.type)
- continue
- sprite.destroy()
- }
-
- switch (item.type) {
- case ItemType.DOT:
- sprite = create_map_dot(data, ...item.pos)
- break;
- }
-
- item_sprites[item_key] = sprite
- }
-
-}
-
-export const startGraphicsUpdater = () => {
-
- let player_sprites = []
- let item_sprites = {}
-
- /**
- * @type {(data: import("../logic.js").GameState) => void}
- */
- return (data) => {
-
- if (!data.map || !data.map.visible) return
-
- let players = Object.keys(data.players).filter(k => data.players[k] !== undefined)
-
- if (player_sprites.length !== players.length) {
- player_sprites = update_player_sprites(data, players, player_sprites)
- console.log("updating player sprites")
- }
-
- draw_sprites(data, item_sprites)
- draw_players(data, players, player_sprites)
-
- }
-}
diff --git a/client/js/gfx/map.js b/client/js/gfx/map.js
deleted file mode 100644
index a9ef0ad..0000000
--- a/client/js/gfx/map.js
+++ /dev/null
@@ -1,293 +0,0 @@
-import { ItemType, get_item_key } from "../logic.js";
-
-const update_style = (map, style) => {
- const css = `
- * {
- --scale: 100;
- --aspect: ${map.width/map.height};
- --scaleX: calc(var(--scale) * 1vw);
- --scaleY: calc(var(--scale) * 1vh);
- }
-
- #container {
- width: calc(var(--scaleY) * var(--aspect));
- height: var(--scaleY);
- margin-top: calc((100vh - var(--scaleY))/2);
- margin-left: calc(50vw - var(--scaleY)*var(--aspect)/2);
- position: relative;
- vertical-align: top;
- line-height: 0;
- }
-
- @media (max-aspect-ratio: ${map.width}/${map.height}) {
- #container {
- width: var(--scaleX);
- height: calc(var(--scaleX) / var(--aspect));
- margin-left: calc((100vw - var(--scaleX))/2);
- margin-top: calc(50vh - var(--scaleX)/var(--aspect)/2);
- }
- }`;
-
- style.innerHTML = css
-}
-
-const Direction = {
- EMPTY: 0,
- WALL_HZ: 1,
- WALL_VT: 2,
- TURN_Q1: 3,
- TURN_Q2: 4,
- TURN_Q3: 5,
- TURN_Q4: 6,
- TEE_NORTH: 7,
- TEE_EAST: 8,
- TEE_SOUTH: 9,
- TEE_WEST: 10,
- CROSS: 11,
- DOT: 12,
- WALL_END_NORTH: 13,
- WALL_END_SOUTH: 14,
- WALL_END_EAST: 15,
- WALL_END_WEST: 16
-}
-
-/**
- * @param {CanvasRenderingContext2D} context
- */
-const draw_tile = (context, x, y, w, type) => {
-
- let atlas_index, rotation;
- switch(type) {
- case Direction.EMPTY:
- return
- case Direction.WALL_HZ:
- atlas_index = [1, 1]
- rotation = 0
- break
- case Direction.WALL_VT:
- atlas_index = [1, 1]
- rotation = 90
- break
- case Direction.TURN_Q1:
- atlas_index = [2, 0]
- rotation = 0
- break
- case Direction.TURN_Q2:
- atlas_index = [2, 0]
- rotation = 270
- break
- case Direction.TURN_Q3:
- atlas_index = [2, 0]
- rotation = 180
- break
- case Direction.TURN_Q4:
- atlas_index = [2, 0]
- rotation = 90
- break
- case Direction.TEE_NORTH:
- atlas_index = [1, 0]
- rotation = 180
- break
- case Direction.TEE_EAST:
- atlas_index = [1, 0]
- rotation = 270
- break
- case Direction.TEE_SOUTH:
- atlas_index = [1, 0]
- rotation = 0
- break
- case Direction.TEE_WEST:
- atlas_index = [1, 0]
- rotation = 90
- break
- case Direction.CROSS:
- atlas_index = [0, 0]
- rotation = 0
- break
- case Direction.DOT:
- atlas_index = [2, 1]
- rotation = 0
- break
- case Direction.WALL_END_NORTH:
- atlas_index = [0, 1]
- rotation = 0
- break;
- case Direction.WALL_END_EAST:
- atlas_index = [0, 1]
- rotation = 90
- break;
- case Direction.WALL_END_SOUTH:
- atlas_index = [0, 1]
- rotation = 180
- break;
- case Direction.WALL_END_WEST:
- atlas_index = [0, 1]
- rotation = 270
- break;
- }
-
- let atlas = document.getElementById("atlas")
- context.save()
- context.translate((x+.5)*w, (y+.5)*w)
- context.rotate(rotation * Math.PI / 180)
- context.drawImage(atlas, atlas_index[0]*w, atlas_index[1]*w, w, w, -w/2, -w/2, w, w)
- context.restore()
-}
-
-const get_point = (width, height, data, x, y) => {
- if (x < 0 || x >= width || y < 0 || y >= height) {
- return 0
- } else {
- return data[y * width + x]
- }
-}
-
-const gen_walls = (width, height, data) => {
-
- let walls = Array(width * height)
- for (let y = 0; y < height; y++) {
- for (let x = 0; x < width; x++) {
-
- let north = get_point(width, height, data, x, y-1) == 1
- let south = get_point(width, height, data, x, y+1) == 1
- let east = get_point(width, height, data, x+1, y) == 1
- let west = get_point(width, height, data, x-1, y) == 1
- let current = get_point(width, height, data, x, y) == 1
-
- let point = Direction.EMPTY
-
- if (!current) {
- walls[y * width + x] = point
- continue
- }
-
- if (north && south && east && west) {
- point = Direction.CROSS
- } else if (east && west && north) {
- point = Direction.TEE_NORTH
- } else if (east && west && south) {
- point = Direction.TEE_SOUTH
- } else if (north && south && east) {
- point = Direction.TEE_EAST
- } else if (north && south && west) {
- point = Direction.TEE_WEST
- } else if (east && west) {
- point = Direction.WALL_HZ
- } else if (north && south) {
- point = Direction.WALL_VT
- } else if (west && south) {
- point = Direction.TURN_Q1
- } else if (south && east) {
- point = Direction.TURN_Q2
- } else if (east && north) {
- point = Direction.TURN_Q3
- } else if (north && west) {
- point = Direction.TURN_Q4
- } else if (north) {
- point = Direction.WALL_END_NORTH
- } else if (east) {
- point = Direction.WALL_END_EAST
- } else if (south) {
- point = Direction.WALL_END_SOUTH
- } else if (west) {
- point = Direction.WALL_END_WEST
- } else {
- point = Direction.DOT
- }
-
- walls[y * width + x] = point
-
- }
- }
-
- return walls
-}
-
-const update_canvas = (map, canvas) => {
- let context = canvas.getContext("2d");
- for (let y = 0; y < map.height; y++) {
- for (let x = 0; x < map.width; x++) {
- draw_tile(context, x, y, map.tile_width, map.walls[y * map.width + x])
- }
- }
-}
-
-const gen_items = (map) => {
-
- let width = map.width
- let height = map.height
-
- let items = {}
-
- for (let y = 0; y < height; y++) {
- for (let x = 0; x < width; x++) {
- let tile = get_point(width, height, map.data, x, y)
- if (tile != 0) continue
-
- let item_key = get_item_key(x, y, width)
- items[item_key] = {type: ItemType.DOT, pos: [x, y]}
-
- let tile_south = get_point(width, height, map.data, x, y + 1)
- if (tile_south == 0) {
- item_key = get_item_key(x, y + .5, width)
- items[item_key] = {type: ItemType.DOT, pos: [x, y + .5]}
- }
-
- let tile_east = get_point(width, height, map.data, x + 1, y)
- if (tile_east == 0) {
- item_key = get_item_key(x + .5, y, width)
- items[item_key] = {type: ItemType.DOT, pos: [x + .5, y]}
- }
- }
- }
-
- return items
-}
-
-export class Map {
-
- static data
- static walls
-
- constructor(width, height, data) {
-
- this.width = width
- this.height = height
- this.data = data
- this.walls = gen_walls(width, height, data)
- this.items = gen_items(this)
- this.visible = false
- this.tile_width = 32
-
- canvas.width = this.width * this.tile_width
- canvas.height = this.height * this.tile_width
-
-
-
- }
-
- show() {
-
- let container = document.getElementById("container")
- container.style.display = ""
-
- let canvas = document.getElementById("canvas")
- canvas.style.display = ""
-
- let style = document.getElementById("style")
-
- update_canvas(this, canvas)
- update_style(this, style)
-
- this.visible = true
- }
-
- hide() {
-
- let canvas = document.getElementById("canvas")
- canvas.style.display = "none"
- container.style.display = "none"
-
- this.visible = false
- }
-}
diff --git a/client/js/gfx/sprite.js b/client/js/gfx/sprite.js
deleted file mode 100644
index 07360f1..0000000
--- a/client/js/gfx/sprite.js
+++ /dev/null
@@ -1,71 +0,0 @@
-export class Sprite {
-
- constructor(image_src, map) {
- this.element = document.createElement("img")
- this.element.src = image_src
- this.element.className = "sprite"
- document.getElementById("container").appendChild(this.element)
-
- this.map = map
- this.x = 0
- this.y = 0
- this.w = 1
- this.h = 1
- this.z = 1
- this.d = 0
- this.hide()
- }
-
- #update_pos() {
- let width = 100 / this.map.width * this.w
- let height = 100 / this.map.height * this.h
- let left = 100 / this.map.width * (this.x + (1 - this.w) / 2)
- let top = 100 / this.map.height * (this.y + (1 - this.h) / 2)
-
- this.element.style.width = `${width}%`
- this.element.style.height = `${height}%`
- this.element.style.left = `${left}%`
- this.element.style.top = `${top}%`
- this.element.style.transform = `rotate(${this.d}deg)`
- this.element.style.zIndex = `${this.z}`
- }
-
- move(x, y) {
- this.x = x
- this.y = y
- this.#update_pos()
- }
-
- resize(w, h) {
- this.w = w
- this.h = h
- this.#update_pos()
- }
-
- layer(z) {
- this.z = z
- this.#update_pos()
- }
-
- set_img(src) {
- this.element.src = src
- }
-
- rotate(d) {
- this.d = d
- this.#update_pos()
- }
-
- hide() {
- this.element.style.display = "none"
- }
-
- show() {
- this.element.style.display = "initial"
- }
-
- destroy() {
- this.element.remove()
- }
-
-}
diff --git a/client/js/input.js b/client/js/input.js
deleted file mode 100644
index 3928e91..0000000
--- a/client/js/input.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Key } from "./logic.js";
-
-const debug_style = document.body.appendChild(document.createElement("style"))
-var debug_enabled = false
-
-export function startInputListener() {
- let dir = 0;
- let start = false;
-
- // document.getElementById("start").onclick = e => {
- // e.preventDefault();
- // start = true;
- // }
-
- let keymap = {
- "KeyW": Key.UP,
- "KeyA": Key.LEFT,
- "KeyS": Key.DOWN,
- "KeyD": Key.RIGHT,
- };
-
- document.getElementById("start").onclick = function() {
- start = true
- }
-
- window.addEventListener("keydown", ev => {
- if(ev.repeat) {
- return;
- }
- if(!(ev.code in keymap)) {
- if (ev.code === "KeyB") {
- debug_enabled = !debug_enabled
- if (debug_enabled) {
- debug_style.innerHTML = ` \
- #container img { \
- box-shadow: 0 0 1px red inset; \
- } \
- #container .sprite { \
- box-shadow: 0 0 1px white inset; \
- } \
- `
- } else {
- debug_style.innerHTML = ""
- }
- }
- return;
- }
- dir = keymap[ev.code];
- });
-
- window.addEventListener("keyup", ev => {
- if (ev.repeat) {
- return;
- }
- if (!(ev.code in keymap)) {
- return
- }
- if (dir == keymap[ev.code]) {
- dir = Key.NOTHING
- }
- })
-
- let last = {
- dir: 0,
- }
-
- return function() {
-
- if(dir === last.dir && !start) {
- return;
- }
-
- last = {
- dir,
- };
-
- let s = start;
- start = false;
- return {
- dir,
- start: s,
- }
- }
-}
diff --git a/client/js/logic.js b/client/js/logic.js
deleted file mode 100644
index 395e4f0..0000000
--- a/client/js/logic.js
+++ /dev/null
@@ -1,335 +0,0 @@
-import { Map } from "./gfx/map.js";
-
-// enum
-export const Key = {
- NOTHING: undefined,
- UP: 1,
- DOWN: 2,
- LEFT: 3,
- RIGHT: 4,
-}
-
-// enum
-export const Rotation = {
- NOTHING: undefined,
- NORTH: 1,
- EAST: 2,
- SOUTH: 3,
- WEST: 4
-}
-
-export const ItemType = {
- DOT: 1
-}
-
-/**
- * @typedef {[number, number]} Vec2
- *
- * @typedef {{[key: number]: Key}} InputMap
- *
- * @typedef {{pos: Vec2, move_rot: Rotation, input_rot: Rotation, moving: boolean, name?: string}} Player
- * @typedef {{start: boolean, key: Key, name?: string}} PlayerInput
- * @typedef {{players: {[key: number]: PlayerInput}, added?: number[], removed?: number[] }} Input
- *
- * @typedef {{[key: number]: Player}} Players
- *
- * @typedef {{width: number, height: number, data: number[]}} Map
- *
- * @typedef {{type: ItemType, pos: Vec2}} Item
- *
- * @typedef {{
- * started: boolean,
- * input: InputMap,
- * players: Players,
- * map: Map,
- * items_removed: Item[]
- * }} GameState
- */
-
-/** @type {GameState} */
-export const initState = {
- started: false,
- input: {},
- players: [],
- map: {}
-}
-
-let last = Date.now()
-let fps_div = document.getElementById("fps")
-let frameCount = 0
-
-export function advance(
- pastData = initState,
- input = { players: {} },
- frame
-) {
- let data = processInput(pastData, input, frame);
-
- if (frameCount == 60) {
- frameCount = 0
- let now = Date.now()
- let fps = (now-last)/1000*60
- fps_div.innerHTML = fps.toFixed(2);
- last = now
- }
-
- frameCount++
-
- return data;
-}
-
-/**
- * @param {GameState} pastData
- * @param {Input} input
- * @param {number} frame
- */
-function processInput(pastData, input) {
-
- /** @type {GameState} */
- let data = structuredClone(pastData)
-
- let startPressed = false;
-
- for(const added of input.added || []) {
- if (data.started || Object.keys(data.players).length >= 4) continue;
- console.log("added", added);
- data.input[added] ||= {
- pos: [1, 1],
- input_rot: Rotation.EAST,
- mov_rot: Rotation.EAST,
- moving: false
- };
- if(!(added in data.players)) {
- data.players[added] = structuredClone(data.input[added])
- }
- }
-
- for(const id in input.players) {
- if(!input.players[id]) {
- continue;
- }
-
- if(id in data.players && input.players[id].name !== undefined) {
- let name = input.players[id].name;
- name = name.substring(0, 16);
-
- data.players[id] = {
- ...data.players[id],
- name,
- };
-
- }
-
- startPressed ||= input.players[id].start;
- data.input[id] = input.players[id].dir
- }
-
- const player_display = document.getElementById("players")
- for (const id in data.players) {
- if (data.players[id] === null) continue
- let name = data.players[id].name
- if (name === undefined) continue
-
- let element_id = 'span' + id
-
- let element = player_display.children[element_id]
- if (element === null || element === undefined) {
- let span = document.createElement("span")
- span.innerHTML = `[${id}] ${name}`
- span.id = element_id
- player_display.appendChild(span)
- }
- }
-
- if (startPressed && !data.started) {
- init_map(data)
- data.started ||= startPressed;
- }
-
- if (data.started) {
- update_players(data)
- }
-
-
- for(const removed of input.removed || []) {
- console.log("removed", removed);
- delete data.input[removed];
- delete data.players[removed];
-
- let element_id = 'span' + removed
- let element = document.getElementById(element_id)
- if (element !== null && element !== undefined) element.remove()
- }
-
-
- return data
-}
-
-const init_map = (data) => {
-
- document.getElementById("lobby").style.display = "none"
-
- let width = 21
- let height = 21
- let m_data = [
- 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
- 1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,
- 1,0,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,1,1,0,1,
- 1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,
- 1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,
- 1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,
- 1,0,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,0,1,
- 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
- 1,1,1,0,1,0,1,0,1,1,1,1,1,0,1,0,1,0,1,1,1,
- 1,0,0,0,0,0,1,0,1,2,2,2,1,0,1,0,0,0,0,0,1,
- 1,0,1,1,1,0,1,0,1,2,2,2,1,0,1,0,1,1,1,0,1,
- 1,0,0,0,0,0,1,0,1,2,2,2,1,0,1,0,0,0,0,0,1,
- 1,1,1,0,1,0,1,0,1,1,2,1,1,0,1,0,1,0,1,1,1,
- 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
- 1,0,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,0,1,
- 1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,
- 1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,
- 1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,
- 1,0,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,1,1,0,1,
- 1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,
- 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
- ]
-
- data.map = new Map(width, height, m_data)
- data.map.show()
-}
-
-const MOVE_SPEED = .1
-
-const round_pos = (pos) => {
- return [Math.round(pos[0]), Math.round(pos[1])]
-}
-
-const is_stable_pos = (pos) => {
- let rpos = round_pos(pos)
- return Math.abs(rpos[0] - pos[0]) < .05 && Math.abs(rpos[1] - pos[1]) < .05
-}
-
-const get_tile = (map, pos, ox, oy) => {
- let x = Math.round(pos[0] + ox)
- let y = Math.round(pos[1] + oy)
- if (x < 0 || x >= map.width || y < 0 || y >= map.height) return 1
- return map.data[y * map.width + x]
-}
-
-const get_tile_with_rot = (map, pos, rot) => {
- let collider = 1
- switch(rot) {
- case Rotation.NORTH:
- collider = get_tile(map, pos, 0, -.51)
- break
- case Rotation.SOUTH:
- collider = get_tile(map, pos, 0, .51)
- break
- case Rotation.WEST:
- collider = get_tile(map, pos, -.51, 0)
- break
- case Rotation.EAST:
- collider = get_tile(map, pos, .51, 0)
- break
- }
- return collider
-}
-
-const get_rot = (dir) => {
- switch (dir) {
- case Key.UP: return Rotation.NORTH
- case Key.DOWN: return Rotation.SOUTH
- case Key.LEFT: return Rotation.WEST
- case Key.RIGHT: return Rotation.EAST
- case Key.NOTHING: return Rotation.NOTHING
- }
-}
-
-const increment_pos = (pos, rot, speed) => {
- switch (rot) {
- case Rotation.NORTH:
- pos[1] -= speed
- break
- case Rotation.SOUTH:
- pos[1] += speed
- break
- case Rotation.WEST:
- pos[0] -= speed
- break
- case Rotation.EAST:
- pos[0] += speed
- break
- }
-}
-
-export const get_item_key = (x, y, w) => {
- let nx = Math.round(x * 2)
- let ny = Math.round(y * 2)
- let key = ny * w * 2 + nx
- return key
-}
-
-const ceil_half = (n) => {
- return Math.ceil(n*2)/2
-}
-
-const floor_half = (n) => {
- return Math.floor(n*2)/2
-}
-
-/**
- * @param {GameState} data
- */
-const update_players = (data) => {
-
- for(const id in data.input) { // move players
- if(!(id in data.players)) {
- console.log("what. player undefined???", id);
- continue;
- }
-
- let input_key = data.input[id]
- let input_dir = get_rot(input_key)
- let move_dir = data.players[id].move_rot
- let current_pos = data.players[id].pos
-
- let tile_in_front_with_turn = get_tile_with_rot(data.map, current_pos, input_dir)
- if (tile_in_front_with_turn == 1 || tile_in_front_with_turn == 2) {
- input_dir = Rotation.NOTHING
- }
-
- let turning = input_dir != Key.NOTHING && input_dir != move_dir
-
- data.players[id].input_rot = input_dir
-
- if (turning && is_stable_pos(current_pos)) {
- current_pos = round_pos(current_pos)
- data.players[id].move_rot = input_dir
- move_dir = input_dir
- }
-
- let move_pos = structuredClone(current_pos)
- increment_pos(move_pos, move_dir, MOVE_SPEED)
-
- let tile_in_front = get_tile_with_rot(data.map, current_pos, move_dir)
- if (tile_in_front != 1 && tile_in_front != 2) {
- data.players[id].pos = move_pos
- data.players[id].moving = true
- } else {
- data.players[id].pos = round_pos(current_pos)
- data.players[id].moving = false
- }
-
- // eat items
- let pos = data.players[id].pos
- for (let x = ceil_half(pos[0]-.5); x <= floor_half(pos[0]+.5); x += .5) {
- for (let y = ceil_half(pos[1]-.5); y <= floor_half(pos[1]+.5); y += .5) {
- let item_key = get_item_key(x, y, data.map.width)
- delete data.map.items[item_key]
- }
- }
-
- }
-
-}
diff --git a/client/js/main.js b/client/js/main.js
index 1d241b7..c80f8f3 100644
--- a/client/js/main.js
+++ b/client/js/main.js
@@ -1,113 +1,59 @@
-import { Game } from "./game.js";
-import { startInputListener } from "./input.js";
-import { multiplayer } from "./multiplayer.js";
-import { advance, initState } from "./logic.js";
-import { startGraphicsUpdater } from "./gfx/graphics.js";
-
-const join = document.getElementById("join")
-const lobby = document.getElementById("lobby")
-lobby.style.display = "none"
-
-join.onsubmit = async function(event) {
- event.preventDefault()
-
- const room_code = this.elements.room_code.value.trim()
- const player_name = this.elements.name.value.trim()
-
- if (room_code == '') {
- alert('Please enter a room code')
- return
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+import { Game } from "./net/game.js";
+import { InitialState, onLogic } from "./logic/logic.js";
+import { startGraphicsUpdater } from "./renderer.js";
+import { GameKeyMap, Key } from "./types.js";
+const join = document.getElementById("join");
+const lobby = document.getElementById("lobby");
+lobby.style.display = "none";
+join.onsubmit = function (event) {
+ return __awaiter(this, void 0, void 0, function* () {
+ event.preventDefault();
+ const room_code = this.elements.room_code.value.trim();
+ const player_name = this.elements.name.value.trim();
+ if (room_code == '') {
+ alert('Please enter a room code');
+ return;
+ }
+ if (player_name == '') {
+ alert('Please enter a player name');
+ return;
+ }
+ join.style.display = "none";
+ startGame(room_code, player_name);
+ });
+};
+const updateGraphics = startGraphicsUpdater();
+const onLoad = (startData) => {
+ if (startData.data.started) {
+ alert('Room has already started');
+ return false;
}
-
- if (player_name == '') {
- alert('Please enter a player name')
- return
+ let players = Object.values(startData.data.players).filter(p => { return p !== null && p.name !== undefined; });
+ if (players.length >= 4) {
+ alert('Room is full');
+ return false;
}
-
- join.style.display = "none"
-
- startGame(room_code, player_name)
-}
-
-function startGame(code, name) {
-
- const game = window.game = new Game(3000, advance);
- const fps = 60;
- let delay = 3;
-
- // set up the game up
- // const ui = document.getElementById("ui");
- // ui.style.display = "block";
-
- multiplayer(
- game,
- code,
- (startFrame, latency, player, update, ping, desyncCheck) => {
- // document.getElementById("desynccheck").onclick = function(e) {
- // e.preventDefault();
- // this.textContent = "check for desyncs: checking...";
- // desyncCheck(game.currentFrame - 5)
- // .then(res => {
- // this.textContent = "check for desyncs: " + (res ? "desync" : "no desync");
- // });
- // }
- console.log("started game at frame", startFrame);
- window.desyncCheck = () => desyncCheck(game.currentFrame - 5);
-
- lobby.style.display = ""
-
- let startTs = performance.now() - latency;
- let lastFrame = startFrame;
- update({
- name,
- }, startFrame + 1);
-
- const getInput = startInputListener();
- const updateGraphics = startGraphicsUpdater();
-
- const start_data = game.getHistory(startFrame)
- if (start_data.data.started) {
- alert('Room has already started')
- return false
- }
-
- let players = Object.values(start_data.data.players).filter(p => { return p !== null && p.name !== undefined })
- if (players.length >= 4) {
- alert('Room is full')
- return false
- }
-
- // main game loop
- let lastTs = performance.now();
- function f(ts) {
- const frame = Math.floor((ts - startTs) / 1000 * fps) + startFrame;
- if(frame !== lastFrame) { // update input once per frame, regardless of the display refresh rate
- lastFrame = frame;
-
- // gather input
- const input = getInput();
-
- // apply input
- update(input, frame + delay);
- }
-
- // set up graphics
- game.currentFrame = frame;
- const data = game.getHistory(frame);
- updateGraphics(data ? data.data : initState);
- lastTs = ts;
-
- requestAnimationFrame(f);
- }
-
- requestAnimationFrame(f);
- if(startFrame === -1) {
- update({
- name,
- }, 0);
- }
-
- return true
- }
- );
-}
+ lobby.style.display = "";
+ return true;
+};
+const onFrame = (data, frame) => {
+ updateGraphics(data ? data.data : InitialState, frame);
+};
+const startGame = (code, name) => {
+ const game = new Game(3000);
+ game.start(code, GameKeyMap, onLoad, onFrame, onLogic, {
+ start: false,
+ key: Key.NOTHING,
+ name
+ });
+};
+//# sourceMappingURL=main.js.map \ No newline at end of file
diff --git a/client/src/logic/items.ts b/client/src/logic/items.ts
new file mode 100644
index 0000000..5f8a38e
--- /dev/null
+++ b/client/src/logic/items.ts
@@ -0,0 +1,41 @@
+import { getMap, getItemKey } from "../map.js"
+import { GameState, Map, Player } from "../types.js"
+
+const ceilHalf = (n: number): number => {
+ return Math.ceil(n*2)/2
+}
+
+const floorHalf = (n: number): number => {
+ return Math.floor(n*2)/2
+}
+
+const eatItems = (data: GameState, map: Map, player: Player) => {
+
+ let pos = player.pos
+
+ for (let x = ceilHalf(pos.x-.5); x <= floorHalf(pos.x+.5); x += .5) {
+ for (let y = ceilHalf(pos.y-.5); y <= floorHalf(pos.y+.5); y += .5) {
+ let item_key = getItemKey(x, y, map.width)
+ delete data.items[item_key]
+ }
+ }
+}
+
+export const updateItems = (data: GameState) => {
+
+ let map = getMap(data.mapId)
+ if (!map) return
+
+ for(const id in data.input) {
+
+ const player = data.players[id]
+
+ if(!player) {
+ continue;
+ }
+
+ eatItems(data, map, player)
+
+ }
+
+}
diff --git a/client/src/logic/logic.ts b/client/src/logic/logic.ts
new file mode 100644
index 0000000..1cca2b7
--- /dev/null
+++ b/client/src/logic/logic.ts
@@ -0,0 +1,80 @@
+import { genItems, loadMap, getMap } from "../map.js";
+import { updatePlayers } from "./players.js"
+import { updateUI } from "./ui.js"
+import { updateMovement } from "./movement.js"
+import { updateItems } from "./items.js"
+import { GameState, Input } from "../types.js";
+
+
+export const InitialState: GameState = {
+ started: false,
+ input: {},
+ players: [],
+ items: {},
+ mapId: undefined
+}
+
+export const onLogic = (
+ pastData: GameState = InitialState,
+ input: Input = { players: {} },
+ _frame: number
+) => {
+
+ let data = structuredClone(pastData)
+
+ let startPressed = updatePlayers(data, input);
+
+ if (data.started) {
+ updateMovement(data)
+ updateItems(data)
+ } else {
+ updateUI(data)
+ }
+
+ if (startPressed && !data.started) {
+ initMap(data)
+ data.started = true;
+ }
+
+ return data
+
+}
+
+const initMap = (data: GameState) => {
+
+ document.getElementById("lobby").style.display = "none"
+
+ data.mapId = 0
+
+ if (getMap(0)) return
+
+ let width = 21
+ let height = 21
+ let m_data = [
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,
+ 1,0,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,1,1,0,1,
+ 1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,
+ 1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,
+ 1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,
+ 1,0,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,0,1,
+ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
+ 1,1,1,0,1,0,1,0,1,1,1,1,1,0,1,0,1,0,1,1,1,
+ 1,0,0,0,0,0,1,0,1,2,2,2,1,0,1,0,0,0,0,0,1,
+ 1,0,1,1,1,0,1,0,1,2,2,2,1,0,1,0,1,1,1,0,1,
+ 1,0,0,0,0,0,1,0,1,2,2,2,1,0,1,0,0,0,0,0,1,
+ 1,1,1,0,1,0,1,0,1,1,2,1,1,0,1,0,1,0,1,1,1,
+ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
+ 1,0,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,0,1,
+ 1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,
+ 1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,
+ 1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,
+ 1,0,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,1,1,0,1,
+ 1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
+ ]
+
+ loadMap(width, height, m_data) // cursed temp thing
+ data.items = genItems(getMap(0))
+}
+
diff --git a/client/src/logic/movement.ts b/client/src/logic/movement.ts
new file mode 100644
index 0000000..40cfc3e
--- /dev/null
+++ b/client/src/logic/movement.ts
@@ -0,0 +1,142 @@
+import { getMap } from "../map.js"
+import { Vec2, Map, Rotation, Key, Player, GameState } from "../types.js"
+
+const MOVE_SPEED = .1
+
+const roundPos = (pos: Vec2): Vec2 => {
+ return {x: Math.round(pos.x), y: Math.round(pos.y)}
+}
+
+const isStablePos = (pos: Vec2): boolean => {
+ let rpos = roundPos(pos)
+ return Math.abs(rpos.x - pos.x) < .05 && Math.abs(rpos.y - pos.y) < .05
+}
+
+const getTile = (
+ map: Map,
+ pos: Vec2,
+ ox: number,
+ oy: number
+): number => {
+ let x = Math.round(pos.x + ox)
+ let y = Math.round(pos.y + oy)
+ if (x < 0 || x >= map.width || y < 0 || y >= map.height) return 1
+ return map.data[y * map.width + x]
+}
+
+const getTileFrontWithRot = (
+ map: Map,
+ pos: Vec2,
+ rot: Rotation
+): number => {
+ let collider = 1
+ switch(rot) {
+ case Rotation.NORTH:
+ collider = getTile(map, pos, 0, -.51)
+ break
+ case Rotation.SOUTH:
+ collider = getTile(map, pos, 0, .51)
+ break
+ case Rotation.WEST:
+ collider = getTile(map, pos, -.51, 0)
+ break
+ case Rotation.EAST:
+ collider = getTile(map, pos, .51, 0)
+ break
+ }
+ return collider
+}
+
+const getRot = (key: Key): Rotation => {
+ switch (key) {
+ case Key.UP: return Rotation.NORTH
+ case Key.DOWN: return Rotation.SOUTH
+ case Key.LEFT: return Rotation.WEST
+ case Key.RIGHT: return Rotation.EAST
+ case Key.NOTHING: return Rotation.NOTHING
+ }
+}
+
+const incrementPos = (
+ pos: Vec2,
+ rot: Rotation,
+ speed: number
+): void => {
+ switch (rot) {
+ case Rotation.NORTH:
+ pos.y -= speed
+ break
+ case Rotation.SOUTH:
+ pos.y += speed
+ break
+ case Rotation.WEST:
+ pos.x -= speed
+ break
+ case Rotation.EAST:
+ pos.y += speed
+ break
+ }
+}
+
+let i = 0
+
+const updateMovementForPlayer = (
+ map: Map,
+ player: Player,
+ inputKey: Key
+) => {
+
+ let inputRot = getRot(inputKey)
+ let moveRot = player.moveRotation
+ let currentPosition = player.pos
+
+ let turningFrontTile = getTileFrontWithRot(map, currentPosition, inputRot)
+ if (turningFrontTile == 1 || turningFrontTile == 2) {
+ inputRot = Rotation.NOTHING
+ }
+
+ let turning = inputRot != Rotation.NOTHING && inputRot != moveRot
+
+ player.inputRotation = inputRot
+
+ if (turning && isStablePos(currentPosition)) {
+ currentPosition = roundPos(currentPosition)
+ player.moveRotation = inputRot
+ moveRot = inputRot
+ }
+
+ let movePos = structuredClone(currentPosition)
+ incrementPos(movePos, moveRot, MOVE_SPEED)
+
+ let frontTile = getTileFrontWithRot(map, currentPosition, moveRot)
+ if (frontTile != 1 && frontTile != 2) {
+ player.pos = movePos
+ player.moving = true
+ } else {
+ player.pos = roundPos(currentPosition)
+ player.moving = false
+ }
+
+
+}
+
+export const updateMovement = (data: GameState) => {
+
+ let map = getMap(data.mapId)
+ if (!map) return
+
+ for (const id in data.players) {
+
+ const player = data.players[id]
+
+ if(!player) {
+ continue
+ }
+
+ let inputKey = data.input[id]
+
+ updateMovementForPlayer(map, player, inputKey)
+
+ }
+
+}
diff --git a/client/src/logic/players.ts b/client/src/logic/players.ts
new file mode 100644
index 0000000..ebe469f
--- /dev/null
+++ b/client/src/logic/players.ts
@@ -0,0 +1,79 @@
+import { GameState, Input, Key, Rotation } from "../types.js"
+
+const canPlayerJoin = (data: GameState) => {
+
+ // lobby has already started
+ if (data.started) {
+ return false
+ }
+
+ // lobby full
+ if (Object.keys(data.players).length >= 4) {
+ return false
+ }
+
+ return true
+
+}
+
+export const updatePlayers = (data: GameState, input: Input) => {
+
+ let startPressed = false;
+
+ for(const added of input.added || []) {
+
+ if (!canPlayerJoin(data)) {
+ continue
+ }
+
+ console.log("added", added);
+
+ data.input[added] = Key.NOTHING
+
+ data.players[added] ||= {
+ pos: {x: 1, y: 1},
+ inputRotation: Rotation.EAST,
+ moveRotation: Rotation.EAST,
+ moving: false,
+ };
+
+ }
+
+ for(const id in input.players) {
+
+ if(!input.players[id]) {
+ continue;
+ }
+
+ if(id in data.players && input.players[id].name !== undefined) {
+
+ let name = input.players[id].name;
+ name = name.substring(0, 16);
+
+ data.players[id] = {
+ ...data.players[id],
+ name,
+ };
+
+ }
+
+ startPressed ||= input.players[id].start;
+ if (input.players[id].key)
+ data.input[id] = input.players[id].key
+
+ }
+
+ for(const removed of input.removed || []) {
+ console.log("removed", removed);
+ delete data.input[removed];
+ delete data.players[removed];
+
+ let element_id = 'span' + removed
+ let element = document.getElementById(element_id)
+ if (element !== null && element !== undefined) element.remove()
+ }
+
+ return startPressed
+
+}
+
diff --git a/client/src/logic/ui.ts b/client/src/logic/ui.ts
new file mode 100644
index 0000000..5706843
--- /dev/null
+++ b/client/src/logic/ui.ts
@@ -0,0 +1,32 @@
+import { GameState } from "../types.js"
+
+export const updateUI = (data: GameState) => {
+
+ const player_display = document.getElementById("players")
+
+ for (const id in data.players) {
+
+ const player = data.players[id]
+
+ if (!player) {
+ continue
+ }
+
+ let name = player.name
+
+ if (!name) {
+ continue
+ }
+
+ let element_id = 'span' + id
+ let element = player_display.children[element_id]
+
+ if (!element) {
+ let span = document.createElement("span")
+ span.textContent = `[${id}] ${name}`
+ span.id = element_id
+ player_display.appendChild(span)
+ }
+ }
+
+}
diff --git a/client/src/main.ts b/client/src/main.ts
new file mode 100644
index 0000000..a6cc3ba
--- /dev/null
+++ b/client/src/main.ts
@@ -0,0 +1,75 @@
+import { Game } from "./net/game.js";
+import { InitialState, onLogic } from "./logic/logic.js";
+import { startGraphicsUpdater } from "./renderer.js";
+import { GameKeyMap, Frame, Key } from "./types.js";
+
+const join = document.getElementById("join")
+const lobby = document.getElementById("lobby")
+lobby.style.display = "none"
+
+join.onsubmit = async function(event) {
+ event.preventDefault()
+
+ const room_code = this.elements.room_code.value.trim()
+ const player_name = this.elements.name.value.trim()
+
+ if (room_code == '') {
+ alert('Please enter a room code')
+ return
+ }
+
+ if (player_name == '') {
+ alert('Please enter a player name')
+ return
+ }
+
+ join.style.display = "none"
+
+ startGame(room_code, player_name)
+}
+
+const updateGraphics = startGraphicsUpdater()
+
+const onLoad = (startData: Frame) => {
+
+ if (startData.data.started) {
+ alert('Room has already started')
+ return false
+ }
+
+ let players = Object.values(startData.data.players).filter(p => { return p !== null && p.name !== undefined })
+ if (players.length >= 4) {
+ alert('Room is full')
+ return false
+ }
+
+ lobby.style.display = ""
+
+ return true
+}
+
+const onFrame = (data: Frame, frame: number) => {
+
+ updateGraphics(data ? data.data : InitialState, frame);
+
+}
+
+
+const startGame = (code: string, name: string) => {
+
+ const game = new Game(3000)
+
+ game.start(
+ code,
+ GameKeyMap,
+ onLoad,
+ onFrame,
+ onLogic,
+ {
+ start: false,
+ key: Key.NOTHING,
+ name
+ }
+ )
+
+}
diff --git a/client/src/map.ts b/client/src/map.ts
new file mode 100644
index 0000000..e6fab9d
--- /dev/null
+++ b/client/src/map.ts
@@ -0,0 +1,151 @@
+import { Wall, ItemType, Map, Maps, Items } from "./types.js"
+
+export const getItemKey = (
+ x: number,
+ y: number,
+ w: number
+): number => {
+ let nx = Math.round(x * 2)
+ let ny = Math.round(y * 2)
+ let key = ny * w * 2 + nx
+ return key
+}
+
+const getPoint = (
+ width: number,
+ height: number,
+ data: number[],
+ x: number,
+ y: number
+): number => {
+ if (x < 0 || x >= width || y < 0 || y >= height) {
+ return 0
+ } else {
+ return data[y * width + x]
+ }
+}
+
+const genWalls = (
+ width: number,
+ height: number,
+ data: number[]
+): number[] => {
+
+ let walls = Array(width * height)
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+
+ let north = getPoint(width, height, data, x, y-1) == 1
+ let south = getPoint(width, height, data, x, y+1) == 1
+ let east = getPoint(width, height, data, x+1, y) == 1
+ let west = getPoint(width, height, data, x-1, y) == 1
+ let current = getPoint(width, height, data, x, y) == 1
+
+ let point = Wall.EMPTY
+
+ if (!current) {
+ walls[y * width + x] = point
+ continue
+ }
+
+ if (north && south && east && west) {
+ point = Wall.CROSS
+ } else if (east && west && north) {
+ point = Wall.TEE_NORTH
+ } else if (east && west && south) {
+ point = Wall.TEE_SOUTH
+ } else if (north && south && east) {
+ point = Wall.TEE_EAST
+ } else if (north && south && west) {
+ point = Wall.TEE_WEST
+ } else if (east && west) {
+ point = Wall.WALL_HZ
+ } else if (north && south) {
+ point = Wall.WALL_VT
+ } else if (west && south) {
+ point = Wall.TURN_Q1
+ } else if (south && east) {
+ point = Wall.TURN_Q2
+ } else if (east && north) {
+ point = Wall.TURN_Q3
+ } else if (north && west) {
+ point = Wall.TURN_Q4
+ } else if (north) {
+ point = Wall.WALL_END_NORTH
+ } else if (east) {
+ point = Wall.WALL_END_EAST
+ } else if (south) {
+ point = Wall.WALL_END_SOUTH
+ } else if (west) {
+ point = Wall.WALL_END_WEST
+ } else {
+ point = Wall.DOT
+ }
+
+ walls[y * width + x] = point
+
+ }
+ }
+
+ return walls
+}
+
+export const genItems = (map: Map): Items => {
+
+ let width = map.width
+ let height = map.height
+ let data = map.data
+
+ let items: Items = {}
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ let tile = getPoint(width, height, data, x, y)
+ if (tile != 0) continue
+
+ let item_key = getItemKey(x, y, width)
+ items[item_key] = {type: ItemType.DOT, pos: {x, y}}
+
+ let tile_south = getPoint(width, height, data, x, y + 1)
+ if (tile_south == 0) {
+ item_key = getItemKey(x, y + .5, width)
+ items[item_key] = {type: ItemType.DOT, pos: {x, y: y + .5}}
+ }
+
+ let tile_east = getPoint(width, height, data, x + 1, y)
+ if (tile_east == 0) {
+ item_key = getItemKey(x + .5, y, width)
+ items[item_key] = {type: ItemType.DOT, pos: {x: x + .5, y}}
+ }
+ }
+ }
+
+ return items
+}
+
+let mapData: Maps = {}
+let id: number = 0
+
+export const loadMap = (
+ width: number,
+ height: number,
+ data: number[]
+): number => {
+
+ let mapId = id++
+
+ mapData[mapId] = {
+ data: structuredClone(data),
+ walls: genWalls(width, height, data),
+ width,
+ height,
+ id: mapId
+ }
+
+ return mapId
+}
+
+export const getMap = (mapId: number): Map | undefined => {
+ if (mapId == undefined) return undefined
+ return mapData[mapId]
+}
diff --git a/client/src/net/game.ts b/client/src/net/game.ts
new file mode 100644
index 0000000..c8e5991
--- /dev/null
+++ b/client/src/net/game.ts
@@ -0,0 +1,186 @@
+import { Frame, GameState, Input, Key, KeyMap, PlayerInput } from "../types.js";
+import { startInputListener } from "./input.js";
+import { multiplayer } from "./multiplayer.js";
+
+/**
+ * @author tint
+ * @template Data, Input
+ */
+export class Game {
+
+ historysize: number
+ history: Frame[]
+ historyStart: number
+ currentFrame: number
+ advance: (pastData: GameState, input: Input, frame: number) => GameState
+
+ constructor(history: number) {
+ this.historysize = history;
+
+ this.history = [];
+ this.historyStart = 0;
+
+ // the game may have inputs from the "future"
+ // (local input delay to make sure inputs play at the same time on all machines)
+ // so the "present" isn't always the latest frame
+ // the game loop should set this every frame
+ this.currentFrame = 0;
+ }
+
+ startHistory(frame: number, data: GameState) {
+ this.historyStart = frame;
+ this.history = [{ data, input: { players: {} }}];
+ this.currentFrame = frame;
+ }
+
+ getHistory(frame: number): Frame {
+ return this.history[frame - this.historyStart];
+ }
+
+ getFrame(): number {
+ return this.historyStart + this.history.length - 1;
+ }
+
+ getCurrentData(): GameState {
+ const entry = this.history[this.history.length - 1];
+ return entry && entry.data;
+ }
+
+ /**
+ * Sets the input at a specific frame. If that frame is in history,
+ * the game will be rewound, the input applied, and then fast-forwarded to the current head.
+ * If the frame is ahead of the current latest frame, the game will be run until that frame.
+ */
+ setInput(frame: number, input: Input) {
+ console.log('input', frame, input)
+ this.editFrame(frame, (index: number): void => {
+ let past = this.history[index - 1];
+ if(index === 0) {
+ past = { data: undefined, input: undefined };
+ }
+ this.history[index] = {
+ input,
+ data: this.advance(past ? past.data : undefined, input, frame),
+ };
+ });
+ }
+
+ setData(frame: number, data: GameState) {
+ console.log('data', frame, data)
+ this.editFrame(frame, (index: number): void => {
+ this.history[index] = {
+ data,
+ input: this.history[index] && this.history[index].input,
+ }
+ });
+ }
+
+ editFrame(frame: number, edit: (index: number) => void) {
+ const head = this.historyStart + this.history.length;
+ if(frame < head) {
+ if(frame < this.historyStart) {
+ throw new Error("Tried to edit a past frame not in history: " + frame);
+ }
+
+ edit(frame - this.historyStart);
+ // fast forward back to the present with the new data
+ for(let i = frame + 1; i < head; i++) {
+ const past = this.history[i - this.historyStart - 1];
+ this.history[i - this.historyStart].data = this.advance(
+ past ? past.data : undefined,
+ this.history[i - this.historyStart].input,
+ i
+ );
+ }
+ } else {
+ // fast forward the inbetween frames with no input
+ for(let i = head; i < frame; i++) {
+ const entry = this.history[i - this.historyStart - 1];
+ this.history[i - this.historyStart] = {
+ input: undefined,
+ data: this.advance(entry ? entry.data : undefined, undefined, i),
+ };
+ }
+ edit(frame - this.historyStart);
+ }
+
+ while(this.history.length > this.historysize) {
+ this.history.shift();
+ this.historyStart++;
+ }
+ }
+
+ start (
+ code: string,
+ keymap: KeyMap,
+ onLoad: (startFrame: Frame) => boolean,
+ onFrame: (data: Frame, frame: number) => void,
+ onLogic: (pastData: GameState, input: Input, frame: number) => GameState,
+ data: PlayerInput = { start: false, key: Key.NOTHING }
+ ): void {
+
+ const fps = 60;
+ let delay = 3;
+
+ this.advance = onLogic
+
+ const onStart = (
+ startFrame: number,
+ latency: number,
+ _connection: number,
+ update: (input: PlayerInput, frame: number) => void,
+ _ping: () => Promise<number>,
+ _desyncCheck: (frame: number) => Promise<boolean>,
+ ) => {
+ console.log("started game at frame", startFrame);
+ // window.desyncCheck = () => desyncCheck(this.currentFrame - 5);
+
+ let startTs = performance.now() - latency;
+ let lastFrame = startFrame;
+ update(data, startFrame + 1);
+
+ let getInput = startInputListener(keymap)
+
+ const startData = this.getHistory(startFrame)
+
+ if (!onLoad(startData)) return false
+
+ let lastTs = performance.now();
+
+ let loop = (ts: number) => {
+
+ const frame = Math.floor((ts - startTs) / 1000 * fps) + startFrame;
+
+ if(frame !== lastFrame) { // update input once per frame, regardless of the display refresh rate
+ lastFrame = frame;
+
+ // gather input
+ const input: PlayerInput = getInput();
+
+ // apply input
+ update(input, frame + delay);
+ }
+
+ this.currentFrame = frame
+ const data = this.getHistory(frame)
+
+ onFrame(data, frame)
+
+ lastTs = ts
+
+ requestAnimationFrame(loop)
+ }
+
+ requestAnimationFrame(loop)
+
+ if(startFrame === -1) {
+ update(data, 0);
+ }
+
+ return true
+ }
+
+ multiplayer(this, code, onStart)
+
+ }
+}
diff --git a/client/src/net/input.ts b/client/src/net/input.ts
new file mode 100644
index 0000000..75be3e6
--- /dev/null
+++ b/client/src/net/input.ts
@@ -0,0 +1,70 @@
+import { Key, KeyMap, PlayerInput } from "../types.js"
+
+let pressed = {}
+
+const updateRecent = (keymap: KeyMap) => {
+ let max = -1
+ let key = undefined
+ for (let code in pressed) {
+ let weight = pressed[code]
+ if (weight < max) continue
+ max = weight
+ key = keymap[code]
+ }
+
+ return key
+}
+
+export const startInputListener = (keymap: KeyMap): () => PlayerInput => {
+ let key: Key = Key.NOTHING;
+ let start = false;
+
+ document.getElementById("start").onclick = function() {
+ start = true
+ }
+
+ window.addEventListener("keydown", ev => {
+ if(ev.repeat) {
+ return;
+ }
+ if(!(ev.code in keymap)) {
+ return;
+ }
+ pressed[ev.code] = Object.keys(pressed).length
+ key = updateRecent(keymap)
+ });
+
+ window.addEventListener("keyup", ev => {
+ if (ev.repeat) {
+ return;
+ }
+ if (!(ev.code in keymap)) {
+ return
+ }
+ delete pressed[ev.code]
+ key = updateRecent(keymap)
+ })
+
+ let last = {
+ key: Key.NOTHING,
+ }
+
+ return (): PlayerInput => {
+
+ if(key === last.key && !start) {
+ return;
+ }
+
+ last = {
+ key,
+ };
+
+ let s = start;
+ start = false;
+
+ return {
+ key,
+ start: s,
+ }
+ }
+}
diff --git a/client/js/multiplayer.js b/client/src/net/multiplayer.ts
index eec1c7d..e9f3057 100644
--- a/client/js/multiplayer.js
+++ b/client/src/net/multiplayer.ts
@@ -2,52 +2,39 @@
* @author tint
*/
-/**
- * @template Input
- * @typedef {{
- * added?: number[],
- * removed?: number[],
- * players: {
- * [conn: number]: Input
- * },
- * }} GameInput
- */
+import { GameState, Message, PlayerInput } from "../types.js";
+import { Game } from "./game";
-/**
- * @template Data, Input
- * @param {import("./game.js").Game<Data, GameInput<Input>>} game
- * @param {string} code
- * @param {(
- * startFrame: number,
- * latency: number,
- * connection: number,
- * update: (input: Input, frame: number) => void,
- * ping: () => Promise<number>,
- * desyncCheck: () => Promise<boolean>,
- * ) => void} onStart
- * Called when the game is in a ready state with the current frame
- * (or -1 if you're starting the room), an estimate of how many milliseconds have elapsed since that frame was sent,
- * and your connection ID
- */
-export function multiplayer(game, code, onStart) {
- const url = new URL("api/join/" + encodeURIComponent(code), window.location);
+export function multiplayer(
+ game: Game,
+ code: string,
+ onStart: (
+ startFrame: number,
+ latency: number,
+ connection: number,
+ update: (input: PlayerInput, frame: number) => void,
+ ping: () => Promise<number>,
+ desyncCheck: (frame: number) => Promise<boolean>,
+ ) => boolean
+) {
+ const url = new URL("api/join/" + encodeURIComponent(code), window.location.toString());
url.protocol = url.protocol.replace("http", "ws");
const socket = new WebSocket(url);
- let requestStateTime;
+ let requestStateTime: number;
let hasState = false;
- let connectionId;
+ let connectionId: number;
let cachedInputs = [];
let connections = [];
- let pingPromise;
+ let pingPromise: (type: Promise<number>) => void;
- function send(obj) {
+ function send(obj: any) {
socket.send(JSON.stringify(obj));
}
- function applyInput(input) {
+ function applyInput(input: Message) {
let prev = game.getHistory(input.frame);
let newInput = prev && prev.input ? {...prev.input} : { players: {} };
@@ -74,11 +61,10 @@ export function multiplayer(game, code, onStart) {
newInput.removed = (newInput.removed || []).concat([input.removed]);
}
}
-
game.setInput(input.frame, newInput);
}
- function flushCachedInputs(latency = 0) {
+ function flushCachedInputs(latency = 0): boolean {
for(const input of cachedInputs) {
// only care about inputs after the new state
if(input.frame <= game.historyStart) {
@@ -91,15 +77,14 @@ export function multiplayer(game, code, onStart) {
return onStart(game.getFrame(), latency, connectionId, update, ping, desyncCheck);
}
- function update(input, frame) {
+ function update(input: PlayerInput, frame: number) {
if(input === undefined) { // used to update the game locally
if(hasState) {
- applyInput({
- frame,
- });
+ applyInput({})
}
return;
}
+
const data = {
type: "input",
data: input,
@@ -118,17 +103,17 @@ export function multiplayer(game, code, onStart) {
type: "ping",
frame: Math.max(0, game.currentFrame),
});
- const frame = await new Promise(r => pingPromise = r);
+ const frame: number = await new Promise(r => pingPromise = r);
return game.currentFrame - frame;
}
- async function desyncCheck(frame) {
+ async function desyncCheck(frame: number): Promise<boolean> {
const history = game.getHistory(frame);
if(!history) {
console.error("tried to check for desyncs on a frame not in memory", frame);
return true;
}
- const localstate = history.data;
+ // const localstate = history.data;
const proms = connections
.filter(n => n !== connectionId)
.map(connection => {
@@ -138,7 +123,7 @@ export function multiplayer(game, code, onStart) {
connection,
});
return new Promise(r => {
- stateRequests[frame + "," + connection] = state => {
+ stateRequests[frame + "," + connection] = (state: GameState) => {
r({
state,
connection,
@@ -150,11 +135,11 @@ export function multiplayer(game, code, onStart) {
if(!proms.length) {
return false; // this is the only connection, no check necessary
}
- const states = await Promise.all(proms);
- if(!states.every(({ state }) => objeq(localstate, state))) {
- console.error("desync! remote states:", states, "local state:", localstate);
- return true;
- }
+ // const states = await Promise.all(proms);
+ // if(!states.every(({ state }) => objeq(localstate, state))) {
+ // console.error("desync! remote states:", states, "local state:", localstate);
+ // return true;
+ // }
return false;
}
@@ -203,7 +188,7 @@ export function multiplayer(game, code, onStart) {
case "requeststate":
// wait until there's some state to send
const startTime = performance.now();
- function check() {
+ const check = () => {
if(performance.now() - startTime > 5000) {
return; // give up after 5s
}
@@ -272,7 +257,7 @@ export function multiplayer(game, code, onStart) {
}
// compare two plain objects (things that can be JSON.stringified)
-function objeq(a, b) {
+function objeq(a: any, b: any) {
if(typeof(a) !== typeof(b)) {
return false;
}
diff --git a/client/src/renderer.ts b/client/src/renderer.ts
new file mode 100644
index 0000000..c7bbbc2
--- /dev/null
+++ b/client/src/renderer.ts
@@ -0,0 +1,320 @@
+import { getMap } from "./map.js";
+import { Items, Players, Rotation, ItemType, Map, Wall, GameState } from "./types.js";
+
+const ATLAS_TILE_WIDTH = 32
+
+const update_style = (width: number, height: number) => {
+
+ let style = document.getElementById("style")
+
+ const css = `
+ * {
+ --scale: 100;
+ --aspect: ${width/height};
+ --scaleX: calc(var(--scale) * 1vw);
+ --scaleY: calc(var(--scale) * 1vh);
+ }
+
+ #canvas {
+ width: calc(var(--scaleY) * var(--aspect));
+ height: var(--scaleY);
+ margin-top: calc((100vh - var(--scaleY))/2);
+ margin-left: calc(50vw - var(--scaleY)*var(--aspect)/2);
+ position: relative;
+ vertical-align: top;
+ line-height: 0;
+ }
+
+ @media (max-aspect-ratio: ${width}/${height}) {
+ #canvas {
+ width: var(--scaleX);
+ height: calc(var(--scaleX) / var(--aspect));
+ margin-left: calc((100vw - var(--scaleX))/2);
+ margin-top: calc(50vh - var(--scaleX)/var(--aspect)/2);
+ }
+ }`;
+
+ style.innerHTML = css
+}
+
+const draw_sprite = (
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ y: number,
+ width: number,
+ atlas: CanvasImageSource,
+ atlas_index: [number, number],
+ atlas_tile_width: number,
+ rotation: Rotation
+) => {
+ ctx.save()
+ ctx.translate(
+ (x + 0.5) * ATLAS_TILE_WIDTH,
+ (y + 0.5) * ATLAS_TILE_WIDTH
+ )
+ ctx.rotate(rotation * Math.PI / 180)
+ ctx.drawImage(
+ atlas,
+ atlas_index[0] * atlas_tile_width,
+ atlas_index[1] * atlas_tile_width,
+ atlas_tile_width,
+ atlas_tile_width,
+ -width * ATLAS_TILE_WIDTH / 2,
+ -width * ATLAS_TILE_WIDTH / 2,
+ width * ATLAS_TILE_WIDTH,
+ width * ATLAS_TILE_WIDTH
+ )
+ ctx.restore()
+}
+
+const draw_players = (
+ ctx: CanvasRenderingContext2D,
+ atlas: CanvasImageSource,
+ players: Players,
+ frame: number
+) => {
+
+ let atlas_frames: [number, number][] = [
+ [0, 2],
+ [1, 2],
+ [2, 2],
+ [0, 3],
+ [1, 3],
+ [0, 3],
+ [2, 2],
+ [1, 2],
+ ]
+
+ for (let id in players) {
+
+ let player = players[id]
+ if (!player) continue
+
+ let atlas_index = atlas_frames[0]
+ if (player.moving) {
+ atlas_index = atlas_frames[Math.floor(frame / 2) % atlas_frames.length]
+ }
+
+ let rotation: number
+ switch (player.moveRotation) {
+ case Rotation.NORTH:
+ rotation = 270
+ break
+ case Rotation.SOUTH:
+ rotation = 90
+ break
+ case Rotation.WEST:
+ rotation = 180
+ break
+ case Rotation.EAST:
+ default:
+ rotation = 0
+ break
+ }
+
+ draw_sprite (
+ ctx,
+ player.pos.x,
+ player.pos.y,
+ 1,
+ atlas,
+ atlas_index,
+ ATLAS_TILE_WIDTH,
+ rotation
+ )
+ }
+}
+
+const draw_items = (
+ ctx: CanvasRenderingContext2D,
+ atlas: CanvasImageSource,
+ items: Items
+) => {
+
+ for (let item_key in items) {
+
+ let item = items[item_key]
+ if (!item) continue
+
+ let width: number, atlas_index: [number, number]
+ switch (item.type) {
+ case ItemType.DOT:
+ width = .2,
+ atlas_index = [2, 3]
+ break
+ default:
+ continue
+ }
+
+ draw_sprite (
+ ctx,
+ item.pos.x,
+ item.pos.y,
+ width,
+ atlas,
+ atlas_index,
+ ATLAS_TILE_WIDTH,
+ 0
+ )
+
+ }
+
+}
+
+const draw_map_canvas = (
+ ctx: CanvasRenderingContext2D,
+ atlas: CanvasImageSource,
+ map: Map
+) => {
+
+ for (let y = 0; y < map.height; y++) {
+ for (let x = 0; x < map.width; x++) {
+
+ let wall_type = map.walls[y * map.width + x]
+
+ let atlas_index: [number, number], rotation: number;
+ switch(wall_type) {
+ case Wall.EMPTY:
+ continue
+ case Wall.WALL_HZ:
+ atlas_index = [1, 1]
+ rotation = 0
+ break
+ case Wall.WALL_VT:
+ atlas_index = [1, 1]
+ rotation = 90
+ break
+ case Wall.TURN_Q1:
+ atlas_index = [2, 0]
+ rotation = 0
+ break
+ case Wall.TURN_Q2:
+ atlas_index = [2, 0]
+ rotation = 270
+ break
+ case Wall.TURN_Q3:
+ atlas_index = [2, 0]
+ rotation = 180
+ break
+ case Wall.TURN_Q4:
+ atlas_index = [2, 0]
+ rotation = 90
+ break
+ case Wall.TEE_NORTH:
+ atlas_index = [1, 0]
+ rotation = 180
+ break
+ case Wall.TEE_EAST:
+ atlas_index = [1, 0]
+ rotation = 270
+ break
+ case Wall.TEE_SOUTH:
+ atlas_index = [1, 0]
+ rotation = 0
+ break
+ case Wall.TEE_WEST:
+ atlas_index = [1, 0]
+ rotation = 90
+ break
+ case Wall.CROSS:
+ atlas_index = [0, 0]
+ rotation = 0
+ break
+ case Wall.DOT:
+ atlas_index = [2, 1]
+ rotation = 0
+ break
+ case Wall.WALL_END_NORTH:
+ atlas_index = [0, 1]
+ rotation = 0
+ break;
+ case Wall.WALL_END_EAST:
+ atlas_index = [0, 1]
+ rotation = 90
+ break;
+ case Wall.WALL_END_SOUTH:
+ atlas_index = [0, 1]
+ rotation = 180
+ break;
+ case Wall.WALL_END_WEST:
+ atlas_index = [0, 1]
+ rotation = 270
+ break;
+ }
+
+ draw_sprite (
+ ctx,
+ x,
+ y,
+ 1,
+ atlas,
+ atlas_index,
+ ATLAS_TILE_WIDTH,
+ rotation
+ )
+ }
+ }
+
+}
+
+let map_canvas = document.createElement("canvas")
+const draw_map = (
+ ctx: CanvasRenderingContext2D,
+ atlas: CanvasImageSource,
+ map: Map,
+ last: number | undefined
+) => {
+
+ if (map.id !== last) {
+ map_canvas.width = map.width * ATLAS_TILE_WIDTH
+ map_canvas.height = map.height * ATLAS_TILE_WIDTH
+
+ let map_ctx = map_canvas.getContext("2d")
+ draw_map_canvas(map_ctx, atlas, map)
+ }
+
+ ctx.drawImage (
+ map_canvas,
+ 0,
+ 0
+ )
+
+}
+
+let last_map_drawn: number | undefined
+export const startGraphicsUpdater = () => {
+
+ let canvas = document.getElementById("canvas") as HTMLCanvasElement
+ let atlas = document.getElementById("atlas") as HTMLImageElement
+
+ /**
+ * @param {import("./logic").GameState} data
+ */
+ return (
+ data: GameState,
+ frame: number
+ ) => {
+
+ let map = getMap(data.mapId)
+
+ if (!map) return
+
+ if (map.id !== last_map_drawn) {
+ canvas.style.display = ""
+ canvas.width = map.width * ATLAS_TILE_WIDTH
+ canvas.height = map.height * ATLAS_TILE_WIDTH
+ }
+
+ let ctx = canvas.getContext("2d")
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
+
+ draw_map(ctx, atlas, map, last_map_drawn)
+ draw_items(ctx, atlas, data.items)
+ draw_players(ctx, atlas, data.players, frame)
+ update_style(map.width, map.height)
+
+ last_map_drawn = map.id
+
+ }
+
+}
diff --git a/client/src/types.ts b/client/src/types.ts
new file mode 100644
index 0000000..df0c8b1
--- /dev/null
+++ b/client/src/types.ts
@@ -0,0 +1,131 @@
+
+export enum Wall {
+ EMPTY,
+ WALL_HZ,
+ WALL_VT,
+ TURN_Q1,
+ TURN_Q2,
+ TURN_Q3,
+ TURN_Q4,
+ TEE_NORTH,
+ TEE_EAST,
+ TEE_SOUTH,
+ TEE_WEST,
+ CROSS,
+ DOT,
+ WALL_END_NORTH,
+ WALL_END_SOUTH,
+ WALL_END_EAST,
+ WALL_END_WEST
+}
+
+export enum ItemType {
+ DOT
+}
+
+export enum Key {
+ NOTHING,
+ UP,
+ DOWN,
+ LEFT,
+ RIGHT
+}
+
+export type KeyMap = {
+ [key: string]: Key
+}
+
+export const GameKeyMap = {
+ "KeyW": Key.UP,
+ "KeyA": Key.LEFT,
+ "KeyS": Key.DOWN,
+ "KeyD": Key.RIGHT,
+}
+
+export enum Rotation {
+ NOTHING,
+ NORTH,
+ EAST,
+ SOUTH,
+ WEST
+}
+
+export type Vec2 = {
+ x: number,
+ y: number
+}
+
+export type InputMap = {
+ [key: number]: Key
+}
+
+export type Player = {
+ pos: Vec2,
+ moveRotation: Rotation,
+ inputRotation: Rotation,
+ name?: string,
+ moving: boolean
+}
+
+export type PlayerInput = {
+ start: boolean,
+ key: Key,
+ name?: string
+}
+
+export type Input = {
+ players: {[key: number]: PlayerInput},
+ added?: number[],
+ removed?: number[],
+}
+
+export type Message = {
+ type?: string;
+ connections?: number[],
+ added?: number,
+ removed?: number,
+ id?: number,
+ frame?: number,
+ data?: any,
+ connection?: number,
+ state?: GameState,
+ error?: string
+}
+
+export type Players = {
+ [key: number]: Player
+}
+
+export type Item = {
+ type: ItemType,
+ pos: Vec2
+}
+
+export type Items = {
+ [key: number]: Item
+}
+
+export type Map = {
+ data: number[],
+ walls: number[],
+ width: number,
+ height: number,
+ id: number
+}
+
+export type Maps = {
+ [key: number]: Map
+}
+
+export type GameState = {
+ started: boolean,
+ input: InputMap,
+ players: Players,
+ items: Items,
+ mapId: number | undefined
+}
+
+export type Frame = {
+ data: GameState,
+ input: Input
+}
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000..4850356
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "noImplicitAny": false,
+ "sourceMap": true,
+ "outDir": "./js",
+ "allowJs": false,
+ "module": "es6",
+ "target": "es6"
+ },
+ "include": ["./**/*"]
+}
diff --git a/Cargo.lock b/server/Cargo.lock
index 6b0a690..6b0a690 100644
--- a/Cargo.lock
+++ b/server/Cargo.lock
diff --git a/Cargo.toml b/server/Cargo.toml
index 98ffd29..98ffd29 100644
--- a/Cargo.toml
+++ b/server/Cargo.toml
diff --git a/src/main.rs b/server/src/main.rs
index 34783ac..34783ac 100644
--- a/src/main.rs
+++ b/server/src/main.rs
diff --git a/src/room/handle.rs b/server/src/room/handle.rs
index d397c70..d397c70 100644
--- a/src/room/handle.rs
+++ b/server/src/room/handle.rs
diff --git a/src/room/messages.rs b/server/src/room/messages.rs
index 72958a6..72958a6 100644
--- a/src/room/messages.rs
+++ b/server/src/room/messages.rs
diff --git a/src/room/mod.rs b/server/src/room/mod.rs
index 8b3d8c2..8b3d8c2 100644
--- a/src/room/mod.rs
+++ b/server/src/room/mod.rs
diff --git a/src/room/websocket.rs b/server/src/room/websocket.rs
index 50a4537..50a4537 100644
--- a/src/room/websocket.rs
+++ b/server/src/room/websocket.rs
diff --git a/src/rooms.rs b/server/src/rooms.rs
index c8199d1..c8199d1 100644
--- a/src/rooms.rs
+++ b/server/src/rooms.rs
diff --git a/src/routes.rs b/server/src/routes.rs
index 1fa16c7..a8c96a3 100644
--- a/src/routes.rs
+++ b/server/src/routes.rs
@@ -16,7 +16,7 @@ pub fn routes() -> Router {
.route("/api/check", get(|| async {"ok"}))
.route("/api/exists/:code", get(game_exists))
.route("/api/join/:code", get(game_join))
- .nest_service("/", ServeDir::new("client"))
+ .nest_service("/", ServeDir::new("../client"))
.layer(Extension(room_server))
}