2025-03-25 16:47:59 +00:00
|
|
|
import socket
|
|
|
|
import threading
|
2025-03-25 18:08:42 +00:00
|
|
|
import pickle
|
2025-03-25 16:47:59 +00:00
|
|
|
import pygame
|
|
|
|
import sys
|
|
|
|
import time
|
2025-03-26 23:35:34 +00:00
|
|
|
import math
|
2025-03-25 16:47:59 +00:00
|
|
|
|
|
|
|
HOST = '127.0.0.1'
|
|
|
|
PORT = 65432
|
|
|
|
MOVE_SPEED = 200 # pixels per second
|
2025-03-26 23:35:34 +00:00
|
|
|
GUN_LENGTH = 30
|
|
|
|
PLAYER_RADIUS = 20
|
|
|
|
BULLET_RADIUS = 4
|
|
|
|
FLASH_EFFECT_RADIUS = 200
|
|
|
|
FLASH_DURATION = 1.0 # must match server value
|
2025-03-25 18:08:42 +00:00
|
|
|
|
|
|
|
class Event:
|
|
|
|
def __init__(self, name, payload):
|
|
|
|
self.name = name
|
|
|
|
self.payload = payload
|
|
|
|
|
|
|
|
def recvall(sock, n):
|
|
|
|
data = b''
|
|
|
|
while len(data) < n:
|
|
|
|
packet = sock.recv(n - len(data))
|
|
|
|
if not packet:
|
|
|
|
return None
|
|
|
|
data += packet
|
|
|
|
return data
|
|
|
|
|
|
|
|
def send_event(sock, event):
|
|
|
|
data = pickle.dumps(event)
|
|
|
|
length = len(data)
|
|
|
|
sock.sendall(length.to_bytes(4, byteorder='big') + data)
|
|
|
|
|
|
|
|
def recv_event(sock):
|
|
|
|
raw_len = recvall(sock, 4)
|
|
|
|
if not raw_len:
|
|
|
|
return None
|
|
|
|
msg_len = int.from_bytes(raw_len, byteorder='big')
|
|
|
|
data = recvall(sock, msg_len)
|
|
|
|
return pickle.loads(data)
|
2025-03-25 16:47:59 +00:00
|
|
|
|
2025-03-26 23:35:34 +00:00
|
|
|
# Global state.
|
|
|
|
players = {} # client_id -> {"username", "x", "y", "mouse_x", "mouse_y", "health"}
|
|
|
|
bullets = [] # List of bullet dicts from server.
|
|
|
|
grenades = [] # List of grenade dicts.
|
|
|
|
my_client_id = None # Our unique client id from the server.
|
2025-03-25 18:08:42 +00:00
|
|
|
predicted_state = {
|
|
|
|
"x": 100, "y": 100,
|
|
|
|
"keys": set(),
|
|
|
|
"username": "Guest",
|
|
|
|
"mouse_x": 0,
|
|
|
|
"mouse_y": 0
|
|
|
|
}
|
|
|
|
ping = 0 # measured ping in seconds
|
2025-03-26 23:35:34 +00:00
|
|
|
server_details = {} # Updated from server_info and state_update events.
|
|
|
|
map_obstacles = [] # Map obstacles from server info.
|
2025-03-25 16:47:59 +00:00
|
|
|
|
|
|
|
def listen_to_server(sock):
|
2025-03-26 23:35:34 +00:00
|
|
|
global my_client_id, players, predicted_state, ping, server_details, bullets, grenades, map_obstacles
|
2025-03-25 16:47:59 +00:00
|
|
|
while True:
|
|
|
|
try:
|
2025-03-25 18:08:42 +00:00
|
|
|
event = recv_event(sock)
|
|
|
|
if event is None:
|
2025-03-25 16:47:59 +00:00
|
|
|
break
|
2025-03-25 18:08:42 +00:00
|
|
|
if event.name == "self_id":
|
2025-03-25 16:47:59 +00:00
|
|
|
if my_client_id is None:
|
2025-03-25 18:08:42 +00:00
|
|
|
my_client_id = event.payload.get("client_id")
|
2025-03-25 16:47:59 +00:00
|
|
|
print(f"[INFO] My client ID: {my_client_id}")
|
2025-03-25 18:08:42 +00:00
|
|
|
elif event.name == "server_info":
|
|
|
|
server_details = event.payload
|
2025-03-26 23:35:34 +00:00
|
|
|
map_obstacles = server_details.get("map", [])
|
2025-03-25 18:08:42 +00:00
|
|
|
print(f"[INFO] Received server info: {server_details}")
|
|
|
|
elif event.name == "client_connect":
|
|
|
|
print(f"[INFO] Client connected: {event.payload.get('client_id')}")
|
|
|
|
elif event.name == "client_disconnect":
|
|
|
|
cid = event.payload.get("client_id")
|
|
|
|
print(f"[INFO] Client disconnected: {cid}")
|
|
|
|
if cid in players:
|
|
|
|
del players[cid]
|
|
|
|
elif event.name == "state_update":
|
|
|
|
players = event.payload.get("players", {})
|
2025-03-26 23:35:34 +00:00
|
|
|
bullets = event.payload.get("bullets", [])
|
|
|
|
grenades = event.payload.get("grenades", [])
|
2025-03-25 18:08:42 +00:00
|
|
|
# Update dynamic server details.
|
|
|
|
if "total_users" in event.payload:
|
|
|
|
server_details["total_users"] = event.payload["total_users"]
|
|
|
|
if "max_users" in event.payload:
|
|
|
|
server_details["max_users"] = event.payload["max_users"]
|
2025-03-25 16:47:59 +00:00
|
|
|
if my_client_id and my_client_id in players:
|
|
|
|
server_x = players[my_client_id].get("x", 100)
|
|
|
|
server_y = players[my_client_id].get("y", 100)
|
2025-03-25 18:08:42 +00:00
|
|
|
tickrate = server_details.get("tickrate", 64)
|
|
|
|
correction_factor = 0.1 * (64 / tickrate)
|
2025-03-25 16:47:59 +00:00
|
|
|
if not predicted_state["keys"]:
|
|
|
|
predicted_state["x"] = server_x
|
|
|
|
predicted_state["y"] = server_y
|
|
|
|
else:
|
|
|
|
predicted_state["x"] += (server_x - predicted_state["x"]) * correction_factor
|
|
|
|
predicted_state["y"] += (server_y - predicted_state["y"]) * correction_factor
|
2025-03-25 18:08:42 +00:00
|
|
|
elif event.name == "pong":
|
|
|
|
sent_time = event.payload.get("timestamp")
|
2025-03-25 16:47:59 +00:00
|
|
|
if sent_time:
|
|
|
|
round_trip = time.time() - sent_time
|
|
|
|
ping = round_trip
|
|
|
|
print(f"[PING] {ping*1000:.0f} ms")
|
|
|
|
except Exception as e:
|
|
|
|
print(f"[ERROR] {e}")
|
|
|
|
break
|
|
|
|
|
2025-03-25 18:08:42 +00:00
|
|
|
def send_message(sock, event):
|
|
|
|
send_event(sock, event)
|
2025-03-25 16:47:59 +00:00
|
|
|
|
|
|
|
def ping_loop(sock):
|
|
|
|
while True:
|
|
|
|
ts = time.time()
|
2025-03-25 18:08:42 +00:00
|
|
|
send_message(sock, Event("ping", {"timestamp": ts}))
|
2025-03-25 16:47:59 +00:00
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
def main():
|
|
|
|
global my_client_id, predicted_state
|
|
|
|
username = input("Enter your username: ")
|
|
|
|
predicted_state["username"] = username
|
|
|
|
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
sock.connect((HOST, PORT))
|
|
|
|
|
|
|
|
threading.Thread(target=listen_to_server, args=(sock,), daemon=True).start()
|
|
|
|
threading.Thread(target=ping_loop, args=(sock,), daemon=True).start()
|
|
|
|
|
2025-03-25 18:08:42 +00:00
|
|
|
# Register for events.
|
|
|
|
for event_name in ["client_connect", "client_disconnect", "state_update"]:
|
|
|
|
send_message(sock, Event("register_event", {"event": event_name}))
|
|
|
|
print(f"[INFO] Registered for event: {event_name}")
|
2025-03-25 16:47:59 +00:00
|
|
|
|
2025-03-25 18:08:42 +00:00
|
|
|
send_message(sock, Event("set_username", {"username": username}))
|
2025-03-25 16:47:59 +00:00
|
|
|
|
|
|
|
pygame.init()
|
|
|
|
screen = pygame.display.set_mode((800, 600))
|
2025-03-26 23:35:34 +00:00
|
|
|
pygame.display.set_caption("Top Down Shooter Multiplayer")
|
2025-03-25 16:47:59 +00:00
|
|
|
clock = pygame.time.Clock()
|
|
|
|
|
|
|
|
local_keys = set()
|
|
|
|
|
|
|
|
while True:
|
2025-03-25 18:08:42 +00:00
|
|
|
dt = clock.tick(60) / 1000.0
|
2025-03-26 23:35:34 +00:00
|
|
|
|
2025-03-25 18:08:42 +00:00
|
|
|
for ev in pygame.event.get():
|
|
|
|
if ev.type == pygame.QUIT:
|
2025-03-25 16:47:59 +00:00
|
|
|
pygame.quit()
|
|
|
|
sys.exit()
|
2025-03-25 18:08:42 +00:00
|
|
|
elif ev.type == pygame.KEYDOWN:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = None
|
2025-03-25 18:08:42 +00:00
|
|
|
if ev.key == pygame.K_LEFT:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = "left"
|
2025-03-25 18:08:42 +00:00
|
|
|
elif ev.key == pygame.K_RIGHT:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = "right"
|
2025-03-25 18:08:42 +00:00
|
|
|
elif ev.key == pygame.K_UP:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = "up"
|
2025-03-25 18:08:42 +00:00
|
|
|
elif ev.key == pygame.K_DOWN:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = "down"
|
2025-03-26 23:35:34 +00:00
|
|
|
# Grenade throw keys.
|
|
|
|
elif ev.key == pygame.K_q:
|
|
|
|
send_message(sock, Event("throw_smoke", {}))
|
|
|
|
print("[ACTION] Smoke grenade thrown")
|
|
|
|
elif ev.key == pygame.K_e:
|
|
|
|
send_message(sock, Event("throw_flash", {}))
|
|
|
|
print("[ACTION] Flash grenade thrown")
|
2025-03-25 16:47:59 +00:00
|
|
|
if key_name and key_name not in local_keys:
|
|
|
|
local_keys.add(key_name)
|
|
|
|
predicted_state["keys"].add(key_name)
|
2025-03-25 18:08:42 +00:00
|
|
|
send_message(sock, Event("keydown", {"key": key_name}))
|
|
|
|
elif ev.type == pygame.KEYUP:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = None
|
2025-03-25 18:08:42 +00:00
|
|
|
if ev.key == pygame.K_LEFT:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = "left"
|
2025-03-25 18:08:42 +00:00
|
|
|
elif ev.key == pygame.K_RIGHT:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = "right"
|
2025-03-25 18:08:42 +00:00
|
|
|
elif ev.key == pygame.K_UP:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = "up"
|
2025-03-25 18:08:42 +00:00
|
|
|
elif ev.key == pygame.K_DOWN:
|
2025-03-25 16:47:59 +00:00
|
|
|
key_name = "down"
|
|
|
|
if key_name and key_name in local_keys:
|
|
|
|
local_keys.remove(key_name)
|
|
|
|
if key_name in predicted_state["keys"]:
|
|
|
|
predicted_state["keys"].remove(key_name)
|
2025-03-25 18:08:42 +00:00
|
|
|
send_message(sock, Event("keyup", {"key": key_name}))
|
|
|
|
elif ev.type == pygame.MOUSEMOTION:
|
|
|
|
mouse_x, mouse_y = ev.pos
|
|
|
|
predicted_state["mouse_x"] = mouse_x
|
|
|
|
predicted_state["mouse_y"] = mouse_y
|
|
|
|
send_message(sock, Event("mouse_move", {"x": mouse_x, "y": mouse_y}))
|
2025-03-26 23:35:34 +00:00
|
|
|
elif ev.type == pygame.MOUSEBUTTONDOWN:
|
|
|
|
if ev.button == 1: # Left click: shoot
|
|
|
|
mouse_x, mouse_y = ev.pos
|
|
|
|
dx = mouse_x - predicted_state["x"]
|
|
|
|
dy = mouse_y - predicted_state["y"]
|
|
|
|
angle = math.atan2(dy, dx)
|
|
|
|
send_message(sock, Event("shoot", {"angle": angle}))
|
|
|
|
print("[ACTION] Shoot event sent")
|
|
|
|
|
2025-03-25 18:08:42 +00:00
|
|
|
# Update predicted state locally.
|
2025-03-26 23:35:34 +00:00
|
|
|
dx = 0
|
|
|
|
dy = 0
|
2025-03-25 16:47:59 +00:00
|
|
|
if "left" in predicted_state["keys"]:
|
|
|
|
dx -= MOVE_SPEED * dt
|
|
|
|
if "right" in predicted_state["keys"]:
|
|
|
|
dx += MOVE_SPEED * dt
|
|
|
|
if "up" in predicted_state["keys"]:
|
|
|
|
dy -= MOVE_SPEED * dt
|
|
|
|
if "down" in predicted_state["keys"]:
|
|
|
|
dy += MOVE_SPEED * dt
|
|
|
|
predicted_state["x"] += dx
|
|
|
|
predicted_state["y"] += dy
|
|
|
|
|
2025-03-26 23:35:34 +00:00
|
|
|
# Camera follows local player.
|
|
|
|
local_x = predicted_state["x"]
|
|
|
|
local_y = predicted_state["y"]
|
|
|
|
screen_width, screen_height = 800, 600
|
|
|
|
camera_offset = (local_x - screen_width // 2, local_y - screen_height // 2)
|
|
|
|
|
|
|
|
# Drawing.
|
|
|
|
screen.fill((30, 30, 30)) # Background.
|
|
|
|
|
|
|
|
# Draw map obstacles.
|
|
|
|
for obs in map_obstacles:
|
|
|
|
rect = pygame.Rect(obs["x"] - camera_offset[0], obs["y"] - camera_offset[1], obs["width"], obs["height"])
|
|
|
|
pygame.draw.rect(screen, (100, 100, 100), rect)
|
|
|
|
|
|
|
|
# Draw bullets.
|
|
|
|
for bullet in bullets:
|
|
|
|
bx = int(bullet.get("x", 0) - camera_offset[0])
|
|
|
|
by = int(bullet.get("y", 0) - camera_offset[1])
|
|
|
|
pygame.draw.circle(screen, (255, 255, 255), (bx, by), BULLET_RADIUS)
|
|
|
|
|
|
|
|
# Draw grenades.
|
|
|
|
for gren in grenades:
|
|
|
|
gx = int(gren.get("x", 0) - camera_offset[0])
|
|
|
|
gy = int(gren.get("y", 0) - camera_offset[1])
|
|
|
|
gtype = gren.get("type", "")
|
|
|
|
if gren.get("fuse", 0) > 0:
|
|
|
|
pygame.draw.circle(screen, (200, 200, 200), (gx, gy), 6)
|
|
|
|
else:
|
|
|
|
if gtype == "smoke":
|
|
|
|
# Draw semi-transparent smoke flood fill.
|
|
|
|
smoke_radius = 80
|
|
|
|
smoke_surface = pygame.Surface((smoke_radius*2, smoke_radius*2), pygame.SRCALPHA)
|
|
|
|
pygame.draw.circle(smoke_surface, (100, 100, 100, 150), (smoke_radius, smoke_radius), smoke_radius)
|
|
|
|
screen.blit(smoke_surface, (gx - smoke_radius, gy - smoke_radius))
|
|
|
|
elif gtype == "flash":
|
|
|
|
pygame.draw.circle(screen, (255, 255, 0), (gx, gy), 30)
|
2025-03-25 16:47:59 +00:00
|
|
|
|
2025-03-26 23:35:34 +00:00
|
|
|
# Draw players.
|
2025-03-25 16:47:59 +00:00
|
|
|
for cid, data in players.items():
|
2025-03-26 23:35:34 +00:00
|
|
|
# Get player position and health.
|
|
|
|
px = data.get("x", 100)
|
|
|
|
py = data.get("y", 100)
|
|
|
|
health = data.get("health", 100)
|
2025-03-25 16:47:59 +00:00
|
|
|
uname = data.get("username", "Guest")
|
2025-03-26 23:35:34 +00:00
|
|
|
# Determine aiming direction.
|
|
|
|
if cid == my_client_id:
|
|
|
|
aim_dx = predicted_state["mouse_x"] - local_x
|
|
|
|
aim_dy = predicted_state["mouse_y"] - local_y
|
|
|
|
else:
|
|
|
|
aim_dx = data.get("mouse_x", 0) - px
|
|
|
|
aim_dy = data.get("mouse_y", 0) - py
|
|
|
|
aim_angle = math.atan2(aim_dy, aim_dx)
|
|
|
|
# Adjust drawing positions by camera offset.
|
|
|
|
draw_x = int(px - camera_offset[0])
|
|
|
|
draw_y = int(py - camera_offset[1])
|
|
|
|
# Draw player body.
|
|
|
|
if cid == my_client_id:
|
|
|
|
color = (0, 0, 255)
|
|
|
|
else:
|
|
|
|
color = (255, 0, 0)
|
|
|
|
pygame.draw.circle(screen, color, (draw_x, draw_y), PLAYER_RADIUS)
|
|
|
|
# Draw face (an eye) indicating aim.
|
|
|
|
eye_offset = 10
|
|
|
|
eye_x = draw_x + int(eye_offset * math.cos(aim_angle))
|
|
|
|
eye_y = draw_y + int(eye_offset * math.sin(aim_angle))
|
|
|
|
pygame.draw.circle(screen, (255, 255, 255), (eye_x, eye_y), 4)
|
|
|
|
# Draw health bar above player.
|
|
|
|
bar_width = 40
|
|
|
|
bar_height = 5
|
|
|
|
health_ratio = health / 100
|
|
|
|
health_bar_back = pygame.Rect(draw_x - bar_width//2, draw_y - PLAYER_RADIUS - 15, bar_width, bar_height)
|
|
|
|
health_bar_front = pygame.Rect(draw_x - bar_width//2, draw_y - PLAYER_RADIUS - 15, int(bar_width * health_ratio), bar_height)
|
|
|
|
pygame.draw.rect(screen, (100, 100, 100), health_bar_back)
|
|
|
|
pygame.draw.rect(screen, (0, 255, 0), health_bar_front)
|
|
|
|
# Draw username.
|
|
|
|
font = pygame.font.SysFont(None, 20)
|
|
|
|
name_surface = font.render(uname, True, (255, 255, 255))
|
|
|
|
screen.blit(name_surface, (draw_x - name_surface.get_width()//2, draw_y - PLAYER_RADIUS - 30))
|
|
|
|
|
|
|
|
# Flash effect: if a flash grenade is active and the local player is nearby, overlay white.
|
|
|
|
flash_alpha = 0
|
|
|
|
for gren in grenades:
|
|
|
|
if gren.get("type") == "flash" and gren.get("fuse", 0) <= 0:
|
|
|
|
gx = gren.get("x", 0)
|
|
|
|
gy = gren.get("y", 0)
|
|
|
|
dist = math.hypot(local_x - gx, local_y - gy)
|
|
|
|
if dist < FLASH_EFFECT_RADIUS:
|
|
|
|
intensity = gren.get("duration", 0) / FLASH_DURATION
|
|
|
|
flash_alpha = max(flash_alpha, int(255 * intensity))
|
|
|
|
if flash_alpha > 0:
|
|
|
|
flash_overlay = pygame.Surface((screen_width, screen_height))
|
|
|
|
flash_overlay.fill((255, 255, 255))
|
|
|
|
flash_overlay.set_alpha(flash_alpha)
|
|
|
|
screen.blit(flash_overlay, (0, 0))
|
|
|
|
|
2025-03-25 18:08:42 +00:00
|
|
|
# Display ping.
|
2025-03-25 16:47:59 +00:00
|
|
|
ping_font = pygame.font.SysFont(None, 24)
|
|
|
|
ping_surface = ping_font.render(f"Ping: {int(ping*1000)} ms", True, (255, 255, 0))
|
|
|
|
screen.blit(ping_surface, (10, 10))
|
2025-03-26 23:35:34 +00:00
|
|
|
# Display HUD info.
|
2025-03-25 18:08:42 +00:00
|
|
|
hud_font = pygame.font.SysFont(None, 24)
|
|
|
|
total_users = server_details.get("total_users", len(players))
|
|
|
|
max_users = server_details.get("max_users", "?")
|
|
|
|
tickrate = server_details.get("tickrate", 64)
|
|
|
|
hud_text = hud_font.render(
|
|
|
|
f"Users: {total_users}/{max_users} | Client ID: {my_client_id or 'N/A'} | Tickrate: {tickrate} tps",
|
|
|
|
True,
|
|
|
|
(255, 255, 255)
|
|
|
|
)
|
|
|
|
screen.blit(hud_text, (10, 40))
|
2025-03-25 16:47:59 +00:00
|
|
|
pygame.display.flip()
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|