diff options
Diffstat (limited to 'client/js')
-rw-r--r-- | client/js/game.js | 111 | ||||
-rw-r--r-- | client/js/gfx/graphics.js | 128 | ||||
-rw-r--r-- | client/js/gfx/map.js | 293 | ||||
-rw-r--r-- | client/js/gfx/sprite.js | 71 | ||||
-rw-r--r-- | client/js/input.js | 84 | ||||
-rw-r--r-- | client/js/logic.js | 335 | ||||
-rw-r--r-- | client/js/main.js | 168 | ||||
-rw-r--r-- | client/js/multiplayer.js | 318 |
8 files changed, 57 insertions, 1451 deletions
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/js/multiplayer.js b/client/js/multiplayer.js deleted file mode 100644 index eec1c7d..0000000 --- a/client/js/multiplayer.js +++ /dev/null @@ -1,318 +0,0 @@ -/** - * @author tint - */ - -/** - * @template Input - * @typedef {{ - * added?: number[], - * removed?: number[], - * players: { - * [conn: number]: Input - * }, - * }} GameInput - */ - -/** - * @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); - url.protocol = url.protocol.replace("http", "ws"); - - const socket = new WebSocket(url); - - let requestStateTime; - let hasState = false; - let connectionId; - let cachedInputs = []; - let connections = []; - - let pingPromise; - - function send(obj) { - socket.send(JSON.stringify(obj)); - } - - function applyInput(input) { - let prev = game.getHistory(input.frame); - let newInput = prev && prev.input ? {...prev.input} : { players: {} }; - - if(input.type === "input") { - if(input.connection === undefined) { // local input - if(input.data) { - // send it to the server - send(input); - - // then apply it - newInput.players[connectionId] = input.data; - } - } else { - newInput.players[input.connection] = input.data; - } - } else if(input.type === "connections") { - if(input.added !== null) { - newInput.added = (newInput.added || []).concat([input.added]); - } - if(input.removed !== null) { - if(newInput.added) { - newInput.added = newInput.added.filter(n => n !== input.removed); - } - newInput.removed = (newInput.removed || []).concat([input.removed]); - } - } - - game.setInput(input.frame, newInput); - } - - function flushCachedInputs(latency = 0) { - for(const input of cachedInputs) { - // only care about inputs after the new state - if(input.frame <= game.historyStart) { - continue; - } - - applyInput(input); - } - cachedInputs = []; - return onStart(game.getFrame(), latency, connectionId, update, ping, desyncCheck); - } - - function update(input, frame) { - if(input === undefined) { // used to update the game locally - if(hasState) { - applyInput({ - frame, - }); - } - return; - } - const data = { - type: "input", - data: input, - frame: frame, - }; - - if(!hasState) { - cachedInputs.push(data); - } else { - applyInput(data); - } - } - - async function ping() { - send({ - type: "ping", - frame: Math.max(0, game.currentFrame), - }); - const frame = await new Promise(r => pingPromise = r); - return game.currentFrame - frame; - } - - async function desyncCheck(frame) { - 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 proms = connections - .filter(n => n !== connectionId) - .map(connection => { - send({ - type: "requeststate", - frame, - connection, - }); - return new Promise(r => { - stateRequests[frame + "," + connection] = state => { - r({ - state, - connection, - }); - } - }); - }); - - 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; - } - return false; - } - - let stateRequests = {}; - - socket.onmessage = message => { - const data = JSON.parse(message.data.toString()); - - switch(data.type) { - case "error": - console.error(data); - break; - case "framerequest": - send({ - type: "frame", - frame: Math.max(game.currentFrame, 1), - }); - break; - case "state": - if(data.frame + "," + data.connection in stateRequests) { - stateRequests[data.frame + "," + data.connection](data.state); - } - if(!hasState) { - game.startHistory(data.frame, data.state); - hasState = true; - - // this state is from the past - // i want to find out exactly how far in the past - // the sequence of requests looks like: - // client -[staterequest]-> server -[staterequest]-> client2 - // client2 -[state]-> server -[state]-> client - // and the time i'm concerned with is the second half, - // how long it takes the state to come from client2 - let delta = 0; - if(requestStateTime !== undefined) { - delta = performance.now() - requestStateTime; - } - if (!flushCachedInputs(delta / 2)) { - socket.close() - document.getElementById("lobby").style.display = "none" - document.getElementById("join").style.display = "" - return - } - } - break; - case "requeststate": - // wait until there's some state to send - const startTime = performance.now(); - function check() { - if(performance.now() - startTime > 5000) { - return; // give up after 5s - } - const state = game.getHistory(data.frame); - if(!state) { - return; - } - - send({ - type: "state", - frame: data.frame, - state: state.data, - }); - clearInterval(interval); - } - const interval = setInterval(check, 100); - check(); - break; - case "connections": - connections = data.connections; - if(connectionId === undefined) { - console.log("setting connection id", data.id); - connectionId = data.id; - if(data.connections.length === 1) { // no need to request state - hasState = true; - applyInput(data); - flushCachedInputs(); // just in case, also it calls onStart - break; - } - - // grab the state from another client - console.log("requesting state"); - // measure the time it takes for state to be delivered - requestStateTime = performance.now(); - send({ - type: "requeststate", - frame: data.frame, - }); - } - - if(!hasState) { - cachedInputs.push(data); - } else { - applyInput(data); - } - - break; - case "input": - if(!hasState) { - cachedInputs.push(data); - } else { - applyInput(data); - } - break; - case "pong": - if(pingPromise) { - pingPromise(data.frame); - pingPromise = undefined; - } - break; - default: - console.warn("unknown server message", data); - break; - } - } -} - -// compare two plain objects (things that can be JSON.stringified) -function objeq(a, b) { - if(typeof(a) !== typeof(b)) { - return false; - } - // array diff - if(Array.isArray(a) && Array.isArray(b)) { - if(a.length !== b.length) { - return false; - } - for(let i = 0; i < a.length; i++) { - if(!objeq(a[i], b[i])) { - return false; - } - } - return true; - } - switch(typeof(a)) { - // primitives can be compared directly - case "number": - case "boolean": - case "string": - case "undefined": return a === b; - - case "object": - // typeof(null) = "object" but null can be compared directly - if(a === null || b === null) { - return a === b; - } - // object diff - for(let k in a) { - if(!(k in b) || !objeq(a[k], b[k])) { - return false; - } - } - for(let k in b) { - if(!(k in a)) { - return false; - } - } - return true; - default: // incomparable things - return false; - } -} |