diff --git a/README.md b/README.md index 88cfa0c..662e115 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ ``` заход игрока [J]: {ник_игрока} отправить velocity [V]: {vel_x}, {vel_y} +отправить позицию [X]: {x}, {y} установить блок [P]: {x}, {y}, {тип} сломать блок [D]: {x}, {y} нажатие кнопки (список кнопок ниже) [K]: {кнопка}, {нажата ли} diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..34f0d25 --- /dev/null +++ b/client/index.html @@ -0,0 +1,82 @@ + + + + + + cubic + + + + + + + + + + + + + +
+

Cubic

+

игра кубик переделанная на js

+ +
+ +
+
+
+
+ +
+ + + +
+ +

список серверов:

+ + + +
+ +

инструкция по игре:

+ +
+ходить через кнопки A и D
+прыгать кнопкой Space
+выбрать блок цифрами
+поставить блок через ПКМ только перед этим выберите
+ломать блоки через ЛКМ
+меню отладки на F3
+ресетнуться на кнопку R
+открыть чат кнопка T
+        
+ +
+ + репозиторий гитхаб
+ протокол сервера
+ готовая реализация сервера на Python +
+ + + + \ No newline at end of file diff --git a/client/src/block.ts b/client/src/block.ts new file mode 100644 index 0000000..e69de29 diff --git a/client/src/network.ts b/client/src/network.ts new file mode 100644 index 0000000..38417a7 --- /dev/null +++ b/client/src/network.ts @@ -0,0 +1,55 @@ +interface Packet { + getId(): string + getData(): string +} + +function createPacket(id: string, ...params: string[]): Packet { + return { + getId: () => id, + getData: () => params.join("\n"), + } +} + +function fromPacket(data: string): Packet { + return createPacket(data[0], ...data.slice(1).split("\n")) +} + +class Connection { + private socket: WebSocket + private on_packet: (packet: Packet) => {} + private on_close: (error: string | null) => {} + + constructor( + socket: WebSocket, + on_packet: (packet: Packet) => {}, + on_close: (error: string | null) => {} + ) { + this.socket = socket + 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 + } + + private _on_message(event: MessageEvent) { + this.on_packet(fromPacket(event.data)) + } + + private _on_close(event: CloseEvent) { + this.on_close(null) + } + + private _on_error(event: Event) { + this.on_close(event.toString()) + } + + close() { + this.socket.close() + } + + send(packet: Packet) { + this.socket.send(packet.getId()+packet.getData()) + } +} \ No newline at end of file diff --git a/client/src/player.ts b/client/src/player.ts new file mode 100644 index 0000000..e69de29 diff --git a/client/src/render.ts b/client/src/render.ts new file mode 100644 index 0000000..e69de29 diff --git a/client/src/script.ts b/client/src/script.ts new file mode 100644 index 0000000..1b40e95 --- /dev/null +++ b/client/src/script.ts @@ -0,0 +1,757 @@ +const canvas = document.getElementById("game") as HTMLCanvasElement +const ctx = canvas.getContext("2d"); + +const width = 640 +const height = 480 + +function lerp(a: number, b: number, alpha: number): number { + return a + alpha * ( b - a ) +} + +const server_ip = document.getElementById("server-ip") as HTMLInputElement +const server_nick = document.getElementById("server-nick") as HTMLInputElement +const connect_server = document.getElementById("connect-server") as HTMLButtonElement +const server_error = document.getElementById("server-error") as HTMLSpanElement + +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" +] + +connect_server.onclick = () => { + let ip = server_ip.value + let nick = server_nick.value + + server_error.innerText = "" + + if (ip.length == 0) { + server_error.innerText = "введите айпи пж" + return + } + + if (nick.length == 0) { + server_error.innerText = "введите ник пж" + return + } + + if (!ip.includes(":")) { + ip += ":8000" + } + + if (typeof player.socket !== "undefined") { + player.socket.close() + } + + player.onConnect(nick) + + blocks = [] + + try { + let socket = new WebSocket( + "ws://"+ip, + "cubic", + ) + + socket.onopen = e => { + socket.send("J"+nick); + player.socket = socket + } + + socket.onmessage = e => { + player.onRecvPacket(e.data[0], e.data.slice(1).split("\n")) + } + + socket.onclose = e => { + player.socket = undefined + resetWorld() + } + + socket.onerror = () => { + server_error.innerText = "Connection closed due to error" + resetWorld() + } + } catch (exception) { + server_error.innerText = exception + } +} + +function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string[] { + 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; +} + +var ticksAlive = 0 + +var debugMode = false + +var camera = { + x: 0.0, + y: 0.0, + size: 1.5 +} + +var chatOpened = false +var chatMessages = [] +var chatTyping = "" + +type Rect = [number, number, number, number] + +class Block { + public x: number + public y: number + public color: string + public collides: boolean + public type: string + + constructor( + x: number, + y: number, + color: string, + collides: boolean, + type: string, + ) { + 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: Rect): boolean { + return rect[0] + rect[2] > 0 || rect[1] + rect[3] > 0 || rect[0] < width || rect[1] < height + } + + translate_to_camera(): Rect { + 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: Player, x: number, y: number) { + 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 { + public velocity_x: number + public velocity_y: number + public name: string + public on_ground: boolean + + constructor( + x: number, + y: number, + name: string, + color: string, + velocity_x: number, + velocity_y: number, + ) { + 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: Player, x: number, y: number) { + super.on_collide(player, x, y) + // if (x != 0) { + // player.vel_x *= 0.5 + // this.vel_x = player.vel_x + // } + // if (y != 0) { + // player.vel_y *= 0.5 + // this.vel_y = player.vel_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: number, y: number) { + this.velocity_x = x - this.x + this.velocity_y = y - this.y + } + + forceTeleport(x: number, y: number) { + this.x = x + this.y = y + this.velocity_x = 0 + this.velocity_y = 0 + } +} + +class MainPlayer extends Player { + public socket: WebSocket | undefined + public walk_speed: number + public jump_speed: number + public gravity_speed: number + public controls_x: number + public controls_jump: boolean + public block_type: string | null + public all_block_types: string[] + + constructor() { + super(0.0, 0.0, "unnamed player", "#5e6", 0, 0) + + this.reset() + + this.socket = undefined + } + + 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" + + 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("M", 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 (typeof this.socket === "undefined") { + 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("K",key,"1") + } + + if (key == "Escape") { + if (typeof this.socket !== "undefined") { + this.socket.close() + } + } + } + + 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("K",key,"0") + } + }) + + 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 (typeof this.socket === "undefined") { + placeBlock(new Block(x,y,"#555",true,this.block_type)) + } + this.sendPacket("P", x.toString(), y.toString(), this.block_type) + } else if (e.buttons == 1) { + if (typeof this.socket === "undefined") { + removeBlock(x, y) + } + this.sendPacket("D", x.toString(), y.toString()) + } + }) + } + + onRecvPacket(packet_id: string, packet_data: string[]) { + if (packet_id == "K") { + server_error.innerText = packet_data[0] + this.socket.close() + this.socket = null + } + + 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 + } + } + + sendPacket(id: string, ...params: string[]) { + if (typeof this.socket === "undefined") return + this.socket.send(id+params.join("\n")) + } + + sendVelPacket(x, y) { + if (x == 0 && y == 0) return + this.sendPacket("V", x, y) + } + + 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.sendVelPacket(this.velocity_x-vel_x, this.velocity_y-vel_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: Block[] = [] + +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")); +} + +resetWorld() + +function getBlock(x: number, y: number): Block { + 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: Block) { + blocks.push(block); +} + +function removeBlock(x: number, y: number) { + blocks = blocks.filter(o => o instanceof Player || o.x != x || o.y != y) +} + +function getPlayer(name: string): Player | null { + let value = blocks.find(o => o instanceof Player && o.name == name) as Player + if (typeof value === "undefined") { + return null + } else { + return value + } +} + +function removePlayer(name: string) { + blocks = blocks.filter(o => !(o instanceof Player) || o.name != name) +} + +setInterval(() => { + for (const block of blocks) + block.tick() + player.tick() +}, 1000 / 20) + +setInterval(() => { + for (const block of blocks) + block.renderTick() + player.renderTick() +}, 1000 / 60) + +let renderTimer = () => { + 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() + + requestAnimationFrame(renderTimer) +} + +requestAnimationFrame(renderTimer) \ No newline at end of file diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..7c66794 --- /dev/null +++ b/server/main.py @@ -0,0 +1,390 @@ +from websockets.server import serve, ServerConnection +import random, sys, asyncio, time + +class Block: + def __init__(self, x, y, block_type, color, collides): + self.x = x + self.y = y + self.type = block_type + self.color = color + self.collides = collides + + async def tick(self): + pass + + async def render(self): + pass + + async def onCollide(self, player, x, y): + if x != 0: player.vel_x = self.x + x - player.x + if y != 0: player.vel_y = self.y + y - player.y + + if x != 0 or y != 0: # special blocks + if self.type == "jump_boost": + player.jump_speed = 5 + player.gravity_speed = 1.25 + player.on_ground = True + await writePacket(player.websocket, "S", ["J", "5"]) + await writePacket(player.websocket, "S", ["G", "1.25"]) + elif player.jump_speed != 2: + player.jump_speed = 2 + player.gravity_speed = 0.5 + await writePacket(player.websocket, "S", ["J", "2"]) + await writePacket(player.websocket, "S", ["G", "0.5"]) + + if self.type == "killer": + await player.setPos(*SPAWN) + await player.sendToPlayers() + + def toStatement(self, add=True): + return f"B1{self.x},{self.y},{int(self.collides)},{self.type},{self.color}" if add else f"B0{self.x},{self.y}" + +class Player(Block): + def __init__(self, websocket, x=None, y=None, name=None, color=None, vel_x=None, vel_y=None): + super().__init__(x, y, None, color, True) + self.x = x + self.y = y + + self.name = name + self.color = color + self.vel_x = vel_x + self.vel_y = vel_y + + self.websocket = websocket + + self.controls_x = 0 + self.controls_jump = False + + self.on_ground = False + + self.walk_speed = 1 + self.jump_speed = 2 + self.gravity_speed = 0.5 + + self.last_ + + async def setWalkSpeed(self, speed): + await writePacket(self.websocket, "S", ["W", str(speed)]) + self.walk_speed = speed + + async def setGravitySpeed(self, speed): + await writePacket(self.websocket, "S", ["G", str(speed)]) + self.gravity_speed = speed + + async def setJumpSpeed(self, speed): + await writePacket(self.websocket, "S", ["J", str(speed)]) + self.jump_speed = speed + + async def sendName(self, name): + await writePacket(self.websocket, "N", [name]) + + async def setName(self, name): + self.name = name + await self.sendName(name) + + async def setColor(self, color): + self.color = color + await writePacket(self.websocket, "C", [color]) + + async def setVel(self, x, y): + if x == self.vel_x and y == self.vel_y: return + + self.vel_x = x + self.vel_y = y + + await self.sendVel(x, y) + + async def setPos(self, x, y): + if x == self.x and y == self.y: return + + self.x = x + self.y = y + + await self.sendPos(x, y) + + async def sendVel(self, x, y): + await writePacket(self.websocket, "V", [str(x), str(y)]) + + async def sendPos(self, x, y): + await writePacket(self.websocket, "P", [str(x), str(y)]) + + async def sendMessage(self, message): + await writePacket(self.websocket, "M", message.split("\n")) + + async def sendWorld(self, statements): + if len(statements) == 0: return + await writePacket(self.websocket, "W", statements) + + async def sendBlockTypes(self, types): + await writePacket(self.websocket, "B", types) + + async def sendToPlayers(self): + for p in getPlayers(): + if p != self: + await p.sendWorld([self.toStatement()]) + + async def tick(self): + self.x = round(self.x * 100) / 100 + self.y = round(self.y * 100) / 100 + self.vel_x = round(self.vel_x * 100) / 100 + self.vel_y = round(self.vel_y * 100) / 100 + + if not self.on_ground: + self.vel_y -= self.gravity_speed + + await self.collide() + + async def collide(self): + global WORLD + + self.on_ground = False + + for block in WORLD: + if not block.collides: continue + if block == self: continue + + collide_x = 0 + collide_y = 0 + + if self.x > block.x-1 and self.x < block.x+1: + if self.y > block.y and self.y + self.vel_y - 1 < block.y: + self.on_ground = True + collide_y = 1 + if self.y < block.y and self.y + self.vel_y > block.y - 1: + collide_y = -1 + + if self.y > block.y-1 and self.y < block.y+1: + if self.x > block.x and self.x + self.vel_x - 1 < block.x: + collide_x = 1 + if self.x < block.x and self.x + self.vel_x > block.x - 1: + collide_x = -1 + + await block.onCollide(self, collide_x, collide_y) + + async def onCollide(self, player, x, y): + await super().onCollide(player, x, y) + # if x != 0: + # player.vel_x *= 0.5 + # self.vel_x = player.vel_x + # if y != 0: + # player.vel_y *= 0.5 + # self.vel_y = player.vel_y + # pass + + async def render(self): + self.vel_x *= 0.5 + self.vel_y *= 0.5 + self.x += self.vel_x + self.y += self.vel_y + # await self.setVel(self.vel_x * 0.5, self.vel_y * 0.5) + # await self.setPos(self.x + self.vel_x, self.y + self.vel_y) + return self.vel_x != 0 or self.vel_y != 0 + + async def keepAlive(self): + await writePacket(self.websocket, "R", [str(self.x), str(self.y), str(self.vel_x), str(self.vel_y)]) + + def toStatement(self, add=True): + return f"P1{self.name},{self.x},{self.y},{self.vel_x},{self.vel_y},{self.color}" if add else f"P0{self.name}" + +def getPlayers(): + global WORLD + for b in WORLD: + if type(b) == Player: + yield b + +def getPlayer(name): + global WORLD + for b in WORLD: + if type(b) == Player: + if b.name == name: + return b + +def current_milli_time(): + return round(time.time() * 1000) + +async def readPacket(websocket: ServerConnection) -> tuple[str, list[str]]: + data = await websocket.recv() + id,data = data[0], data[1:].splitlines() + print(id, data) + return id,data + +async def writePacket(websocket: ServerConnection, packet_id: str, packet_data: list[str]): + await websocket.send(packet_id + ("\n".join(packet_data))) + +async def handler(websocket: ServerConnection): + packet_id, packet_data = await readPacket(websocket) + + name = packet_data[0] + + if packet_id != "J": + await writePacket(websocket, "K", ["join packet is invalid"]) + return + if getPlayer(name) != None: + await writePacket(websocket, "K", ["this nickname is already in use"]) + return + + print(name, "joined to the server") + + try: + player = Player(websocket) + + await player.sendWorld([b.toStatement() for b in WORLD]) + + await player.sendBlockTypes(BLOCK_TYPES.keys()) + await player.setColor(random.choice(COLORS)) + await player.setPos(*SPAWN) + await player.setVel(0,0) + await player.setName(name) + await player.setWalkSpeed(0.5) + + await player.sendToPlayers() + + WORLD.append(player) + + while True: + packet_id, packet_data = await readPacket(websocket) + + if packet_id == "V": + vel_x, vel_y = float(packet_data[0]), float(packet_data[1]) + vel_x = max(min(vel_x, player.walk_speed), -player.walk_speed) + vel_y = max(min(vel_y, player.jump_speed), 0) + + player.vel_x += vel_x + + if player.on_ground: + player.vel_y += vel_y + player.on_ground = False + + await player.sendToPlayers() + + if packet_id == "K": + key,pressed = packet_data + pressed = pressed == "1" + + if key == "KeyR" and pressed: + await player.setPos(SPAWN[0], SPAWN[1]) + if key == "ShiftLeft": + if pressed: + await player.setWalkSpeed(1) + else: + await player.setWalkSpeed(0.5) + + if packet_id == "M": + message = packet_data[0] + message = f"{name} > {message}" + + for p in getPlayers(): + await p.sendMessage(message) + + print(message) + + if packet_id == "D": + x,y = packet_data + x,y = int(x),int(y) + + block = None + for i in WORLD: + if type(i) == Player: + continue + if i.x == x and i.y == y: + block = i + break + if not block: continue + + if block.type == "spawn": continue # spawn block protection + + if abs(x - player.x) ** 2 + abs(y - player.y) ** 2 > REACH_DISTANCE ** 2: + continue + + WORLD.remove(block) + + for p in getPlayers(): + await p.sendWorld([block.toStatement(False)]) + + if packet_id == "P": + x,y,block_type = packet_data + x,y = int(x),int(y) + + if block_type not in BLOCK_TYPES: + continue + + if abs(x - player.x) ** 2 + abs(y - player.y) ** 2 > REACH_DISTANCE ** 2: + continue + + found_block = False + for i in WORLD: + if type(i) == Player: + continue + if i.x == x and i.y == y: + found_block = True + break + if found_block: continue + + block = Block(x,y,block_type,BLOCK_TYPES[block_type],True) + + WORLD.append(block) + + for p in getPlayers(): + await p.sendWorld([block.toStatement()]) + except Exception as exc: + WORLD.remove(player) + + for p in getPlayers(): + await p.sendWorld([player.toStatement(False)]) + + await writePacket(websocket, "K", [str(exc)]) + + print(name, "left the server") + +async def tickTimer(): + while True: + for b in WORLD: + await b.tick() + await asyncio.sleep(1/20) + +async def keepAliveTimer(): + while True: + for b in getPlayers(): + await b.keepAlive() + await asyncio.sleep(1) + +async def renderTimer(): + while True: + for p in getPlayers(): + await p.sendWorld([b.toStatement() for b in WORLD if await b.render() and b != p]) + await asyncio.sleep(1/60) + +async def main(): + asyncio.get_event_loop().create_task(tickTimer()) + asyncio.get_event_loop().create_task(keepAliveTimer()) + asyncio.get_event_loop().create_task(renderTimer()) + async with serve(handler, HOST, PORT) as server: + print(f"started server on {HOST}:{PORT}") + await asyncio.get_running_loop().create_future() + + + +HOST,PORT = sys.argv[1].split(":") +PORT = int(PORT) + +COLORS = ["#d22", "#2d2", "#22d", "#dd2", "#2dd", "#d2d", "#ddd"] +SPAWN = (0, 0) + +REACH_DISTANCE = 15 + +BLOCK_TYPES = { + "normal": "#555", + "jump_boost": "#2d2", + "killer": "#d22", +} + +WORLD = [ + Block(-1, -1, "normal", "#555", True), + Block(0, -1, "spawn", "#2ad", True), + Block(1, -1, "normal", "#555", True) +] + + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file