ts
This commit is contained in:
parent
9e43bcbfe9
commit
44334fc385
37 changed files with 1416 additions and 1211 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
server/target
|
||||||
|
client/js
|
||||||
|
|
|
@ -71,24 +71,3 @@ input {
|
||||||
padding: .25rem;
|
padding: .25rem;
|
||||||
margin-bottom: .215rem;
|
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;
|
|
||||||
}
|
|
||||||
|
|
BIN
client/img/atlas.png
Normal file
BIN
client/img/atlas.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1,004 B |
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
|
@ -2,15 +2,13 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="css/main.css"/>
|
<link rel="stylesheet" href="css/main.css"/>
|
||||||
|
<script>var exports = {};</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<img src="img/wall_atlas.png" id="atlas" style="display: none;"/>
|
<img src="img/atlas.png" id="atlas" style="display: none;"/>
|
||||||
<div id="container" style="display: none;">
|
|
||||||
<canvas id="canvas" style="display: none;"></canvas>
|
<canvas id="canvas" style="display: none;"></canvas>
|
||||||
</div>
|
|
||||||
<style id="style"></style>
|
<style id="style"></style>
|
||||||
<div id="center">
|
<div id="center">
|
||||||
<div id="fps"></div>
|
|
||||||
<form id="join" autocomplete="off">
|
<form id="join" autocomplete="off">
|
||||||
<input type="text" id="room_code" name="room_code" placeholder="Room Code">
|
<input type="text" id="room_code" name="room_code" placeholder="Room Code">
|
||||||
<input type="text" id="name" name="name" placeholder="Player Name">
|
<input type="text" id="name" name="name" placeholder="Player Name">
|
||||||
|
|
|
@ -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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,113 +1,59 @@
|
||||||
import { Game } from "./game.js";
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
import { startInputListener } from "./input.js";
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
import { multiplayer } from "./multiplayer.js";
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
import { advance, initState } from "./logic.js";
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
import { startGraphicsUpdater } from "./gfx/graphics.js";
|
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); }
|
||||||
const join = document.getElementById("join")
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
const lobby = document.getElementById("lobby")
|
});
|
||||||
lobby.style.display = "none"
|
};
|
||||||
|
import { Game } from "./net/game.js";
|
||||||
join.onsubmit = async function(event) {
|
import { InitialState, onLogic } from "./logic/logic.js";
|
||||||
event.preventDefault()
|
import { startGraphicsUpdater } from "./renderer.js";
|
||||||
|
import { GameKeyMap, Key } from "./types.js";
|
||||||
const room_code = this.elements.room_code.value.trim()
|
const join = document.getElementById("join");
|
||||||
const player_name = this.elements.name.value.trim()
|
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 == '') {
|
if (room_code == '') {
|
||||||
alert('Please enter a room code')
|
alert('Please enter a room code');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player_name == '') {
|
if (player_name == '') {
|
||||||
alert('Please enter a player name')
|
alert('Please enter a player name');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
join.style.display = "none";
|
||||||
join.style.display = "none"
|
startGame(room_code, player_name);
|
||||||
|
});
|
||||||
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 updateGraphics = startGraphicsUpdater();
|
||||||
|
const onLoad = (startData) => {
|
||||||
const start_data = game.getHistory(startFrame)
|
if (startData.data.started) {
|
||||||
if (start_data.data.started) {
|
alert('Room has already started');
|
||||||
alert('Room has already started')
|
return false;
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
let players = Object.values(startData.data.players).filter(p => { return p !== null && p.name !== undefined; });
|
||||||
let players = Object.values(start_data.data.players).filter(p => { return p !== null && p.name !== undefined })
|
|
||||||
if (players.length >= 4) {
|
if (players.length >= 4) {
|
||||||
alert('Room is full')
|
alert('Room is full');
|
||||||
return false
|
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
|
41
client/src/logic/items.ts
Normal file
41
client/src/logic/items.ts
Normal file
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
80
client/src/logic/logic.ts
Normal file
80
client/src/logic/logic.ts
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
|
142
client/src/logic/movement.ts
Normal file
142
client/src/logic/movement.ts
Normal file
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
79
client/src/logic/players.ts
Normal file
79
client/src/logic/players.ts
Normal file
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
32
client/src/logic/ui.ts
Normal file
32
client/src/logic/ui.ts
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
75
client/src/main.ts
Normal file
75
client/src/main.ts
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
151
client/src/map.ts
Normal file
151
client/src/map.ts
Normal file
|
@ -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]
|
||||||
|
}
|
186
client/src/net/game.ts
Normal file
186
client/src/net/game.ts
Normal file
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
70
client/src/net/input.ts
Normal file
70
client/src/net/input.ts
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,52 +2,39 @@
|
||||||
* @author tint
|
* @author tint
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
import { GameState, Message, PlayerInput } from "../types.js";
|
||||||
* @template Input
|
import { Game } from "./game";
|
||||||
* @typedef {{
|
|
||||||
* added?: number[],
|
|
||||||
* removed?: number[],
|
|
||||||
* players: {
|
|
||||||
* [conn: number]: Input
|
|
||||||
* },
|
|
||||||
* }} GameInput
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
export function multiplayer(
|
||||||
* @template Data, Input
|
game: Game,
|
||||||
* @param {import("./game.js").Game<Data, GameInput<Input>>} game
|
code: string,
|
||||||
* @param {string} code
|
onStart: (
|
||||||
* @param {(
|
startFrame: number,
|
||||||
* startFrame: number,
|
latency: number,
|
||||||
* latency: number,
|
connection: number,
|
||||||
* connection: number,
|
update: (input: PlayerInput, frame: number) => void,
|
||||||
* update: (input: Input, frame: number) => void,
|
ping: () => Promise<number>,
|
||||||
* ping: () => Promise<number>,
|
desyncCheck: (frame: number) => Promise<boolean>,
|
||||||
* desyncCheck: () => Promise<boolean>,
|
) => boolean
|
||||||
* ) => void} onStart
|
) {
|
||||||
* Called when the game is in a ready state with the current frame
|
const url = new URL("api/join/" + encodeURIComponent(code), window.location.toString());
|
||||||
* (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);
|
|
||||||
url.protocol = url.protocol.replace("http", "ws");
|
url.protocol = url.protocol.replace("http", "ws");
|
||||||
|
|
||||||
const socket = new WebSocket(url);
|
const socket = new WebSocket(url);
|
||||||
|
|
||||||
let requestStateTime;
|
let requestStateTime: number;
|
||||||
let hasState = false;
|
let hasState = false;
|
||||||
let connectionId;
|
let connectionId: number;
|
||||||
let cachedInputs = [];
|
let cachedInputs = [];
|
||||||
let connections = [];
|
let connections = [];
|
||||||
|
|
||||||
let pingPromise;
|
let pingPromise: (type: Promise<number>) => void;
|
||||||
|
|
||||||
function send(obj) {
|
function send(obj: any) {
|
||||||
socket.send(JSON.stringify(obj));
|
socket.send(JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyInput(input) {
|
function applyInput(input: Message) {
|
||||||
let prev = game.getHistory(input.frame);
|
let prev = game.getHistory(input.frame);
|
||||||
let newInput = prev && prev.input ? {...prev.input} : { players: {} };
|
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]);
|
newInput.removed = (newInput.removed || []).concat([input.removed]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
game.setInput(input.frame, newInput);
|
game.setInput(input.frame, newInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushCachedInputs(latency = 0) {
|
function flushCachedInputs(latency = 0): boolean {
|
||||||
for(const input of cachedInputs) {
|
for(const input of cachedInputs) {
|
||||||
// only care about inputs after the new state
|
// only care about inputs after the new state
|
||||||
if(input.frame <= game.historyStart) {
|
if(input.frame <= game.historyStart) {
|
||||||
|
@ -91,15 +77,14 @@ export function multiplayer(game, code, onStart) {
|
||||||
return onStart(game.getFrame(), latency, connectionId, update, ping, desyncCheck);
|
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(input === undefined) { // used to update the game locally
|
||||||
if(hasState) {
|
if(hasState) {
|
||||||
applyInput({
|
applyInput({})
|
||||||
frame,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
type: "input",
|
type: "input",
|
||||||
data: input,
|
data: input,
|
||||||
|
@ -118,17 +103,17 @@ export function multiplayer(game, code, onStart) {
|
||||||
type: "ping",
|
type: "ping",
|
||||||
frame: Math.max(0, game.currentFrame),
|
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;
|
return game.currentFrame - frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function desyncCheck(frame) {
|
async function desyncCheck(frame: number): Promise<boolean> {
|
||||||
const history = game.getHistory(frame);
|
const history = game.getHistory(frame);
|
||||||
if(!history) {
|
if(!history) {
|
||||||
console.error("tried to check for desyncs on a frame not in memory", frame);
|
console.error("tried to check for desyncs on a frame not in memory", frame);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const localstate = history.data;
|
// const localstate = history.data;
|
||||||
const proms = connections
|
const proms = connections
|
||||||
.filter(n => n !== connectionId)
|
.filter(n => n !== connectionId)
|
||||||
.map(connection => {
|
.map(connection => {
|
||||||
|
@ -138,7 +123,7 @@ export function multiplayer(game, code, onStart) {
|
||||||
connection,
|
connection,
|
||||||
});
|
});
|
||||||
return new Promise(r => {
|
return new Promise(r => {
|
||||||
stateRequests[frame + "," + connection] = state => {
|
stateRequests[frame + "," + connection] = (state: GameState) => {
|
||||||
r({
|
r({
|
||||||
state,
|
state,
|
||||||
connection,
|
connection,
|
||||||
|
@ -150,11 +135,11 @@ export function multiplayer(game, code, onStart) {
|
||||||
if(!proms.length) {
|
if(!proms.length) {
|
||||||
return false; // this is the only connection, no check necessary
|
return false; // this is the only connection, no check necessary
|
||||||
}
|
}
|
||||||
const states = await Promise.all(proms);
|
// const states = await Promise.all(proms);
|
||||||
if(!states.every(({ state }) => objeq(localstate, state))) {
|
// if(!states.every(({ state }) => objeq(localstate, state))) {
|
||||||
console.error("desync! remote states:", states, "local state:", localstate);
|
// console.error("desync! remote states:", states, "local state:", localstate);
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +188,7 @@ export function multiplayer(game, code, onStart) {
|
||||||
case "requeststate":
|
case "requeststate":
|
||||||
// wait until there's some state to send
|
// wait until there's some state to send
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
function check() {
|
const check = () => {
|
||||||
if(performance.now() - startTime > 5000) {
|
if(performance.now() - startTime > 5000) {
|
||||||
return; // give up after 5s
|
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)
|
// 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)) {
|
if(typeof(a) !== typeof(b)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
320
client/src/renderer.ts
Normal file
320
client/src/renderer.ts
Normal file
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
131
client/src/types.ts
Normal file
131
client/src/types.ts
Normal file
|
@ -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
|
||||||
|
}
|
11
client/tsconfig.json
Normal file
11
client/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./js",
|
||||||
|
"allowJs": false,
|
||||||
|
"module": "es6",
|
||||||
|
"target": "es6"
|
||||||
|
},
|
||||||
|
"include": ["./**/*"]
|
||||||
|
}
|
0
Cargo.lock → server/Cargo.lock
generated
0
Cargo.lock → server/Cargo.lock
generated
|
@ -16,7 +16,7 @@ pub fn routes() -> Router {
|
||||||
.route("/api/check", get(|| async {"ok"}))
|
.route("/api/check", get(|| async {"ok"}))
|
||||||
.route("/api/exists/:code", get(game_exists))
|
.route("/api/exists/:code", get(game_exists))
|
||||||
.route("/api/join/:code", get(game_join))
|
.route("/api/join/:code", get(game_join))
|
||||||
.nest_service("/", ServeDir::new("client"))
|
.nest_service("/", ServeDir::new("../client"))
|
||||||
.layer(Extension(room_server))
|
.layer(Extension(room_server))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue