673 lines
21 KiB
JavaScript
673 lines
21 KiB
JavaScript
const canvas = document.getElementById("game");
|
|
const ctx = canvas.getContext("2d");
|
|
const width = 640;
|
|
const height = 480;
|
|
const server_ip = document.getElementById("server-ip");
|
|
const server_nick = document.getElementById("server-nick");
|
|
const connect_server = document.getElementById("connect-server");
|
|
const server_error = document.getElementById("server-error");
|
|
function wrapText(ctx, text, maxWidth) {
|
|
const lines = [];
|
|
let currentLine = '';
|
|
for (let i = 0; i < text.length; i++) {
|
|
const char = text[i];
|
|
const testLine = currentLine + char;
|
|
const testWidth = ctx.measureText(testLine).width;
|
|
if (testWidth > maxWidth && currentLine) {
|
|
lines.push(currentLine);
|
|
currentLine = char;
|
|
}
|
|
else {
|
|
currentLine = testLine;
|
|
}
|
|
}
|
|
if (currentLine) {
|
|
lines.push(currentLine);
|
|
}
|
|
return lines;
|
|
}
|
|
function lerp(a, b, alpha) {
|
|
return a + alpha * (b - a);
|
|
}
|
|
function setServerError(text) {
|
|
server_error.innerText = text;
|
|
}
|
|
connect_server.onclick = () => {
|
|
let ip = server_ip.value;
|
|
let nick = server_nick.value;
|
|
setServerError("");
|
|
if (ip.length == 0)
|
|
return setServerError("введите айпи пж");
|
|
if (nick.length == 0)
|
|
return setServerError("введите ник пж");
|
|
if (!ip.includes(":"))
|
|
ip += ":8000";
|
|
connectServer(ip, nick);
|
|
};
|
|
setInterval(tick, 1000 / 20);
|
|
setInterval(renderTick, 1000 / 60);
|
|
let renderTimer = () => {
|
|
render();
|
|
requestAnimationFrame(renderTimer);
|
|
};
|
|
requestAnimationFrame(renderTimer);
|
|
class Block {
|
|
constructor(x, y, color, collides, type) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.color = color;
|
|
this.collides = collides;
|
|
this.type = type;
|
|
}
|
|
render() {
|
|
let rect = this.translate_to_camera();
|
|
if (this.is_need_render(rect)) {
|
|
ctx.fillStyle = this.color;
|
|
ctx.fillRect(...rect);
|
|
}
|
|
}
|
|
is_need_render(rect) {
|
|
return rect[0] + rect[2] > 0 || rect[1] + rect[3] > 0 || rect[0] < width || rect[1] < height;
|
|
}
|
|
translate_to_camera() {
|
|
let size = camera.size * 16;
|
|
return [
|
|
this.x * size - size / 2 + (width / 2 - camera.x * size),
|
|
height - (this.y + 1) * size + size / 2 - (height / 2 - camera.y * size),
|
|
size,
|
|
size
|
|
];
|
|
}
|
|
tick() {
|
|
}
|
|
renderTick() {
|
|
}
|
|
on_collide(player, x, y) {
|
|
if (x != 0)
|
|
player.velocity_x = this.x + x - player.x;
|
|
if (y != 0)
|
|
player.velocity_y = this.y + y - player.y;
|
|
}
|
|
renderText() {
|
|
}
|
|
}
|
|
class Player extends Block {
|
|
constructor(x, y, name, color, velocity_x, velocity_y) {
|
|
super(x, y, color, true, null);
|
|
this.velocity_x = velocity_x;
|
|
this.velocity_y = velocity_y;
|
|
this.name = name;
|
|
}
|
|
reset() {
|
|
this.on_ground = false;
|
|
}
|
|
on_collide(player, x, y) {
|
|
super.on_collide(player, x, y);
|
|
}
|
|
tick(collide = true) {
|
|
this.x = Math.round(this.x * 100) / 100;
|
|
this.y = Math.round(this.y * 100) / 100;
|
|
this.velocity_x = Math.round(this.velocity_x * 100) / 100;
|
|
this.velocity_y = Math.round(this.velocity_y * 100) / 100;
|
|
if (collide)
|
|
this.collide();
|
|
}
|
|
collide() {
|
|
this.on_ground = false;
|
|
for (const block of blocks) {
|
|
if (!block.collides)
|
|
continue;
|
|
let collide_x = 0;
|
|
let collide_y = 0;
|
|
if (this.x > block.x - 1 && this.x < block.x + 1) {
|
|
if (this.y > block.y && this.y + this.velocity_y - 1 < block.y) {
|
|
this.on_ground = true;
|
|
collide_y = 1;
|
|
}
|
|
if (this.y < block.y && this.y + this.velocity_y > block.y - 1)
|
|
collide_y = -1;
|
|
}
|
|
if (this.y > block.y - 1 && this.y < block.y + 1) {
|
|
if (this.x > block.x && this.x + this.velocity_x - 1 < block.x)
|
|
collide_x = 1;
|
|
if (this.x < block.x && this.x + this.velocity_x > block.x - 1)
|
|
collide_x = -1;
|
|
}
|
|
block.on_collide(this, collide_x, collide_y);
|
|
}
|
|
}
|
|
renderTick() {
|
|
this.velocity_x *= 0.5;
|
|
this.velocity_y *= 0.5;
|
|
this.x += this.velocity_x;
|
|
this.y += this.velocity_y;
|
|
}
|
|
renderText() {
|
|
super.renderText();
|
|
let rect = this.translate_to_camera();
|
|
if (this.is_need_render(rect)) {
|
|
ctx.fillStyle = "#ddd";
|
|
ctx.font = "15px monospace";
|
|
let width = ctx.measureText(this.name).width;
|
|
ctx.fillText(this.name, rect[0] + rect[2] / 2 - width / 2, rect[1] - 5);
|
|
}
|
|
}
|
|
teleport(x, y) {
|
|
this.velocity_x = x - this.x;
|
|
this.velocity_y = y - this.y;
|
|
}
|
|
forceTeleport(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.velocity_x = 0;
|
|
this.velocity_y = 0;
|
|
}
|
|
}
|
|
class Packet {
|
|
constructor(id, data) {
|
|
this.id = id;
|
|
this.data = data;
|
|
}
|
|
getId() { return this.id; }
|
|
getData() { return this.data; }
|
|
static fromString(data) {
|
|
return new Packet(data[0], data.slice(1).split("\n"));
|
|
}
|
|
}
|
|
class JoinPacket extends Packet {
|
|
constructor(name) {
|
|
super("J", [name]);
|
|
}
|
|
}
|
|
class MessagePacket extends Packet {
|
|
constructor(message) {
|
|
super("M", [message]);
|
|
}
|
|
}
|
|
class KeyPacket extends Packet {
|
|
constructor(key, pressed) {
|
|
super("K", [key, pressed ? "1" : "0"]);
|
|
}
|
|
}
|
|
class PlaceBlockPacket extends Packet {
|
|
constructor(x, y, type) {
|
|
super("P", [x.toString(), y.toString(), type]);
|
|
}
|
|
}
|
|
class DestroyBlockPacket extends Packet {
|
|
constructor(x, y) {
|
|
super("D", [x.toString(), y.toString()]);
|
|
}
|
|
}
|
|
class PositionPacket extends Packet {
|
|
constructor(x, y) {
|
|
super("X", [x.toString(), y.toString()]);
|
|
}
|
|
}
|
|
class VelocityPacket extends Packet {
|
|
constructor(x, y) {
|
|
super("V", [x.toString(), y.toString()]);
|
|
}
|
|
}
|
|
class Connection {
|
|
constructor(address, on_packet, on_close) {
|
|
this.socket = new WebSocket("ws://" + address, "cubic");
|
|
this.on_packet = on_packet;
|
|
this.on_close = on_close;
|
|
this.socket.onmessage = this._on_message;
|
|
this.socket.onclose = this._on_close;
|
|
this.socket.onerror = this._on_error;
|
|
}
|
|
_on_message(event) {
|
|
this.on_packet(Packet.fromString(event.data));
|
|
}
|
|
_on_close(event) {
|
|
this.on_close(null);
|
|
}
|
|
_on_error(event) {
|
|
this.on_close(event.toString());
|
|
}
|
|
close() {
|
|
this.socket.close();
|
|
}
|
|
send(packet) {
|
|
this.socket.send(packet.getId() + packet.getData());
|
|
}
|
|
}
|
|
class MainPlayer extends Player {
|
|
constructor() {
|
|
super(0.0, 0.0, "unnamed player", "#5e6", 0, 0);
|
|
this.reset();
|
|
this.conn = null;
|
|
}
|
|
reset() {
|
|
super.reset();
|
|
this.walk_speed = 1;
|
|
this.jump_speed = 2;
|
|
this.gravity_speed = 0.5;
|
|
this.controls_x = 0;
|
|
this.controls_jump = false;
|
|
this.block_type = null;
|
|
this.all_block_types = [
|
|
"normal", "normal", "normal", "normal", "normal",
|
|
"normal", "normal", "normal", "normal", "normal"
|
|
];
|
|
}
|
|
onConnect(name) {
|
|
this.x = 0.0;
|
|
this.y = 0.0;
|
|
this.velocity_x = 0.0;
|
|
this.velocity_y = 0.0;
|
|
camera.x = 0.0;
|
|
camera.y = 0.0;
|
|
chatOpened = false;
|
|
chatMessages = [];
|
|
this.name = name;
|
|
this.color = "#5e6";
|
|
blocks = [];
|
|
this.reset();
|
|
}
|
|
register() {
|
|
document.addEventListener("keydown", (e) => {
|
|
let key = e.code;
|
|
if (chatOpened) {
|
|
if (key == "Backspace") {
|
|
chatTyping = chatTyping.slice(0, chatTyping.length - 1);
|
|
}
|
|
else if (key == "Enter") {
|
|
if (chatTyping == "") {
|
|
chatOpened = false;
|
|
return;
|
|
}
|
|
this.sendPacket(new MessagePacket(chatTyping));
|
|
chatTyping = "";
|
|
chatOpened = false;
|
|
}
|
|
else if (key == "Escape") {
|
|
chatOpened = false;
|
|
}
|
|
else if (e.key.length == 1) {
|
|
chatTyping += e.key;
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
if (key == "KeyD") {
|
|
this.controls_x = 1;
|
|
}
|
|
else if (key == "KeyA") {
|
|
this.controls_x = -1;
|
|
}
|
|
else if (key == "Space") {
|
|
this.controls_jump = true;
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
else if (key == "KeyR") {
|
|
if (this.conn == null) {
|
|
this.forceTeleport(0, 1);
|
|
}
|
|
}
|
|
else if (key == "KeyT") {
|
|
chatOpened = true;
|
|
}
|
|
if (e.key == "0")
|
|
this.block_type = null;
|
|
if ("123456789".includes(e.key))
|
|
this.block_type = this.all_block_types[parseInt(e.key) - 1];
|
|
if (allowed_key_to_send.includes(key)) {
|
|
this.sendPacket(new KeyPacket(key, true));
|
|
}
|
|
if (key == "Escape") {
|
|
this.closeConnection();
|
|
}
|
|
}
|
|
if (key == "F3") {
|
|
debugMode = !debugMode;
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
});
|
|
document.addEventListener("keyup", (e) => {
|
|
let key = e.code;
|
|
if ((key == "KeyD" && this.controls_x == 1)
|
|
|| (key == "KeyA" && this.controls_x == -1)) {
|
|
this.controls_x = 0;
|
|
}
|
|
else if (key == "Space" && this.controls_jump) {
|
|
this.controls_jump = false;
|
|
}
|
|
if (allowed_key_to_send.includes(key)) {
|
|
this.sendPacket(new KeyPacket(key, false));
|
|
}
|
|
});
|
|
canvas.addEventListener("wheel", e => {
|
|
if (e.deltaY > 0) {
|
|
camera.size *= 0.5 * (e.deltaY / 114);
|
|
}
|
|
else {
|
|
camera.size *= 2 * (e.deltaY / -114);
|
|
}
|
|
e.preventDefault();
|
|
return false;
|
|
});
|
|
canvas.addEventListener("mousedown", e => {
|
|
let rect = canvas.getBoundingClientRect();
|
|
let size = 16 * camera.size;
|
|
let x = Math.round((e.clientX - rect.x) / size - (width / size / 2) + camera.x);
|
|
let y = Math.round((height - (e.clientY - rect.y)) / size - (height / size / 2) + camera.y);
|
|
if (e.buttons == 2 && this.block_type != null) {
|
|
if (this.conn == null) {
|
|
placeBlock(new Block(x, y, "#555", true, this.block_type));
|
|
}
|
|
this.sendPacket(new PlaceBlockPacket(x, y, this.block_type));
|
|
}
|
|
else if (e.buttons == 1) {
|
|
if (this.conn == null) {
|
|
removeBlock(x, y);
|
|
}
|
|
this.sendPacket(new DestroyBlockPacket(x, y));
|
|
}
|
|
});
|
|
}
|
|
sendPacket(packet) {
|
|
if (this.conn != null) {
|
|
this.conn.send(packet);
|
|
}
|
|
}
|
|
closeConnection() {
|
|
if (this.conn != null)
|
|
this.conn.close();
|
|
this.conn = null;
|
|
}
|
|
onPacket(packet) {
|
|
let packet_id = packet.getId();
|
|
let packet_data = packet.getData();
|
|
if (packet_id == "K") {
|
|
setServerError(packet_data[0]);
|
|
this.closeConnection();
|
|
}
|
|
if (packet_id == "N") {
|
|
this.name = packet_data[0];
|
|
}
|
|
if (packet_id == "C") {
|
|
this.color = packet_data[0];
|
|
}
|
|
if (packet_id == "M") {
|
|
chatMessages.unshift(...packet_data);
|
|
}
|
|
if (packet_id == "R") {
|
|
this.velocity_x = parseFloat(packet_data[0]) - this.x + parseFloat(packet_data[2]);
|
|
this.velocity_y = parseFloat(packet_data[1]) - this.y + parseFloat(packet_data[3]);
|
|
}
|
|
if (packet_id == "P") {
|
|
let x = parseFloat(packet_data[0]);
|
|
let y = parseFloat(packet_data[1]);
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
if (packet_id == "V") {
|
|
let x = parseFloat(packet_data[0]);
|
|
let y = parseFloat(packet_data[1]);
|
|
this.velocity_x = x;
|
|
this.velocity_y = y;
|
|
}
|
|
if (packet_id == "S") {
|
|
let speed_type = packet_data[0];
|
|
let speed = parseFloat(packet_data[1]);
|
|
if (speed_type == "W") {
|
|
this.walk_speed = speed;
|
|
}
|
|
else if (speed_type == "J") {
|
|
this.jump_speed = speed;
|
|
}
|
|
else if (speed_type == "G") {
|
|
this.gravity_speed = speed;
|
|
}
|
|
}
|
|
if (packet_id == "W") {
|
|
for (const data of packet_data) {
|
|
let type = data[0];
|
|
let create = data[1] == "1";
|
|
let params = data.slice(2).split(",");
|
|
if (type == "B") {
|
|
let x = parseFloat(params[0]);
|
|
let y = parseFloat(params[1]);
|
|
if (create) {
|
|
let collides = params[2] == "1";
|
|
let type = params[3];
|
|
let color = params[4];
|
|
let block = getBlock(x, y);
|
|
if (block != null) {
|
|
block.x = x;
|
|
block.y = y;
|
|
block.color = color;
|
|
block.collides = collides;
|
|
block.type = type;
|
|
}
|
|
else {
|
|
placeBlock(new Block(x, y, color, collides, type));
|
|
}
|
|
}
|
|
else {
|
|
removeBlock(x, y);
|
|
}
|
|
}
|
|
else if (type == "P") {
|
|
let name = params[0];
|
|
if (create) {
|
|
let x = parseFloat(params[1]);
|
|
let y = parseFloat(params[2]);
|
|
let vel_x = parseFloat(params[3]);
|
|
let vel_y = parseFloat(params[4]);
|
|
let color = params[5];
|
|
let player = getPlayer(name);
|
|
if (player != null) {
|
|
player.x = x;
|
|
player.y = y;
|
|
player.color = color;
|
|
player.velocity_x = vel_x;
|
|
player.velocity_y = vel_y;
|
|
}
|
|
else {
|
|
placeBlock(new Player(x, y, name, color, vel_x, vel_y));
|
|
}
|
|
}
|
|
else {
|
|
removePlayer(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (packet_id == "B") {
|
|
this.all_block_types = packet_data.slice(0, 9);
|
|
this.block_type = null;
|
|
}
|
|
}
|
|
tick() {
|
|
super.tick(false);
|
|
let vel_x = this.velocity_x;
|
|
let vel_y = this.velocity_y;
|
|
this.velocity_x += this.controls_x * this.walk_speed;
|
|
if (this.controls_jump && this.on_ground) {
|
|
this.velocity_y += this.jump_speed;
|
|
this.on_ground = false;
|
|
}
|
|
else {
|
|
this.velocity_y -= this.gravity_speed;
|
|
}
|
|
this.collide();
|
|
ticksAlive++;
|
|
this.sendPacket(new VelocityPacket(this.velocity_x - vel_x, this.velocity_y - vel_y));
|
|
this.sendPacket(new PositionPacket(this.x, this.y));
|
|
}
|
|
render() {
|
|
super.render();
|
|
camera.x = Math.round(lerp(camera.x, this.x, 0.075) * 1000) / 1000;
|
|
camera.y = Math.round(lerp(camera.y, this.y, 0.075) * 1000) / 1000;
|
|
}
|
|
renderText() {
|
|
super.renderText();
|
|
if (this.block_type != null) {
|
|
ctx.fillStyle = "#76d";
|
|
ctx.font = "15px monospace";
|
|
ctx.fillText("selected: " + this.block_type, 0, 15);
|
|
}
|
|
if (debugMode) {
|
|
ctx.fillStyle = "#de7";
|
|
ctx.font = "20px monospace";
|
|
ctx.fillText("x: " + this.x, 0, 20);
|
|
ctx.fillText("y: " + this.y, 0, 40);
|
|
ctx.fillText("velocity_x: " + this.velocity_x, 0, 60);
|
|
ctx.fillText("velocity_y: " + this.velocity_y, 0, 80);
|
|
ctx.fillText("camera.x: " + camera.x, 0, 100);
|
|
ctx.fillText("camera.y: " + camera.y, 0, 120);
|
|
}
|
|
if (!chatOpened) {
|
|
ctx.fillStyle = "#22222288";
|
|
ctx.fillRect(5, height - 35, width * 0.4, 30);
|
|
ctx.fillStyle = "#aaaaaa88";
|
|
ctx.font = "15px monospace";
|
|
ctx.fillText("Нажмите T для чата", 15, height - 15);
|
|
}
|
|
else {
|
|
ctx.fillStyle = "#22222288";
|
|
ctx.fillRect(5, height - 35, width - 10, 30);
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.moveTo(5, height - 35);
|
|
ctx.lineTo(width - 5, height - 35);
|
|
ctx.lineTo(width - 5, height - 5);
|
|
ctx.lineTo(5, height - 5);
|
|
ctx.lineTo(5, height - 35);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
ctx.font = "15px monospace";
|
|
if (chatTyping.length == 0) {
|
|
ctx.fillStyle = "#aaaaaa88";
|
|
ctx.fillText("Напишите сообщение...", 15, height - 15);
|
|
}
|
|
else {
|
|
ctx.fillStyle = "#ffffff";
|
|
let text_width = ctx.measureText(chatTyping).width;
|
|
ctx.fillText(chatTyping, Math.min(10, width - text_width - 10), height - 15);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
if (chatMessages.length > 0) {
|
|
let draw_message = (message) => {
|
|
let lines = wrapText(ctx, message, message_width - 20);
|
|
let height = lines.length * 20 + 5 + 5;
|
|
let top = message_bottom - height;
|
|
ctx.fillStyle = "#22222288";
|
|
ctx.fillRect(5, top, message_width, height);
|
|
let y = 5;
|
|
for (let line of lines) {
|
|
ctx.fillStyle = "#ffffff";
|
|
ctx.fillText(line, 15, top + y + 15);
|
|
y += 5 + 20;
|
|
}
|
|
message_bottom -= height + 5;
|
|
};
|
|
let message_width = width * 0.4;
|
|
let message_bottom = height - 35 - 5;
|
|
if (chatOpened) {
|
|
chatMessages.forEach(draw_message);
|
|
}
|
|
else {
|
|
draw_message(chatMessages[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var ticksAlive = 0;
|
|
var debugMode = false;
|
|
var camera = {
|
|
x: 0.0,
|
|
y: 0.0,
|
|
size: 1.5
|
|
};
|
|
var chatOpened = false;
|
|
var chatMessages = [];
|
|
var chatTyping = "";
|
|
var player = new MainPlayer();
|
|
player.register();
|
|
var blocks = [];
|
|
const allowed_key_to_send = [
|
|
"KeyR", "KeyW", "KeyE", "KeyQ", "KeyS",
|
|
"Numpad1", "Numpad2", "Numpad3", "Numpad4", "Numpad5",
|
|
"Numpad6", "Numpad7", "Numpad8", "Numpad9", "Numpad0",
|
|
"ShiftLeft", "ControlLeft", "Enter",
|
|
"F1", "F2", "KeyZ", "KeyX", "KeyC"
|
|
];
|
|
function connectServer(address, name) {
|
|
player.closeConnection();
|
|
player.onConnect(name);
|
|
try {
|
|
let conn = new Connection(address, player.onPacket, (e) => {
|
|
player.conn = null;
|
|
setServerError(e == null ? "Connection closed due to error" : e);
|
|
resetWorld();
|
|
});
|
|
conn.send(new JoinPacket(name));
|
|
}
|
|
catch (exception) {
|
|
setServerError(exception);
|
|
}
|
|
}
|
|
function resetWorld() {
|
|
player.onConnect("unnamed player");
|
|
blocks = [];
|
|
blocks.push(new Block(-1, -1, "#555", true, "normal"));
|
|
blocks.push(new Block(0, -1, "#a67", true, "spawn"));
|
|
blocks.push(new Block(1, -1, "#555", true, "normal"));
|
|
}
|
|
function getBlock(x, y) {
|
|
let value = blocks.find(o => !(o instanceof Player) && o.x == x && o.y == y);
|
|
if (typeof value === "undefined") {
|
|
return null;
|
|
}
|
|
else {
|
|
return value;
|
|
}
|
|
}
|
|
function placeBlock(block) {
|
|
blocks.push(block);
|
|
}
|
|
function removeBlock(x, y) {
|
|
blocks = blocks.filter(o => o instanceof Player || o.x != x || o.y != y);
|
|
}
|
|
function getPlayer(name) {
|
|
let value = blocks.find(o => o instanceof Player && o.name == name);
|
|
if (typeof value === "undefined") {
|
|
return null;
|
|
}
|
|
else {
|
|
return value;
|
|
}
|
|
}
|
|
function removePlayer(name) {
|
|
blocks = blocks.filter(o => !(o instanceof Player) || o.name != name);
|
|
}
|
|
function render() {
|
|
ctx.fillStyle = "#333";
|
|
ctx.fillRect(0, 0, width, height);
|
|
for (const block of blocks)
|
|
block.render();
|
|
for (const block of blocks)
|
|
block.renderText();
|
|
player.render();
|
|
player.renderText();
|
|
}
|
|
function tick() {
|
|
for (const block of blocks)
|
|
block.tick();
|
|
player.tick();
|
|
}
|
|
function renderTick() {
|
|
for (const block of blocks)
|
|
block.renderTick();
|
|
player.renderTick();
|
|
}
|
|
resetWorld();
|