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
+
+
+
+
+
+
+
поддключт к серваа
+
+
+
+
+
+
+
+
список серверов:
+
+
+ meex.lol
+ других пока нет
+
+
+
+
+
инструкция по игре:
+
+
+ходить через кнопки 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