Fix issue when my character not on the floor and feels like he is in 2d space, not 3d. Here is the code for enemy.py: # enemy.py

import pygame
import math
import sys
from PIL import Image, ImageSequence
import os

class Enemy:
    def __init__(self, x, y, world_map, tile_size=64, speed=2, animation_speed=0.1, gif_path='pinky.gif'):
        """
        Initializes the Enemy object.

        :param x: Initial x-coordinate in pixels.
        :param y: Initial y-coordinate in pixels.
        :param world_map: 2D list representing the game world.
        :param tile_size: Size of each tile in the world map.
        :param speed: Movement speed of the enemy.
        :param animation_speed: Speed at which the animation frames update.
        :param gif_path: Path to the enemy's animated GIF.
        """
        self.x = x
        self.y = y
        self.world_map = world_map
        self.tile_size = tile_size
        self.speed = speed
        self.animation_speed = animation_speed

        # Load animation frames
        self.frames = self.load_animation(gif_path)
        self.current_frame = 0
        self.animation_timer = 0

        # Set up the rectangle for rendering
        self.image = self.frames[self.current_frame]
        self.rect = self.image.get_rect()
        self.rect.center = (self.x, self.y)

    def load_animation(self, path):
        """
        Loads animation frames from a GIF file.

        :param path: Path to the GIF file.
        :return: List of Pygame surfaces representing each frame.
        """
        frames = []

        # Check if file exists
        if not os.path.exists(path):
            print(f"Error: Animation file '{path}' does not exist.")
            sys.exit()

        try:
            img = Image.open(path)
            print(f"Opened GIF '{path}' successfully.")
        except IOError as e:
            print(f"Error: Unable to load animation file '{path}'.\n{e}")
            sys.exit()

        try:
            for frame in ImageSequence.Iterator(img):
                # Ensure the frame has data
                if frame.size[0] == 0 or frame.size[1] == 0:
                    print("Warning: Encountered a frame with zero width or height. Skipping.")
                    continue

                frame = frame.convert('RGBA')
                mode = frame.mode
                size = frame.size
                data = frame.tobytes()

                try:
                    py_image = pygame.image.fromstring(data, size, mode)
                    frames.append(py_image)
                except Exception as e:
                    print(f"Error converting frame to Pygame surface: {e}. Skipping frame.")
                    continue

            print(f"Loaded {len(frames)} frames from '{path}'.")
        except Exception as e:
            print(f"Error while processing frames: {e}")
            sys.exit()

        if not frames:
            print(f"Error: No valid frames loaded from '{path}'.")
            sys.exit()

        return frames

    def update(self, player_x, player_y, delta_time):
        """
        Updates the enemy's position and animation.

        :param player_x: Player's current x-coordinate.
        :param player_y: Player's current y-coordinate.
        :param delta_time: Time elapsed since the last frame.
        """
        # Update animation
        self.animation_timer += delta_time
        if self.animation_timer >= self.animation_speed:
            self.animation_timer = 0
            self.current_frame = (self.current_frame + 1) % len(self.frames)
            self.image = self.frames[self.current_frame]
            self.rect = self.image.get_rect(center=(self.x, self.y))

        # Calculate direction towards the player
        dx = player_x - self.x
        dy = player_y - self.y
        distance = math.hypot(dx, dy)

        if distance > 5:  # Threshold to stop too close
            # Normalize direction
            dx /= distance
            dy /= distance

            # Calculate potential new position
            new_x = self.x + dx * self.speed
            new_y = self.y + dy * self.speed

            # Collision detection with the world map
            if self.can_move_to(new_x, self.y):
                self.x = new_x
            if self.can_move_to(self.x, new_y):
                self.y = new_y

            # Update rectangle position
            self.rect.center = (self.x, self.y)

    def can_move_to(self, x, y):
        """
        Checks if the enemy can move to the specified position without colliding with walls.

        :param x: Target x-coordinate in pixels.
        :param y: Target y-coordinate in pixels.
        :return: True if movement is possible, False otherwise.
        """
        map_x = int(x // self.tile_size)
        map_y = int(y // self.tile_size)

        # Check map boundaries
        if map_y >= len(self.world_map) or map_x >= len(self.world_map[0]) or map_y < 0 or map_x < 0:
            return False

        # Check for wall
        return self.world_map[map_y][map_x] != '#'

    def draw(self, screen, bobbing_offset=(0, 0), impulse_offset=(0, 0)):
        """
        Draws the enemy on the screen with applied offsets.

        :param screen: Pygame surface to draw the enemy on.
        :param bobbing_offset: Tuple representing the current bobbing offsets.
        :param impulse_offset: Tuple representing the current impulse offsets.
        """
        draw_x = self.x + bobbing_offset[0] + impulse_offset[0]
        draw_y = self.y + bobbing_offset[1] + impulse_offset[1]
        self.rect.center = (draw_x, draw_y)
        screen.blit(self.image, self.rect)

And this game.py:
import pygame
import math
import sys
from PIL import Image
from bobbing import Bobbing
from Impulse import Impulse
from enemy import Enemy
# Initialize Pygame
pygame.init()

# Set up display
WIDTH, HEIGHT = 1920, 1080
HALF_WIDTH = WIDTH // 2
HALF_HEIGHT = HEIGHT // 2
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("3D Raycasting Demo")

# Clock to control frame rate
clock = pygame.time.Clock()

# Initialize Bobbing
bobbing = Bobbing(amplitude=10, frequency=5, damping=0.9)
# Initialize Impulse
impulse = Impulse(amplitude=3, frequency=15, damping=0.85, max_duration=0.5)

# Simple map (2D grid)
world_map = [
    '##########',
    '#........#',
    '#........#',
    '#........#',
    '#........#',
    '#........#',
    '#........#',
    '#........#',
    '##########',
]

# Map dimensions and tile size
MAP_WIDTH = len(world_map[0]) * 64
MAP_HEIGHT = len(world_map) * 64    
TILE_SIZE = 64

# Example starting position for the enemy
enemy_start_x = TILE_SIZE * 5
enemy_start_y = TILE_SIZE * 5
enemy = Enemy(enemy_start_x, enemy_start_y, world_map, tile_size=TILE_SIZE)

# Player settings
player_x = TILE_SIZE * 1.5
player_y = TILE_SIZE * 1.5
player_angle = 0
player_speed = 5

# Load wall texture
wall_texture = pygame.image.load('wall.png').convert()
wall_texture = pygame.transform.scale(wall_texture, (TILE_SIZE, TILE_SIZE))

# Function to load animation frames from GIF
def load_animation(path):
    frames = []
    img = Image.open(path)
    try:
        while True:
            frame = img.copy()
            frame = frame.convert('RGBA')
            mode = frame.mode
            size = frame.size
            data = frame.tobytes()
            py_image = pygame.image.fromstring(data, size, mode)
            frames.append(py_image)
            img.seek(img.tell() + 1)
    except EOFError:
        pass
    return frames

# Load shooting animation frames
fire_frames = load_animation('fire.gif')
fire_frame_index = 0
fire_animation_speed = 0.05  # Adjust animation speed as needed
fire_animation_timer = 0
fire_rect = fire_frames[0].get_rect()
fire_rect.center = (HALF_WIDTH, HEIGHT - fire_rect.height // 2)

shooting = False

# Colors
BLACK = (0, 0, 0)

# Raycasting parameters
FOV = math.pi / 3
HALF_FOV = FOV / 2
NUM_RAYS = WIDTH // 2
MAX_DEPTH = 800
DELTA_ANGLE = FOV / NUM_RAYS
DIST = NUM_RAYS / (2 * math.tan(HALF_FOV))
PROJ_COEFF = 3 * DIST * TILE_SIZE
SCALE = WIDTH // NUM_RAYS

def mapping(x, y):
    return int(x) // TILE_SIZE * TILE_SIZE, int(y) // TILE_SIZE * TILE_SIZE

def ray_casting():
    global player_x, player_y, player_angle
    cur_angle = player_angle - HALF_FOV
    for ray in range(NUM_RAYS):
        sin_a = math.sin(cur_angle)
        cos_a = math.cos(cur_angle)

        # Vertical lines
        x, dx = (int(player_x // TILE_SIZE) * TILE_SIZE + TILE_SIZE, 1) if cos_a > 0 else (int(player_x // TILE_SIZE) * TILE_SIZE, -1)
        for i in range(0, MAX_DEPTH, TILE_SIZE):
            depth_v = (x - player_x) / cos_a
            yv = player_y + depth_v * sin_a
            tile_v = mapping(x + dx, yv)
            if 0 <= tile_v[0] < MAP_WIDTH and 0 <= tile_v[1] < MAP_HEIGHT:
                if world_map[int(tile_v[1] / TILE_SIZE)][int(tile_v[0] / TILE_SIZE)] == '#':
                    break
            x += dx * TILE_SIZE

        # Horizontal lines
        y, dy = (int(player_y // TILE_SIZE) * TILE_SIZE + TILE_SIZE, 1) if sin_a > 0 else (int(player_y // TILE_SIZE) * TILE_SIZE, -1)
        for i in range(0, MAX_DEPTH, TILE_SIZE):
            depth_h = (y - player_y) / sin_a
            xh = player_x + depth_h * cos_a
            tile_h = mapping(xh, y + dy)
            if 0 <= tile_h[0] < MAP_WIDTH and 0 <= tile_h[1] < MAP_HEIGHT:
                if world_map[int(tile_h[1] / TILE_SIZE)][int(tile_h[0] / TILE_SIZE)] == '#':
                    break
            y += dy * TILE_SIZE

        # Depth selection
        depth = depth_v if depth_v < depth_h else depth_h
        offset = yv % TILE_SIZE if depth_v < depth_h else xh % TILE_SIZE

        # Remove fishbowl effect
        depth *= math.cos(player_angle - cur_angle)

        # Projection
        proj_height = min(int(PROJ_COEFF / (depth + 0.0001)), 2 * HEIGHT)

        # Wall slice
        wall_column = wall_texture.subsurface(int(offset), 0, 1, TILE_SIZE)
        wall_column = pygame.transform.scale(wall_column, (SCALE, proj_height))
        screen.blit(wall_column, (ray * SCALE, HALF_HEIGHT - proj_height // 2))

        cur_angle += DELTA_ANGLE

# Main game loop
while True:
    delta_time = clock.get_time() / 1000  # Time since last frame in seconds

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                pygame.quit()
                sys.exit()
            if event.key == pygame.K_SPACE:
                if not shooting:
                    shooting = True
                    fire_frame_index = 0
                    fire_animation_timer = 0
                    impulse.trigger()  # Trigger impulse when shooting

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_SPACE:
                pass  # Shooting animation will complete even after key is released

    # Movement
    keys = pygame.key.get_pressed()
    sin_a = math.sin(player_angle)
    cos_a = math.cos(player_angle)

    dx, dy = 0, 0
    is_moving = False  # Flag to check if the player is moving
    if keys[pygame.K_w]:
        dx += player_speed * cos_a
        dy += player_speed * sin_a
        is_moving = True
    if keys[pygame.K_s]:
        dx -= player_speed * cos_a
        dy -= player_speed * sin_a
        is_moving = True
    if keys[pygame.K_a]:
        dx += player_speed * sin_a
        dy -= player_speed * cos_a
        is_moving = True
    if keys[pygame.K_d]:
        dx -= player_speed * sin_a
        dy += player_speed * cos_a
        is_moving = True

    # Collision detection
    next_x = player_x + dx
    next_y = player_y + dy
    if world_map[int(next_y / TILE_SIZE)][int(next_x / TILE_SIZE)] == '.':
        player_x = next_x
        player_y = next_y

    if keys[pygame.K_LEFT]:
        player_angle -= 0.05
    if keys[pygame.K_RIGHT]:
        player_angle += 0.05

    # Update Bobbing
    bobbing.update(delta_time, is_moving)
    offset_x, offset_y = bobbing.get_offset()

    # Update Impulse
    impulse.update(delta_time)
    impulse_offset_x, impulse_offset_y = impulse.get_offset()

    # Drawing
    screen.fill(BLACK)
    ray_casting()

    # Update Enemy
    enemy.update(player_x, player_y, delta_time)

    # Draw Enemy
    enemy.draw(screen, bobbing_offset=(offset_x, offset_y), impulse_offset=(impulse_offset_x, impulse_offset_y))
    
    # Update shooting animation
    if shooting:
        fire_animation_timer += delta_time
        if fire_animation_timer >= fire_animation_speed:
            fire_animation_timer = 0
            fire_frame_index += 1
            if fire_frame_index >= len(fire_frames) - 1:
                shooting = False
                fire_frame_index = len(fire_frames) - 1  # Set to idle frame
    else:
        fire_frame_index = len(fire_frames) - 1  # Idle frame index

    # Apply bobbing and impulse offsets to the weapon's position
    fire_rect.center = (
        HALF_WIDTH + offset_x + impulse_offset_x,
        HEIGHT - fire_rect.height // 2 + offset_y + impulse_offset_y
    )

    # Draw shooting animation or idle sprite
    screen.blit(fire_frames[fire_frame_index], fire_rect)

    pygame.display.flip()
    clock.tick(60)