2024-08-30 02:56:03 +00:00
|
|
|
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;
|
2024-08-30 14:18:07 +00:00
|
|
|
var lastUpdate = 0.0;
|
2024-08-30 02:56:03 +00:00
|
|
|
var frames = 0;
|
|
|
|
var ticks = 0;
|
2024-08-30 14:18:07 +00:00
|
|
|
var serverTick = 0;
|
|
|
|
var serverPing = 0;
|
|
|
|
var reconciliations = 0;
|
|
|
|
var lastServerState = 0;
|
2024-08-30 02:56:03 +00:00
|
|
|
var ws;
|
2024-08-30 14:18:07 +00:00
|
|
|
|
|
|
|
const BUFFER_SIZE = TICK_RATE * 5; // 5 seconds of buffer
|
|
|
|
var stateBuffer = new Array(BUFFER_SIZE);
|
|
|
|
var inputBuffer = new Array(BUFFER_SIZE);
|
|
|
|
|
|
|
|
const fakePingInput = document.getElementById("fakeping");
|
|
|
|
var fakePing = new Stateful(localStorage.getItem("fakeping") || 0);
|
|
|
|
fakePingInput.value = fakePing.get();
|
|
|
|
fakePing.onUpdate(val => {
|
|
|
|
localStorage.setItem("fakeping", val);
|
|
|
|
});
|
|
|
|
fakePingInput.addEventListener("change", () => {
|
|
|
|
fakePing.set(fakePingInput.value);
|
|
|
|
});
|
2024-08-30 02:56:03 +00:00
|
|
|
|
|
|
|
const interpolationToggle = document.getElementById("interpolation");
|
2024-08-30 14:18:07 +00:00
|
|
|
var interpolationEnabled = new Stateful(localStorage.getItem("interpolation") || true);
|
|
|
|
interpolationToggle.checked = interpolationEnabled.get();
|
|
|
|
interpolationEnabled.onUpdate(val => {
|
2024-08-30 02:56:03 +00:00
|
|
|
localStorage.setItem("interpolation", val);
|
|
|
|
});
|
|
|
|
interpolationToggle.addEventListener("change", () => {
|
2024-08-30 14:18:07 +00:00
|
|
|
interpolationEnabled.set(interpolationToggle.checked);
|
2024-08-30 02:56:03 +00:00
|
|
|
});
|
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
const authorityPositionToggle = document.getElementById("show-authority");
|
|
|
|
var showAuthorityPositions = new Stateful(localStorage.getItem("show-authority") || false);
|
|
|
|
authorityPositionToggle.checked = showAuthorityPositions.get();
|
|
|
|
showAuthorityPositions.onUpdate(val => {
|
|
|
|
localStorage.setItem("show-authority", val);
|
2024-08-30 02:56:03 +00:00
|
|
|
});
|
2024-08-30 14:18:07 +00:00
|
|
|
authorityPositionToggle.addEventListener("change", () => {
|
|
|
|
showAuthorityPositions.set(authorityPositionToggle.checked);
|
2024-08-30 02:56:03 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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 => {
|
|
|
|
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;
|
2024-08-30 14:18:07 +00:00
|
|
|
case "update":
|
|
|
|
processServerTick(data);
|
2024-08-30 02:56:03 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
function processServerTick(data) {
|
|
|
|
lastServerState = data;
|
|
|
|
serverTick = data.tick;
|
|
|
|
var localState = stateBuffer[data.tick % BUFFER_SIZE]
|
|
|
|
if (localState) serverPing = new Date() - localState.time;
|
|
|
|
|
|
|
|
// update players
|
|
|
|
Object.keys(data.players).forEach(id => {
|
|
|
|
const player = players[id];
|
|
|
|
const serverPlayerState = data.players[id];
|
|
|
|
if (!localState) {
|
|
|
|
player.x = serverPlayerState.x;
|
|
|
|
player.y = serverPlayerState.y;
|
|
|
|
player.in_x = serverPlayerState.in_x;
|
|
|
|
player.in_y = serverPlayerState.in_y;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const localPlayerState = localState.players[id];
|
|
|
|
|
|
|
|
// calculate position difference from authority
|
|
|
|
var dist = Math.sqrt(
|
|
|
|
Math.pow(localPlayerState.x - serverPlayerState.x, 2) +
|
|
|
|
Math.pow(localPlayerState.y - serverPlayerState.y, 2));
|
|
|
|
|
|
|
|
if (dist > 10.0) {
|
|
|
|
return;
|
|
|
|
reconciliations++;
|
|
|
|
|
|
|
|
console.warn("Player#" + id + ": Reconciling " + dist + " units of error.");
|
|
|
|
console.warn("\tclient said (" + localPlayerState.x.toPrecision(4) + "," + localPlayerState.y.toPrecision(4) + ")");
|
|
|
|
console.warn("\tserver said (" + serverPlayerState.x.toPrecision(4) + "," + serverPlayerState.y.toPrecision(4) + ")");
|
|
|
|
|
|
|
|
// resync to authority and roll-forward
|
|
|
|
localState.players[id] = serverPlayerState;
|
|
|
|
for (var tick = serverTick + 1; tick < ticks; tick++) {
|
|
|
|
const state = stateBuffer[(tick - 1) % BUFFER_SIZE].players[id];
|
|
|
|
const input = id == client_id ? inputBuffer[tick % BUFFER_SIZE] : {
|
|
|
|
x: state.in_x,
|
|
|
|
y: state.in_y,
|
|
|
|
};
|
|
|
|
|
|
|
|
player.x = state.x;
|
|
|
|
player.y = state.y;
|
|
|
|
player.in_x = input.x;
|
|
|
|
player.in_y = input.y;
|
|
|
|
|
|
|
|
player.update(1000 / TICK_RATE / 1000);
|
|
|
|
stateBuffer[tick % BUFFER_SIZE].player = {
|
|
|
|
x: player.x,
|
|
|
|
y: player.y,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var lastState = stateBuffer[ticks % BUFFER_SIZE];
|
|
|
|
if (lastState) {
|
|
|
|
player.x = lastState.players[id].x;
|
|
|
|
player.y = lastState.players[id].y;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// update props
|
|
|
|
Object.keys(data.props).forEach(id => {
|
|
|
|
const serverPropState = data.props[id];
|
|
|
|
const prop = props[id];
|
|
|
|
|
|
|
|
prop.x = serverPropState.x;
|
|
|
|
prop.y = serverPropState.y;
|
|
|
|
prop.xv = serverPropState.xv;
|
|
|
|
prop.yv = serverPropState.yv;
|
|
|
|
|
|
|
|
return; // TODO: reimplement this once player reconciliation is stable
|
|
|
|
if (!localState) {
|
|
|
|
prop.x = serverPropState.x;
|
|
|
|
prop.y = serverPropState.y;
|
|
|
|
prop.xv = serverPropState.xv;
|
|
|
|
prop.yv = serverPropState.yv;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const localPropState = localState.props[id];
|
|
|
|
|
|
|
|
// calculate position difference from authority
|
|
|
|
var dist = Math.sqrt(
|
|
|
|
Math.pow(localPropState.x - serverPropState.x, 2) +
|
|
|
|
Math.pow(localPropState.y - serverPropState.y, 2));
|
|
|
|
|
|
|
|
if (dist > 10.0) {
|
|
|
|
reconciliations++;
|
|
|
|
|
|
|
|
console.warn("Prop#" + id + ": Reconciling " + dist + " units of error.");
|
|
|
|
|
|
|
|
// resync to authority and roll-forward
|
|
|
|
localState.props[id] = serverPropState;
|
|
|
|
for (var tick = serverTick; tick < ticks; tick++) {
|
|
|
|
const state = stateBuffer[(tick - 1) % BUFFER_SIZE];
|
|
|
|
const propState = state.props[id];
|
|
|
|
prop.x = propState.x;
|
|
|
|
prop.y = propState.y;
|
|
|
|
prop.xv = propState.xv;
|
|
|
|
prop.yv = propState.yv;
|
|
|
|
prop.update(1000 / TICK_RATE / 1000, Object.values(state.players));
|
|
|
|
stateBuffer[tick % BUFFER_SIZE].props[id] = {
|
|
|
|
x: prop.x,
|
|
|
|
y: prop.y,
|
|
|
|
xv: prop.xv,
|
|
|
|
yv: prop.yv,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var lastState = stateBuffer[ticks % BUFFER_SIZE];
|
|
|
|
if (lastState) {
|
|
|
|
prop.x = lastState.props[id].x;
|
|
|
|
prop.y = lastState.props[id].y;
|
|
|
|
prop.xv = lastState.props[id].xv;
|
|
|
|
prop.yv = lastState.props[id].yv;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-30 02:56:03 +00:00
|
|
|
canvas.addEventListener("keypress", event => {
|
|
|
|
switch (event.key.toLowerCase()) {
|
2024-08-30 11:30:53 +00:00
|
|
|
case 'i':
|
2024-08-30 14:18:07 +00:00
|
|
|
interpolationEnabled.update(val => !val);
|
|
|
|
interpolationToggle.checked = interpolationEnabled.get();
|
2024-08-30 11:30:53 +00:00
|
|
|
break;
|
2024-08-30 02:56:03 +00:00
|
|
|
case 'p':
|
2024-08-30 14:18:07 +00:00
|
|
|
showAuthorityPositions.update(val => !val);
|
|
|
|
authorityPositionToggle.checked = showAuthorityPositions.get();
|
2024-08-30 02:56:03 +00:00
|
|
|
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) {
|
2024-08-30 11:33:40 +00:00
|
|
|
if (msg === "") return;
|
2024-08-30 02:56:03 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
if (!ws) return;
|
|
|
|
ws.send(JSON.stringify({
|
|
|
|
type: "chat",
|
|
|
|
msg: msg,
|
|
|
|
}));
|
2024-08-30 14:18:07 +00:00
|
|
|
}, fakePing.get());
|
2024-08-30 02:56:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function update(delta) {
|
|
|
|
const clientPlayer = players[client_id];
|
2024-08-30 11:30:53 +00:00
|
|
|
if (!clientPlayer) return;
|
|
|
|
|
|
|
|
clientPlayer.in_x = input.move_right - input.move_left;
|
|
|
|
clientPlayer.in_y = input.move_down - input.move_up;
|
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
inputBuffer[ticks % BUFFER_SIZE] = {
|
|
|
|
x: clientPlayer.in_x,
|
|
|
|
y: clientPlayer.in_y,
|
2024-08-30 11:30:53 +00:00
|
|
|
};
|
2024-08-30 02:56:03 +00:00
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
var playerState = {};
|
|
|
|
Object.keys(players).forEach(id => {
|
|
|
|
const player = players[id];
|
|
|
|
player.update(delta);
|
|
|
|
playerState[id] = {
|
|
|
|
x: player.x,
|
|
|
|
y: player.y,
|
|
|
|
in_x: player.in_x,
|
|
|
|
in_y: player.in_y,
|
|
|
|
};
|
|
|
|
});
|
2024-08-30 11:30:53 +00:00
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
var propState = {};
|
2024-08-30 11:30:53 +00:00
|
|
|
Object.keys(props).forEach(id => {
|
|
|
|
const prop = props[id];
|
|
|
|
prop.update(delta, Object.values(players));
|
2024-08-30 14:18:07 +00:00
|
|
|
propState[id] = {
|
2024-08-30 11:30:53 +00:00
|
|
|
x: prop.x,
|
|
|
|
y: prop.y,
|
|
|
|
xv: prop.xv,
|
|
|
|
yv: prop.yv,
|
2024-08-30 02:56:03 +00:00
|
|
|
};
|
2024-08-30 11:30:53 +00:00
|
|
|
});
|
2024-08-30 02:56:03 +00:00
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
stateBuffer[ticks % BUFFER_SIZE] = {
|
|
|
|
tick: ticks,
|
|
|
|
time: new Date(),
|
|
|
|
players: playerState,
|
|
|
|
props: propState,
|
|
|
|
};
|
|
|
|
|
|
|
|
setTimeout(ticks => {
|
|
|
|
if (!ws) return;
|
|
|
|
ws.send(JSON.stringify({
|
|
|
|
type: "update",
|
|
|
|
tick: ticks,
|
|
|
|
x: input.move_right - input.move_left,
|
|
|
|
y: input.move_down - input.move_up,
|
|
|
|
}));
|
|
|
|
}, fakePing.get() / 2, ticks);
|
2024-08-30 02:56:03 +00:00
|
|
|
|
|
|
|
ticks++;
|
|
|
|
}
|
|
|
|
|
|
|
|
function draw() {
|
2024-08-30 14:18:07 +00:00
|
|
|
delta = performance.now() - lastUpdate;
|
|
|
|
if (performance.now() - lastUpdate >= 1000 / TICK_RATE) {
|
|
|
|
lastUpdate = performance.now();
|
2024-08-30 02:56:03 +00:00
|
|
|
update(delta / 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, WORLD_SIZE, WORLD_SIZE);
|
|
|
|
|
|
|
|
ctx.fillStyle = "#f0f0f0";
|
|
|
|
ctx.fillRect(0, 0, WORLD_SIZE, WORLD_SIZE);
|
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
// DEBUG: show authoritative position for client players and props
|
|
|
|
var authorityDistance = 0.0;
|
|
|
|
if (lastServerState && showAuthorityPositions.get()) {
|
|
|
|
const player = players[client_id];
|
|
|
|
const lastPlayerState = lastServerState.players[client_id];
|
|
|
|
authorityDistance = Math.sqrt(
|
|
|
|
Math.pow(players[client_id].x - lastPlayerState.x, 2) +
|
|
|
|
Math.pow(players[client_id].y - lastPlayerState.y, 2));
|
|
|
|
ctx.strokeStyle = player.colour;
|
2024-08-30 02:56:03 +00:00
|
|
|
ctx.beginPath();
|
2024-08-30 14:18:07 +00:00
|
|
|
ctx.rect(
|
|
|
|
lastPlayerState.x - Player.SIZE / 2,
|
|
|
|
lastPlayerState.y - Player.SIZE / 2,
|
2024-08-30 02:56:03 +00:00
|
|
|
Player.SIZE, Player.SIZE);
|
|
|
|
ctx.stroke();
|
2024-08-30 14:18:07 +00:00
|
|
|
|
|
|
|
Object.keys(props).forEach(id => {
|
|
|
|
const prop = props[id];
|
|
|
|
const lastPropState = lastServerState.props[id];
|
|
|
|
ctx.strokeStyle = prop.colour;
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.rect(
|
|
|
|
lastPropState.x - Prop.SIZE / 2,
|
|
|
|
lastPropState.y - Prop.SIZE / 2,
|
|
|
|
Prop.SIZE, Prop.SIZE);
|
|
|
|
ctx.stroke();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
drawPlayers();
|
|
|
|
drawProps();
|
|
|
|
|
|
|
|
var debug = {
|
|
|
|
"ping": serverPing + "ms",
|
|
|
|
"fake ping": + fakePing.get() + "ms",
|
|
|
|
"ticks behind": ticks - serverTick,
|
|
|
|
"reconciliations": reconciliations,
|
|
|
|
}
|
|
|
|
if (showAuthorityPositions.get()) {
|
|
|
|
debug["authority distance"] = authorityDistance;
|
2024-08-30 02:56:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ctx.fillStyle = "#101010";
|
|
|
|
ctx.font = "16px monospace";
|
|
|
|
ctx.textAlign = "left";
|
|
|
|
ctx.textBaseline = "bottom";
|
2024-08-30 14:18:07 +00:00
|
|
|
var debug_y = WORLD_SIZE - 8 - (Object.keys(debug).length - 1) * 16;
|
|
|
|
Object.keys(debug).forEach((key, i) => {
|
|
|
|
ctx.fillText(key + ": " + debug[key], 8, debug_y + 16 * i);
|
|
|
|
});
|
2024-08-30 02:56:03 +00:00
|
|
|
|
|
|
|
frames++;
|
|
|
|
requestAnimationFrame(draw);
|
|
|
|
}
|
|
|
|
|
|
|
|
function drawPlayers() {
|
|
|
|
Object.keys(players).forEach((id, index) => {
|
|
|
|
const player = players[id];
|
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
if (interpolationEnabled.get()) {
|
2024-08-30 02:56:03 +00:00
|
|
|
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];
|
|
|
|
|
2024-08-30 14:18:07 +00:00
|
|
|
if (interpolationEnabled.get()) {
|
2024-08-30 02:56:03 +00:00
|
|
|
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);
|
|
|
|
|