first commit! 🎉
This commit is contained in:
commit
c35c18bbbc
19 changed files with 1046 additions and 0 deletions
432
client/js/main.js
Normal file
432
client/js/main.js
Normal file
|
@ -0,0 +1,432 @@
|
|||
import Player from "/common/player.js";
|
||||
import { WORLD_SIZE } from "/common/world.js";
|
||||
import Stateful from "./silver.min.js";
|
||||
import Prop from "/common/prop.js";
|
||||
|
||||
const canvas = document.getElementById("game");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const chatbox = document.getElementById("chatbox");
|
||||
const composeBox = document.getElementById("compose-msg");
|
||||
const composeBtn = document.getElementById("compose-btn");
|
||||
|
||||
const playerSprite = new Image();
|
||||
playerSprite.src = "/img/player.png";
|
||||
|
||||
const TICK_RATE = 30;
|
||||
|
||||
canvas.height = WORLD_SIZE;
|
||||
canvas.width = WORLD_SIZE;
|
||||
|
||||
var players = {};
|
||||
var props = {};
|
||||
var client_id;
|
||||
var delta = 0.0;
|
||||
var last_update = 0.0;
|
||||
var frames = 0;
|
||||
var ticks = 0;
|
||||
var server_tick = 0;
|
||||
var server_ping = 0;
|
||||
var ws;
|
||||
var predictions = {};
|
||||
|
||||
const interpolationToggle = document.getElementById("interpolation");
|
||||
var enable_interpolation = new Stateful(localStorage.getItem("interpolation") || true);
|
||||
interpolationToggle.checked = enable_interpolation.get();
|
||||
enable_interpolation.onUpdate(val => {
|
||||
localStorage.setItem("interpolation", val);
|
||||
});
|
||||
interpolationToggle.addEventListener("change", () => {
|
||||
enable_interpolation.set(interpolationToggle.checked);
|
||||
});
|
||||
|
||||
const fakePingInput = document.getElementById("fakeping");
|
||||
var fake_ping = new Stateful(localStorage.getItem("fakeping") || 0);
|
||||
fakePingInput.value = fake_ping.get();
|
||||
fake_ping.onUpdate(val => {
|
||||
localStorage.setItem("fakeping", val);
|
||||
});
|
||||
fakePingInput.addEventListener("change", () => {
|
||||
fake_ping.set(fakePingInput.value);
|
||||
});
|
||||
|
||||
var input = {
|
||||
move_up: 0.0,
|
||||
move_down: 0.0,
|
||||
move_left: 0.0,
|
||||
move_right: 0.0,
|
||||
};
|
||||
|
||||
function start() {
|
||||
const secure = location.protocol === "https:";
|
||||
ws = new WebSocket((secure ? "wss://" : "ws://") + location.host);
|
||||
ws.addEventListener("open", () => {
|
||||
canvas.classList.remove("offline");
|
||||
console.log("Websocket connection established!");
|
||||
const name = prompt("What's your name?");
|
||||
canvas.focus();
|
||||
ws.send(JSON.stringify({
|
||||
type: "join",
|
||||
name: name,
|
||||
}));
|
||||
});
|
||||
|
||||
ws.addEventListener("message", packet => {
|
||||
setTimeout(() => {
|
||||
var data = JSON.parse(packet.data);
|
||||
|
||||
switch (data.type) {
|
||||
case "welcome":
|
||||
client_id = data.id;
|
||||
Object.keys(data.players).forEach(id => {
|
||||
players[id] = new Player(
|
||||
data.players[id].name,
|
||||
data.players[id].x,
|
||||
data.players[id].y,
|
||||
data.players[id].col);
|
||||
});
|
||||
Object.keys(data.props).forEach(id => {
|
||||
const prop = new Prop(
|
||||
data.props[id].name,
|
||||
data.props[id].x,
|
||||
data.props[id].y,
|
||||
data.props[id].col,
|
||||
data.props[id].sprite);
|
||||
prop.spriteImage = new Image();
|
||||
prop.spriteImage.src = prop.sprite;
|
||||
props[id] = prop;
|
||||
});
|
||||
console.log("client ID is " + client_id);
|
||||
break;
|
||||
case "join":
|
||||
console.log(data.name + " joined the game.");
|
||||
const p = document.createElement("p");
|
||||
p.className = "chat-message";
|
||||
p.innerText = data.name + " joined the game.";
|
||||
chatbox.appendChild(p);
|
||||
chatbox.scrollTop = chatbox.scrollHeight;
|
||||
players[data.id] = new Player(data.name, data.x, data.y, data.col);
|
||||
break;
|
||||
case "update": {
|
||||
server_tick = data.tick;
|
||||
Object.keys(data.players).forEach(id => {
|
||||
const player = players[id];
|
||||
const update = data.players[id];
|
||||
// this should never be true, but just in case
|
||||
if (!player) return;
|
||||
if (id == client_id) {
|
||||
// clear all predictions prior to this tick
|
||||
Object.keys(predictions).forEach(tick => {
|
||||
if (tick < data.tick) delete predictions[tick];
|
||||
});
|
||||
var prediction = predictions[data.tick];
|
||||
if (!prediction) return;
|
||||
server_ping = new Date() - prediction.time;
|
||||
if (Math.abs(prediction.x - update.x) > 1)
|
||||
players[client_id].x = update.x;
|
||||
if (Math.abs(prediction.y - update.y) > 1)
|
||||
players[client_id].y = update.y;
|
||||
delete predictions[data.tick];
|
||||
} else {
|
||||
player.x = update.x;
|
||||
player.y = update.y;
|
||||
}
|
||||
});
|
||||
Object.keys(data.props).forEach(id => {
|
||||
const prop = props[id];
|
||||
const update = data.props[id];
|
||||
|
||||
prop.x = update.x;
|
||||
prop.y = update.y;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "chat": {
|
||||
const player = players[data.player];
|
||||
|
||||
const _name = document.createElement("span");
|
||||
_name.innerText = player.name;
|
||||
const _msg = document.createElement("span");
|
||||
_msg.innerText = data.msg;
|
||||
|
||||
const p = document.createElement("p");
|
||||
p.className = "chat-message";
|
||||
p.innerHTML = `<<span style="color:${player.colour}">${_name.innerText}</span>> ${_msg.innerText}`;
|
||||
|
||||
chatbox.appendChild(p);
|
||||
chatbox.scrollTop = chatbox.scrollHeight;
|
||||
}
|
||||
case "leave": {
|
||||
const player = players[data.id];
|
||||
if (!player) break;
|
||||
console.log(player.name + " left the game.");
|
||||
const p = document.createElement("p");
|
||||
p.className = "chat-message";
|
||||
p.innerText = player.name + " left the game.";
|
||||
chatbox.appendChild(p);
|
||||
chatbox.scrollTop = chatbox.scrollHeight;
|
||||
delete players[data.id];
|
||||
break;
|
||||
}
|
||||
case "kick": {
|
||||
console.log("Kicked from the server: " + data.reason);
|
||||
const p = document.createElement("p");
|
||||
p.className = "chat-message";
|
||||
p.innerText = "Kicked from the server: " + data.reason;
|
||||
chatbox.appendChild(p);
|
||||
chatbox.scrollTop = chatbox.scrollHeight;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn("Unknown message received from the server.");
|
||||
console.warn(msg);
|
||||
}
|
||||
}, fake_ping.get() / 2);
|
||||
});
|
||||
|
||||
ws.addEventListener("error", error => {
|
||||
canvas.classList.add("offline");
|
||||
console.error(error);
|
||||
const p = document.createElement("p");
|
||||
p.classList.add("chat-message");
|
||||
p.classList.add("error");
|
||||
p.innerText = "Connection error. Please refresh!";
|
||||
chatbox.appendChild(p);
|
||||
chatbox.scrollTop = chatbox.scrollHeight;
|
||||
ws = undefined;
|
||||
});
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
canvas.classList.add("offline");
|
||||
console.log("Websocket connection closed.");
|
||||
const p = document.createElement("p");
|
||||
p.classList.add("chat-message");
|
||||
p.classList.add("error");
|
||||
p.innerText = "Connection error. Please refresh!";
|
||||
chatbox.appendChild(p);
|
||||
chatbox.scrollTop = chatbox.scrollHeight;
|
||||
ws = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
canvas.addEventListener("keypress", event => {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'p':
|
||||
console.log(predictions);
|
||||
break;
|
||||
case 'enter':
|
||||
composeBox.focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("keydown", event => {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'w':
|
||||
input.move_up = 1.0;
|
||||
break;
|
||||
case 'a':
|
||||
input.move_left = 1.0;
|
||||
break;
|
||||
case 's':
|
||||
input.move_down = 1.0;
|
||||
break;
|
||||
case 'd':
|
||||
input.move_right = 1.0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("keyup", event => {
|
||||
switch (event.key.toLowerCase()) {
|
||||
case 'w':
|
||||
input.move_up = 0.0;
|
||||
break;
|
||||
case 'a':
|
||||
input.move_left = 0.0;
|
||||
break;
|
||||
case 's':
|
||||
input.move_down = 0.0;
|
||||
break;
|
||||
case 'd':
|
||||
input.move_right = 0.0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("focusout", () => {
|
||||
input.move_up = 0.0;
|
||||
input.move_left = 0.0;
|
||||
input.move_down = 0.0;
|
||||
input.move_right = 0.0;
|
||||
});
|
||||
|
||||
composeBtn.addEventListener("click", () => {
|
||||
sendChat(composeBox.value);
|
||||
composeBox.value = "";
|
||||
});
|
||||
|
||||
composeBox.addEventListener("keypress", event => {
|
||||
if (event.key != "Enter") return;
|
||||
sendChat(composeBox.value);
|
||||
composeBox.value = "";
|
||||
canvas.focus();
|
||||
});
|
||||
|
||||
function sendChat(msg) {
|
||||
setTimeout(() => {
|
||||
if (!ws) return;
|
||||
ws.send(JSON.stringify({
|
||||
type: "chat",
|
||||
msg: msg,
|
||||
}));
|
||||
}, fake_ping.get() / 2);
|
||||
}
|
||||
|
||||
function update(delta) {
|
||||
const clientPlayer = players[client_id];
|
||||
if (clientPlayer) {
|
||||
clientPlayer.in_x = input.move_right - input.move_left;
|
||||
clientPlayer.in_y = input.move_down - input.move_up;
|
||||
|
||||
clientPlayer.update(delta);
|
||||
|
||||
// insert prediction for the next server tick
|
||||
predictions[ticks] = {
|
||||
time: new Date(),
|
||||
x: clientPlayer.x,
|
||||
y: clientPlayer.y,
|
||||
};
|
||||
|
||||
var t = ticks;
|
||||
setTimeout(() => {
|
||||
if (!ws) return;
|
||||
ws.send(JSON.stringify({
|
||||
type: "update",
|
||||
tick: t,
|
||||
x: input.move_right - input.move_left,
|
||||
y: input.move_down - input.move_up,
|
||||
}));
|
||||
}, fake_ping.get() / 2);
|
||||
}
|
||||
|
||||
ticks++;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
delta = performance.now() - last_update;
|
||||
if (performance.now() - last_update >= 1000 / TICK_RATE) {
|
||||
last_update = performance.now();
|
||||
update(delta / 1000);
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, WORLD_SIZE, WORLD_SIZE);
|
||||
|
||||
ctx.fillStyle = "#f0f0f0";
|
||||
ctx.fillRect(0, 0, WORLD_SIZE, WORLD_SIZE);
|
||||
|
||||
drawPlayers();
|
||||
drawProps();
|
||||
|
||||
// DEBUG: draw last known authoritative state
|
||||
if (Object.values(predictions).length > 10) {
|
||||
const server_state = Object.values(predictions)[0];
|
||||
ctx.fillStyle = "#208020";
|
||||
ctx.beginPath();
|
||||
ctx.rect(server_state.x - Player.SIZE / 2,
|
||||
server_state.y - Player.SIZE / 2,
|
||||
Player.SIZE, Player.SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
var debug = "ping: " + server_ping + "ms\n" +
|
||||
"fake ping: " + fake_ping.get() + "ms\n" +
|
||||
"buffer length: " + Object.keys(predictions).length + "\n" +
|
||||
"delta: " + delta + "\n" +
|
||||
"ticks behind: " + (ticks - server_tick);
|
||||
ctx.fillStyle = "#101010";
|
||||
ctx.font = "16px monospace";
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "bottom";
|
||||
var debug_lines = debug.split('\n');
|
||||
var debug_y = WORLD_SIZE - 8 - (debug_lines.length - 1) * 16;
|
||||
for (var i = 0; i < debug_lines.length; i++) {
|
||||
ctx.fillText(debug_lines[i], 8, debug_y + 16 * i);
|
||||
}
|
||||
|
||||
frames++;
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
function drawPlayers() {
|
||||
Object.keys(players).forEach((id, index) => {
|
||||
const player = players[id];
|
||||
|
||||
if (enable_interpolation.get()) {
|
||||
player.draw_x = player.draw_x + 0.1 * (player.x - player.draw_x);
|
||||
player.draw_y = player.draw_y + 0.1 * (player.y - player.draw_y);
|
||||
} else {
|
||||
player.draw_x = player.x;
|
||||
player.draw_y = player.y;
|
||||
}
|
||||
|
||||
ctx.drawImage(
|
||||
playerSprite,
|
||||
player.draw_x - Player.SIZE / 2,
|
||||
player.draw_y - Player.SIZE / 2,
|
||||
Player.SIZE, Player.SIZE
|
||||
);
|
||||
|
||||
ctx.fillStyle = player.colour;
|
||||
ctx.font = "16px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.fillText(player.name, player.draw_x, player.draw_y - Player.SIZE / 2 - 16);
|
||||
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.fillText(`${player.name} (${id})`, 8, 28 + index * 16);
|
||||
});
|
||||
|
||||
ctx.fillStyle = "#101010";
|
||||
ctx.font = "20px monospace";
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.fillText("Players:", 8, 8);
|
||||
}
|
||||
|
||||
function drawProps() {
|
||||
Object.keys(props).forEach(id => {
|
||||
const prop = props[id];
|
||||
|
||||
if (enable_interpolation.get()) {
|
||||
prop.draw_x = prop.draw_x + 0.1 * (prop.x - prop.draw_x);
|
||||
prop.draw_y = prop.draw_y + 0.1 * (prop.y - prop.draw_y);
|
||||
} else {
|
||||
prop.draw_x = prop.x;
|
||||
prop.draw_y = prop.y;
|
||||
}
|
||||
|
||||
ctx.drawImage(
|
||||
prop.spriteImage,
|
||||
prop.draw_x - Prop.SIZE / 2,
|
||||
prop.draw_y - Prop.SIZE / 2,
|
||||
Prop.SIZE, Prop.SIZE
|
||||
);
|
||||
|
||||
ctx.fillStyle = prop.colour;
|
||||
ctx.font = "16px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.fillText(prop.name, prop.draw_x, prop.draw_y - Prop.SIZE / 2 - 16);
|
||||
});
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue