add bullets collsion to ground and tiles

# game.py
import time
import pygame
import socket
import pickle
import threading
import struct
import sys
import json

# Initialize Pygame
pygame.init()

# Set up the screen
SCREEN_WIDTH, SCREEN_HEIGHT = 1280, 720
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('2D Fight Game')

# Constants
TILE_SIZE = 72  # Tile size after scaling
GRAVITY = 1
JUMP_STRENGTH = 15
SPEED = 11
FRICTION = 0.0001
FONT = pygame.font.Font(None, 48)
SMALL_FONT = pygame.font.Font(None, 36)
WHITE = (255, 255, 255)
BLACK = (46, 46, 46)
BLUE = (135, 206, 235)
RED = (255, 0, 0)
GREEN = (0, 255, 0)

# Bullet settings
MAX_BULLETS = 5          # Maximum number of bullets allowed on screen
BULLET_COOLDOWN = 300    # Cooldown time in milliseconds between shots

# Input boxes
player_name_input_box = pygame.Rect(400, 200, 400, 50)
server_ip_input_box = pygame.Rect(400, 300, 400, 50)

# Text variables
player_name = 'Player'
server_ip = 'localhost'
input_active = None

# Connecting flag
connecting = False

# Function to display text
def draw_text(text, font, color, surface, x, y):
    textobj = font.render(text, True, color)
    textrect = textobj.get_rect(center=(x, y))
    surface.blit(textobj, textrect)

# Connecting screen
def show_connecting_screen():
    screen.fill(BLUE)
    draw_text("Connecting...", FONT, BLACK, screen, SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
    pygame.display.flip()

# Function to handle user input for name and server IP in Pygame
def get_user_input_pygame():
    global player_name, server_ip, input_active, connecting
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                if player_name_input_box.collidepoint(event.pos):
                    input_active = 'name'
                elif server_ip_input_box.collidepoint(event.pos):
                    input_active = 'server_ip'
                else:
                    input_active = None
            if event.type == pygame.KEYDOWN:
                if input_active == 'name':
                    if event.key == pygame.K_RETURN:
                        input_active = 'server_ip'
                    elif event.key == pygame.K_BACKSPACE:
                        player_name = player_name[:-1]
                    else:
                        player_name += event.unicode
                elif input_active == 'server_ip':
                    if event.key == pygame.K_RETURN:
                        connecting = True
                        show_connecting_screen()
                        return player_name, server_ip
                    elif event.key == pygame.K_BACKSPACE:
                        server_ip = server_ip[:-1]
                    else:
                        server_ip += event.unicode

        screen.fill(BLUE)
        draw_text("Enter Player Name:", FONT, BLACK, screen, SCREEN_WIDTH // 2, 160)
        draw_text("Enter Server IP (e.g., localhost:25566):", FONT, BLACK, screen, SCREEN_WIDTH // 2, 260)

        pygame.draw.rect(screen, WHITE, player_name_input_box, 2)
        pygame.draw.rect(screen, WHITE, server_ip_input_box, 2)

        draw_text(player_name, SMALL_FONT, BLACK, screen, player_name_input_box.centerx, player_name_input_box.centery)
        draw_text(server_ip, SMALL_FONT, BLACK, screen, server_ip_input_box.centerx, server_ip_input_box.centery)

        pygame.display.flip()

# Function to send data
def send_data(data):
    serialized_data = pickle.dumps(data)
    length = len(serialized_data)
    try:
        client.sendall(struct.pack('>I', length) + serialized_data)
    except socket.error as e:
        print(f"Failed to send data to server: {e}")

# Function to receive messages
def receive_message(conn):
    length_data = conn.recv(4)
    if len(length_data) < 4:
        raise Exception("Received incomplete length data")

    data_length = struct.unpack('>I', length_data)[0]
    data = b''
    while len(data) < data_length:
        packet = conn.recv(data_length - len(data))
        if not packet:
            raise Exception("Received incomplete data")
        data += packet
    return pickle.loads(data)

# Set up the character
character = pygame.Rect(375, 375, 50, 50)  # Initial position
character_velocity_x = 0
character_velocity_y = 0
is_jumping = False

# Initialize player data and map layout
players = {}
map_layout = []

# Load tile images
tile_images = {
    0: pygame.Surface((TILE_SIZE, TILE_SIZE)),  # Empty space
    1: pygame.image.load('./assets/grass.png').convert_alpha(),  # Grass tile (solid)
    2: pygame.image.load('./assets/grass.png').convert_alpha(),  # Grass tile (jump-through)
}

# Scale the grass tile image
tile_images[1] = pygame.transform.scale(tile_images[1], (TILE_SIZE, TILE_SIZE))
tile_images[2] = pygame.transform.scale(tile_images[2], (TILE_SIZE, TILE_SIZE))

# Set color for transparent tile (matching the screen background)
tile_images[0].fill(BLUE)  # Light blue to blend with background

# Function to check if the character is on the ground (on a grass tile)
def is_on_ground(rect):
    rect.bottom += 1  # Temporarily move the rect down by 1 pixel
    for y in range(max(0, rect.bottom // TILE_SIZE), min(len(map_layout), (rect.bottom // TILE_SIZE) + 1)):
        for x in range(max(0, rect.left // TILE_SIZE), min(len(map_layout[y]), (rect.right // TILE_SIZE) + 1)):
            if map_layout[y][x] == 1 or map_layout[y][x] == 2:
                tile_rect = pygame.Rect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE)
                if rect.colliderect(tile_rect):
                    rect.bottom -= 1  # Move the rect back up
                    return True
    rect.bottom -= 1  # Move the rect back up
    return False

# Function to check and handle general collision with tiles
def handle_tile_collisions(rect):
    global character_velocity_y, character_velocity_x, is_jumping

    # Check vertical collisions first (falling or jumping)
    for y in range(max(0, rect.top // TILE_SIZE), min(len(map_layout), (rect.bottom // TILE_SIZE) + 1)):
        for x in range(max(0, rect.left // TILE_SIZE), min(len(map_layout[y]), (rect.right // TILE_SIZE) + 1)):
            tile_id = map_layout[y][x]
            tile_rect = pygame.Rect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE)

            if rect.colliderect(tile_rect):
                if tile_id == 1:  # Solid tile
                    # Handle vertical collisions
                    if character_velocity_y > 0:  # Falling down
                        if rect.bottom > tile_rect.top and rect.bottom <= tile_rect.bottom:
                            rect.bottom = tile_rect.top
                            character_velocity_y = 0
                            is_jumping = False
                    elif character_velocity_y < 0:  # Jumping up
                        if rect.top < tile_rect.bottom and rect.top >= tile_rect.top:
                            rect.top = tile_rect.bottom
                            character_velocity_y = 0

                elif tile_id == 2:  # Jump-through tile
                    # Handle collision if falling onto the tile
                    if character_velocity_y > 0 and rect.bottom > tile_rect.top and rect.bottom <= tile_rect.bottom:
                        rect.bottom = tile_rect.top
                        character_velocity_y = 0
                        is_jumping = False

    # Check horizontal collisions after vertical collisions are resolved
    for y in range(max(0, rect.top // TILE_SIZE), min(len(map_layout), (rect.bottom // TILE_SIZE) + 1)):
        for x in range(max(0, rect.left // TILE_SIZE), min(len(map_layout[y]), (rect.right // TILE_SIZE) + 1)):
            tile_id = map_layout[y][x]
            tile_rect = pygame.Rect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE)

            if rect.colliderect(tile_rect):
                if tile_id == 1:  # Solid tile
                    # Handle horizontal collisions
                    if character_velocity_x > 0:  # Moving right
                        if rect.right > tile_rect.left and rect.right <= tile_rect.right:
                            rect.right = tile_rect.left
                            character_velocity_x = 0
                    elif character_velocity_x < 0:  # Moving left
                        if rect.left < tile_rect.right and rect.left >= tile_rect.left:
                            rect.left = tile_rect.right

# Networking setup
player_name, server_ip = get_user_input_pygame()

try:
    if ':' in server_ip:
        HOST, PORT = server_ip.split(':')
        PORT = int(PORT)
    else:
        HOST = server_ip
        PORT = 25566
except ValueError:
    HOST = 'localhost'
    PORT = 25566
except TypeError:
    HOST = 'localhost'
    PORT = 25566
except Exception as e:
    print(f"An error occurred: {e}")
    HOST = 'localhost'
    PORT = 25566

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    client.connect((HOST, PORT))
    # Send initial player data to the server
    send_data({'name': player_name, 'position': (character.x, character.y)})
except socket.error as e:
    print(f"Unable to connect to the server: {e}")
    sys.exit()

def receive_data():
    global players, map_layout, running
    while running:
        try:
            data = receive_message(client)
            players = data.get('players', {})
            map_layout = data.get('map_layout', [])
        except Exception as e:
            print(f"Disconnected from server: {e}")
            running = False
            break

# Start a thread to receive data from the server
running = True
threading.Thread(target=receive_data, daemon=True).start()

# Bullet class to manage bullets
class Bullet:
    def __init__(self, owner, position, direction):
        self.owner = owner  # Tuple (IP, PORT)
        self.x, self.y = position
        self.direction = direction  # 1 for right, -1 for left
        self.speed = 15
        self.rect = pygame.Rect(self.x, self.y, 10, 5)

    def move(self):
        self.x += self.speed * self.direction
        self.rect.x = self.x

    def draw(self, surface):
        pygame.draw.rect(surface, RED, self.rect)

# List to store local bullets
bullets = []

# Variables for bullet cooldown
last_shot_time = 0  # Time when the last shot was fired

# Respawn variables
is_dead = False
respawn_time = 0  # Timestamp when respawn should occur

# Main game loop
clock = pygame.time.Clock()
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Get keys pressed
    keys = pygame.key.get_pressed()

    # Handle horizontal movement
    if not is_dead:
        if keys[pygame.K_LEFT]:
            character_velocity_x = -SPEED
        elif keys[pygame.K_RIGHT]:
            character_velocity_x = SPEED
        else:
            character_velocity_x *= FRICTION  # Apply friction when no key is pressed

        # Handle jumping
        if (keys[pygame.K_SPACE] or keys[pygame.K_UP] or keys[pygame.K_w]) and not is_jumping and is_on_ground(character):
            character_velocity_y = -JUMP_STRENGTH
            is_jumping = True

        # Handle shooting with bullet limit and cooldown
        current_time = pygame.time.get_ticks()
        if keys[pygame.K_f]:
            if (current_time - last_shot_time >= BULLET_COOLDOWN) and (len(bullets) < MAX_BULLETS):
                # Determine shooting direction based on movement
                if character_velocity_x >= 0:
                    direction = 1  # Right
                else:
                    direction = -1  # Left
                # Create a bullet at the front of the character
                bullet_x = character.x + character.width if direction == 1 else character.x - 10
                bullet_y = character.y + character.height // 2
                new_bullet = Bullet((client.getsockname()[0], client.getsockname()[1]), (bullet_x, bullet_y), direction)
                bullets.append(new_bullet)
                # Update the last shot time
                last_shot_time = current_time

        # Apply gravity
        character_velocity_y += GRAVITY
        character.x += character_velocity_x
        character.y += character_velocity_y

        # Check and handle collisions with tiles
        handle_tile_collisions(character)

        # Ensure character stays within screen bounds
        if character.left < 0:
            character.left = 0
        if character.right > SCREEN_WIDTH:
            character.right = SCREEN_WIDTH
        if character.top < 0:
            character.top = 0
        if character.bottom > SCREEN_HEIGHT:
            character.bottom = SCREEN_HEIGHT

        # Update bullets
        for bullet in bullets[:]:
            bullet.move()
            # Remove bullets that are off-screen
            if bullet.x < 0 or bullet.x > SCREEN_WIDTH:
                bullets.remove(bullet)

        # Send character position and bullets to server
        try:
            # Serialize bullet data
            bullet_data = [{'x': b.x, 'y': b.y, 'direction': b.direction} for b in bullets]
            send_data({'name': player_name, 'position': (character.x, character.y), 'bullets': bullet_data})
        except socket.error as e:
            print(f"Failed to send data to server: {e}")
            running = False

    # Handle respawn
    if is_dead and pygame.time.get_ticks() >= respawn_time:
        is_dead = False
        character.x, character.y = 375, 375
        character_velocity_x, character_velocity_y = 0, 0
        is_jumping = False
        bullets.clear()
        # Inform server about respawn
        send_data({'name': player_name, 'position': (character.x, character.y), 'respawn': True})

    # Draw everything
    screen.fill(BLUE)

    # Draw the map layout
    for y, row in enumerate(map_layout):
        for x, tile_id in enumerate(row):
            tile_image = tile_images.get(tile_id)
            if tile_image:
                screen.blit(tile_image, (x * TILE_SIZE, y * TILE_SIZE))

    # Draw all players
    for addr, info in players.items():
        pos = info['position']
        color = info['color']
        health = info.get('health', 100)
        player_rect = pygame.Rect(pos[0], pos[1], 50, 50)
        pygame.draw.rect(screen, color, player_rect)
        name = info.get('name', 'Unknown')
        text_surface = SMALL_FONT.render(name, True, BLACK)
        screen.blit(text_surface, (pos[0], pos[1] - 20))  # Display name above player

        # Draw health bar
        health_bar_width = 50
        health_bar_height = 5
        health_ratio = max(health / 100, 0)
        pygame.draw.rect(screen, RED, (pos[0], pos[1] - 10, health_bar_width, health_bar_height))
        pygame.draw.rect(screen, GREEN, (pos[0], pos[1] - 10, health_bar_width * health_ratio, health_bar_height))

    # Draw all bullets from other players
    for addr, info in players.items():
        for b in info.get('bullets', []):
            bullet_rect = pygame.Rect(b['x'], b['y'], 10, 5)
            pygame.draw.rect(screen, RED, bullet_rect)

    # Draw local bullets
    for bullet in bullets:
        bullet.draw(screen)

    # Update the display
    pygame.display.flip()

    # Cap the frame rate
    clock.tick(60)

    # Handle death and respawn if necessary
    for addr, info in players.items():
        if addr == client.getsockname() and info.get('health', 100) <= 0:
            if not is_dead:
                is_dead = True
                respawn_time = pygame.time.get_ticks() + 3000  # 3 seconds cooldown
                print("You have been eliminated! Respawning in 3 seconds...")
                # Reset local character position to server's value for consistency
                character.x, character.y = info.get('position', (375, 375))
                character_velocity_x, character_velocity_y = 0, 0
                is_jumping = False

# Close the client socket and quit Pygame
client.close()
pygame.quit()