From e194968af5ec691aafca39043a0bceebc741ea9d Mon Sep 17 00:00:00 2001 From: MeexReay Date: Sun, 22 Dec 2024 01:29:44 +0300 Subject: [PATCH] client build update --- client/build.sh | 9 + client/dest.script.js | 673 ++++++++++++++++++++++++++++++ client/dest/block.js | 112 +++++ client/dest/core.js | 90 ++++ client/dest/main.js | 53 +++ client/dest/network.js | 71 ++++ client/dest/player.js | 347 +++++++++++++++ client/dest/script.js | 673 ++++++++++++++++++++++++++++++ client/dest/script.min.js | 1 + client/index.html | 6 +- client/src/block.ts | 1 - client/src/core.ts | 2 +- client/src/{script.ts => main.ts} | 2 - client/src/player.ts | 1 + client/tsconfig.json | 17 + 15 files changed, 2050 insertions(+), 8 deletions(-) create mode 100755 client/build.sh create mode 100644 client/dest.script.js create mode 100644 client/dest/block.js create mode 100644 client/dest/core.js create mode 100644 client/dest/main.js create mode 100644 client/dest/network.js create mode 100644 client/dest/player.js create mode 100644 client/dest/script.js create mode 100644 client/dest/script.min.js rename client/src/{script.ts => main.ts} (98%) create mode 100644 client/tsconfig.json diff --git a/client/build.sh b/client/build.sh new file mode 100755 index 0000000..d0b5e75 --- /dev/null +++ b/client/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +tsc +cat dest/main.js > dest/script.js +cat dest/block.js >> dest/script.js +cat dest/network.js >> dest/script.js +cat dest/player.js >> dest/script.js +cat dest/core.js >> dest/script.js +terser dest/script.js -o dest/script.min.js --compress --mangle \ No newline at end of file diff --git a/client/dest.script.js b/client/dest.script.js new file mode 100644 index 0000000..ec3ce83 --- /dev/null +++ b/client/dest.script.js @@ -0,0 +1,673 @@ +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()); + } +} +var ticksAlive = 0; +var debugMode = false; +var camera = { + x: 0.0, + y: 0.0, + size: 1.5 +}; +var chatOpened = false; +var chatMessages = []; +var chatTyping = ""; +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 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(); diff --git a/client/dest/block.js b/client/dest/block.js new file mode 100644 index 0000000..3156695 --- /dev/null +++ b/client/dest/block.js @@ -0,0 +1,112 @@ +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; + } +} diff --git a/client/dest/core.js b/client/dest/core.js new file mode 100644 index 0000000..7ba3ef0 --- /dev/null +++ b/client/dest/core.js @@ -0,0 +1,90 @@ +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(); diff --git a/client/dest/main.js b/client/dest/main.js new file mode 100644 index 0000000..7108b87 --- /dev/null +++ b/client/dest/main.js @@ -0,0 +1,53 @@ +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); diff --git a/client/dest/network.js b/client/dest/network.js new file mode 100644 index 0000000..85521bf --- /dev/null +++ b/client/dest/network.js @@ -0,0 +1,71 @@ +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()); + } +} diff --git a/client/dest/player.js b/client/dest/player.js new file mode 100644 index 0000000..d34f0e8 --- /dev/null +++ b/client/dest/player.js @@ -0,0 +1,347 @@ +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]); + } + } + } +} diff --git a/client/dest/script.js b/client/dest/script.js new file mode 100644 index 0000000..e4e3d25 --- /dev/null +++ b/client/dest/script.js @@ -0,0 +1,673 @@ +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(); diff --git a/client/dest/script.min.js b/client/dest/script.min.js new file mode 100644 index 0000000..e07b7a0 --- /dev/null +++ b/client/dest/script.min.js @@ -0,0 +1 @@ +const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),width=640,height=480,server_ip=document.getElementById("server-ip"),server_nick=document.getElementById("server-nick"),connect_server=document.getElementById("connect-server"),server_error=document.getElementById("server-error");function wrapText(e,t,s){const c=[];let l="";for(let o=0;os&&l?(c.push(l),l=n):l=i}return l&&c.push(l),c}function lerp(e,t,s){return e+s*(t-e)}function setServerError(e){server_error.innerText=e}connect_server.onclick=()=>{let e=server_ip.value,t=server_nick.value;return setServerError(""),0==e.length?setServerError("введите айпи пж"):0==t.length?setServerError("введите ник пж"):(e.includes(":")||(e+=":8000"),void connectServer(e,t))},setInterval(tick,50),setInterval(renderTick,1e3/60);let renderTimer=()=>{render(),requestAnimationFrame(renderTimer)};requestAnimationFrame(renderTimer);class Block{constructor(e,t,s,c,l){this.x=e,this.y=t,this.color=s,this.collides=c,this.type=l}render(){let e=this.translate_to_camera();this.is_need_render(e)&&(ctx.fillStyle=this.color,ctx.fillRect(...e))}is_need_render(e){return e[0]+e[2]>0||e[1]+e[3]>0||e[0]<640||e[1]<480}translate_to_camera(){let e=16*camera.size;return[this.x*e-e/2+(320-camera.x*e),480-(this.y+1)*e+e/2-(240-camera.y*e),e,e]}tick(){}renderTick(){}on_collide(e,t,s){0!=t&&(e.velocity_x=this.x+t-e.x),0!=s&&(e.velocity_y=this.y+s-e.y)}renderText(){}}class Player extends Block{constructor(e,t,s,c,l,o){super(e,t,c,!0,null),this.velocity_x=l,this.velocity_y=o,this.name=s}reset(){this.on_ground=!1}on_collide(e,t,s){super.on_collide(e,t,s)}tick(e=!0){this.x=Math.round(100*this.x)/100,this.y=Math.round(100*this.y)/100,this.velocity_x=Math.round(100*this.velocity_x)/100,this.velocity_y=Math.round(100*this.velocity_y)/100,e&&this.collide()}collide(){this.on_ground=!1;for(const e of blocks){if(!e.collides)continue;let t=0,s=0;this.x>e.x-1&&this.xe.y&&this.y+this.velocity_y-1e.y-1&&(s=-1)),this.y>e.y-1&&this.ye.x&&this.x+this.velocity_x-1e.x-1&&(t=-1)),e.on_collide(this,t,s)}}renderTick(){this.velocity_x*=.5,this.velocity_y*=.5,this.x+=this.velocity_x,this.y+=this.velocity_y}renderText(){super.renderText();let e=this.translate_to_camera();if(this.is_need_render(e)){ctx.fillStyle="#ddd",ctx.font="15px monospace";let t=ctx.measureText(this.name).width;ctx.fillText(this.name,e[0]+e[2]/2-t/2,e[1]-5)}}teleport(e,t){this.velocity_x=e-this.x,this.velocity_y=t-this.y}forceTeleport(e,t){this.x=e,this.y=t,this.velocity_x=0,this.velocity_y=0}}class Packet{constructor(e,t){this.id=e,this.data=t}getId(){return this.id}getData(){return this.data}static fromString(e){return new Packet(e[0],e.slice(1).split("\n"))}}class JoinPacket extends Packet{constructor(e){super("J",[e])}}class MessagePacket extends Packet{constructor(e){super("M",[e])}}class KeyPacket extends Packet{constructor(e,t){super("K",[e,t?"1":"0"])}}class PlaceBlockPacket extends Packet{constructor(e,t,s){super("P",[e.toString(),t.toString(),s])}}class DestroyBlockPacket extends Packet{constructor(e,t){super("D",[e.toString(),t.toString()])}}class PositionPacket extends Packet{constructor(e,t){super("X",[e.toString(),t.toString()])}}class VelocityPacket extends Packet{constructor(e,t){super("V",[e.toString(),t.toString()])}}class Connection{constructor(e,t,s){this.socket=new WebSocket("ws://"+e,"cubic"),this.on_packet=t,this.on_close=s,this.socket.onmessage=this._on_message,this.socket.onclose=this._on_close,this.socket.onerror=this._on_error}_on_message(e){this.on_packet(Packet.fromString(e.data))}_on_close(e){this.on_close(null)}_on_error(e){this.on_close(e.toString())}close(){this.socket.close()}send(e){this.socket.send(e.getId()+e.getData())}}class MainPlayer extends Player{constructor(){super(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=.5,this.controls_x=0,this.controls_jump=!1,this.block_type=null,this.all_block_types=["normal","normal","normal","normal","normal","normal","normal","normal","normal","normal"]}onConnect(e){this.x=0,this.y=0,this.velocity_x=0,this.velocity_y=0,camera.x=0,camera.y=0,chatOpened=!1,chatMessages=[],this.name=e,this.color="#5e6",blocks=[],this.reset()}register(){document.addEventListener("keydown",(e=>{let t=e.code;if(chatOpened){if("Backspace"==t)chatTyping=chatTyping.slice(0,chatTyping.length-1);else if("Enter"==t){if(""==chatTyping)return void(chatOpened=!1);this.sendPacket(new MessagePacket(chatTyping)),chatTyping="",chatOpened=!1}else if("Escape"==t)chatOpened=!1;else if(1==e.key.length)return chatTyping+=e.key,e.preventDefault(),!1}else{if("KeyD"==t)this.controls_x=1;else if("KeyA"==t)this.controls_x=-1;else{if("Space"==t)return this.controls_jump=!0,e.preventDefault(),!1;"KeyR"==t?null==this.conn&&this.forceTeleport(0,1):"KeyT"==t&&(chatOpened=!0)}"0"==e.key&&(this.block_type=null),"123456789".includes(e.key)&&(this.block_type=this.all_block_types[parseInt(e.key)-1]),allowed_key_to_send.includes(t)&&this.sendPacket(new KeyPacket(t,!0)),"Escape"==t&&this.closeConnection()}if("F3"==t)return debugMode=!debugMode,e.preventDefault(),!1})),document.addEventListener("keyup",(e=>{let t=e.code;"KeyD"==t&&1==this.controls_x||"KeyA"==t&&-1==this.controls_x?this.controls_x=0:"Space"==t&&this.controls_jump&&(this.controls_jump=!1),allowed_key_to_send.includes(t)&&this.sendPacket(new KeyPacket(t,!1))})),canvas.addEventListener("wheel",(e=>(e.deltaY>0?camera.size*=e.deltaY/114*.5:camera.size*=e.deltaY/-114*2,e.preventDefault(),!1))),canvas.addEventListener("mousedown",(e=>{let t=canvas.getBoundingClientRect(),s=16*camera.size,c=Math.round((e.clientX-t.x)/s-640/s/2+camera.x),l=Math.round((480-(e.clientY-t.y))/s-480/s/2+camera.y);2==e.buttons&&null!=this.block_type?(null==this.conn&&placeBlock(new Block(c,l,"#555",!0,this.block_type)),this.sendPacket(new PlaceBlockPacket(c,l,this.block_type))):1==e.buttons&&(null==this.conn&&removeBlock(c,l),this.sendPacket(new DestroyBlockPacket(c,l)))}))}sendPacket(e){null!=this.conn&&this.conn.send(e)}closeConnection(){null!=this.conn&&this.conn.close(),this.conn=null}onPacket(e){let t=e.getId(),s=e.getData();if("K"==t&&(setServerError(s[0]),this.closeConnection()),"N"==t&&(this.name=s[0]),"C"==t&&(this.color=s[0]),"M"==t&&chatMessages.unshift(...s),"R"==t&&(this.velocity_x=parseFloat(s[0])-this.x+parseFloat(s[2]),this.velocity_y=parseFloat(s[1])-this.y+parseFloat(s[3])),"P"==t){let e=parseFloat(s[0]),t=parseFloat(s[1]);this.x=e,this.y=t}if("V"==t){let e=parseFloat(s[0]),t=parseFloat(s[1]);this.velocity_x=e,this.velocity_y=t}if("S"==t){let e=s[0],t=parseFloat(s[1]);"W"==e?this.walk_speed=t:"J"==e?this.jump_speed=t:"G"==e&&(this.gravity_speed=t)}if("W"==t)for(const e of s){let t=e[0],s="1"==e[1],c=e.slice(2).split(",");if("B"==t){let e=parseFloat(c[0]),t=parseFloat(c[1]);if(s){let s="1"==c[2],l=c[3],o=c[4],n=getBlock(e,t);null!=n?(n.x=e,n.y=t,n.color=o,n.collides=s,n.type=l):placeBlock(new Block(e,t,o,s,l))}else removeBlock(e,t)}else if("P"==t){let e=c[0];if(s){let t=parseFloat(c[1]),s=parseFloat(c[2]),l=parseFloat(c[3]),o=parseFloat(c[4]),n=c[5],i=getPlayer(e);null!=i?(i.x=t,i.y=s,i.color=n,i.velocity_x=l,i.velocity_y=o):placeBlock(new Player(t,s,e,n,l,o))}else removePlayer(e)}}"B"==t&&(this.all_block_types=s.slice(0,9),this.block_type=null)}tick(){super.tick(!1);let e=this.velocity_x,t=this.velocity_y;this.velocity_x+=this.controls_x*this.walk_speed,this.controls_jump&&this.on_ground?(this.velocity_y+=this.jump_speed,this.on_ground=!1):this.velocity_y-=this.gravity_speed,this.collide(),ticksAlive++,this.sendPacket(new VelocityPacket(this.velocity_x-e,this.velocity_y-t)),this.sendPacket(new PositionPacket(this.x,this.y))}render(){super.render(),camera.x=Math.round(1e3*lerp(camera.x,this.x,.075))/1e3,camera.y=Math.round(1e3*lerp(camera.y,this.y,.075))/1e3}renderText(){if(super.renderText(),null!=this.block_type&&(ctx.fillStyle="#76d",ctx.font="15px monospace",ctx.fillText("selected: "+this.block_type,0,15)),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)),chatOpened){if(ctx.fillStyle="#22222288",ctx.fillRect(5,445,630,30),ctx.save(),ctx.beginPath(),ctx.moveTo(5,445),ctx.lineTo(635,445),ctx.lineTo(635,475),ctx.lineTo(5,475),ctx.lineTo(5,445),ctx.closePath(),ctx.clip(),ctx.font="15px monospace",0==chatTyping.length)ctx.fillStyle="#aaaaaa88",ctx.fillText("Напишите сообщение...",15,465);else{ctx.fillStyle="#ffffff";let e=ctx.measureText(chatTyping).width;ctx.fillText(chatTyping,Math.min(10,640-e-10),465)}ctx.restore()}else ctx.fillStyle="#22222288",ctx.fillRect(5,445,256,30),ctx.fillStyle="#aaaaaa88",ctx.font="15px monospace",ctx.fillText("Нажмите T для чата",15,465);if(chatMessages.length>0){let e=e=>{let c=wrapText(ctx,e,t-20),l=20*c.length+5+5,o=s-l;ctx.fillStyle="#22222288",ctx.fillRect(5,o,t,l);let n=5;for(let e of c)ctx.fillStyle="#ffffff",ctx.fillText(e,15,o+n+15),n+=25;s-=l+5},t=256,s=440;chatOpened?chatMessages.forEach(e):e(chatMessages[0])}}}var ticksAlive=0,debugMode=!1,camera={x:0,y:0,size:1.5},chatOpened=!1,chatMessages=[],chatTyping="",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(e,t){player.closeConnection(),player.onConnect(t);try{new Connection(e,player.onPacket,(e=>{player.conn=null,setServerError(null==e?"Connection closed due to error":e),resetWorld()})).send(new JoinPacket(t))}catch(e){setServerError(e)}}function resetWorld(){player.onConnect("unnamed player"),(blocks=[]).push(new Block(-1,-1,"#555",!0,"normal")),blocks.push(new Block(0,-1,"#a67",!0,"spawn")),blocks.push(new Block(1,-1,"#555",!0,"normal"))}function getBlock(e,t){let s=blocks.find((s=>!(s instanceof Player)&&s.x==e&&s.y==t));return void 0===s?null:s}function placeBlock(e){blocks.push(e)}function removeBlock(e,t){blocks=blocks.filter((s=>s instanceof Player||s.x!=e||s.y!=t))}function getPlayer(e){let t=blocks.find((t=>t instanceof Player&&t.name==e));return void 0===t?null:t}function removePlayer(e){blocks=blocks.filter((t=>!(t instanceof Player)||t.name!=e))}function render(){ctx.fillStyle="#333",ctx.fillRect(0,0,640,480);for(const e of blocks)e.render();for(const e of blocks)e.renderText();player.render(),player.renderText()}function tick(){for(const e of blocks)e.tick();player.tick()}function renderTick(){for(const e of blocks)e.renderTick();player.renderTick()}resetWorld(); \ No newline at end of file diff --git a/client/index.html b/client/index.html index 34f0d25..731859a 100644 --- a/client/index.html +++ b/client/index.html @@ -72,11 +72,9 @@
- репозиторий гитхаб
- протокол сервера
- готовая реализация сервера на Python + репозиторий гитхаб. там же протокол сервер и готовая реализация сервера на Python
- + \ No newline at end of file diff --git a/client/src/block.ts b/client/src/block.ts index 33b9b91..ddcc2e7 100644 --- a/client/src/block.ts +++ b/client/src/block.ts @@ -1,4 +1,3 @@ - class Block { public x: number public y: number diff --git a/client/src/core.ts b/client/src/core.ts index 546cd92..fb5038a 100644 --- a/client/src/core.ts +++ b/client/src/core.ts @@ -11,7 +11,7 @@ var camera = { } var chatOpened = false -var chatMessages = [] +var chatMessages: string[] = [] var chatTyping = "" var player = new MainPlayer() diff --git a/client/src/script.ts b/client/src/main.ts similarity index 98% rename from client/src/script.ts rename to client/src/main.ts index 18e991e..73c749e 100644 --- a/client/src/script.ts +++ b/client/src/main.ts @@ -55,8 +55,6 @@ connect_server.onclick = () => { if (nick.length == 0) return setServerError("введите ник пж") if (!ip.includes(":")) ip += ":8000" - player.closeConnection() - connectServer(ip, nick) } diff --git a/client/src/player.ts b/client/src/player.ts index 574b92f..e481c3b 100644 --- a/client/src/player.ts +++ b/client/src/player.ts @@ -1,3 +1,4 @@ + class MainPlayer extends Player { public conn: Connection | null public walk_speed: number diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..bbe430c --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "amd", + "outDir": "./dest", + "rootDir": "./src", + "removeComments": true, + "strict": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": ["DOM", "ES2016"], + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": [] + } + \ No newline at end of file