typescript rewrite

This commit is contained in:
MeexReay 2024-12-21 14:19:03 +03:00
parent d1de965ae4
commit 1d1e7a1688
8 changed files with 1285 additions and 0 deletions

View file

@ -38,6 +38,7 @@
``` ```
заход игрока [J]: {ник_игрока} заход игрока [J]: {ник_игрока}
отправить velocity [V]: {vel_x}, {vel_y} отправить velocity [V]: {vel_x}, {vel_y}
отправить позицию [X]: {x}, {y}
установить блок [P]: {x}, {y}, {тип} установить блок [P]: {x}, {y}, {тип}
сломать блок [D]: {x}, {y} сломать блок [D]: {x}, {y}
нажатие кнопки (список кнопок ниже) [K]: {кнопка}, {нажата ли} нажатие кнопки (список кнопок ниже) [K]: {кнопка}, {нажата ли}

82
client/index.html Normal file
View file

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cubic</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="apple-touch-icon" sizes="180x180" href="/icon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/icon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/icon/favicon-16x16.png">
<link rel="manifest" href="/icon/site.webmanifest">
<script>
if (!new URLSearchParams(location.search).has("PLEASE_HTTPS")) {
if (location.protocol !== 'http:') {
location.replace(`http:${location.href.substring(location.protocol.length)}`);
}
}
</script>
<style>
pre {
font-family: 'Fira Code', 'Times New Roman', Times, serif;
font-size: 15px;
border-radius: 10px;
padding: 15px;
background: #333;
}
</style>
</head>
<body oncontextmenu="return false;">
<div class="container">
<h1>Cubic</h1>
<p>игра кубик переделанная на js</p>
<div class="margin-25"></div>
<input type="text" id="server-ip" placeholder="айпи серрве"><br>
<input type="text" id="server-nick" placeholder="ник игроаа"><br>
<span id="server-error" style="color: red;margin: 5px 0;display: inline-block;"></span><br>
<button id="connect-server">поддключт к серваа</button><br>
<div class="margin-25"></div>
<canvas width="640" height="480" id="game"></canvas>
<div class="margin-25"></div>
<h3>список серверов:</h3>
<ul style="list-style: thai;">
<li>meex.lol</li>
<li>других пока нет</li>
</ul>
<div class="margin-25"></div>
<h3>инструкция по игре:</h3>
<pre>
ходить через кнопки A и D
прыгать кнопкой Space
выбрать блок цифрами
поставить блок через ПКМ только перед этим выберите
ломать блоки через ЛКМ
меню отладки на F3
ресетнуться на кнопку R
открыть чат кнопка T
</pre>
<div class="margin-25"></div>
<a href="https://github.com/MeexReay/cubicjs">репозиторий гитхаб</a><br>
<a href="README.md">протокол сервера</a><br>
<a href="server.py">готовая реализация сервера на Python</a>
</div>
<script src="script.js"></script>
</body>
</html>

0
client/src/block.ts Normal file
View file

55
client/src/network.ts Normal file
View file

@ -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())
}
}

0
client/src/player.ts Normal file
View file

0
client/src/render.ts Normal file
View file

757
client/src/script.ts Normal file
View file

@ -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)

390
server/main.py Normal file
View file

@ -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())