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, 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_eaten: Item[] * }} GameState */ /** @type {GameState} */ export const initState = { started: false, input: {}, players: [], map: {}, items_eaten: [] } let last = Date.now() let fps_div = document.getElementById("fps") export function advance( pastData = initState, input = { players: {} }, frame ) { let data = processInput(pastData, input, frame); if (frame % 60 == 0) { let now = Date.now() let fps = (now-last)/1000*60 fps_div.innerHTML = fps.toFixed(2); last = now } 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 }; 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 } else { data.players[id].pos = round_pos(current_pos) } // 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) let item = data.map.items[item_key] if (item) { data.items_eaten.push(item_key) delete data.map.items[item_key] } } } } }