tuxman/client/js/logic.js

333 lines
8.4 KiB
JavaScript

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]
}
}
}
}
}