""" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Project: Platformer game Standard: TE PUNGA - PyGame Programming Assessment School: Tauranga Boys' College Author: Yashil Singh Date: July 2024 Python: 3.11.9 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ """ import pygame, sys, random, time, pymunk, pickle, math from pygame.locals import * from pygame import mixer from os import path DISPLAY_WIDTH = 1000 DISPLAY_HEIGHT = 1000 pygame.mixer.pre_init(44100, -16, 2, 512) mixer.init() pygame.init() display = pygame.display.set_mode((DISPLAY_WIDTH, DISPLAY_HEIGHT)) clock = pygame.time.Clock() FPS = 60 pygame.display.set_caption("Platformer") #Define font font_score = pygame.font.SysFont('Bauhaus 93', 30) font = pygame.font.SysFont('Bauhaus 93', 70) #define game variables tile_size = 50 game_over = 0 main_menu = True level = 1 MAX_LEVELS = 7 score = 0 #Define colours white = (255, 255, 255) blue = (0, 0, 255) #load images sun_img = pygame.image.load('Python/sun.png') bg_img = pygame.image.load('Python/Pink.png').convert_alpha() restart_img = pygame.image.load('Python/restart_btn.png') start_img = pygame.image.load('Python/start_btn.png') exit_img = pygame.image.load('Python/exit_btn.png') #Load sounds pygame.mixer.music.load("Python/music.wav") pygame.mixer.music.play(-1, 0.0, -1000) coin_fx = pygame.mixer.Sound("Python/coin.wav") coin_fx.set_volume(0.5) jump_fx = pygame.mixer.Sound("Python/jump.wav") jump_fx.set_volume(0.5) game_over_fx = pygame.mixer.Sound("Python/game_over.wav") game_over_fx.set_volume(0.5) def draw_text(text, font, text_col, x, y): img = font.render(text, True, text_col) display.blit(img, (x, y)) # Get the dimentions of the background img bg_width, bg_height = bg_img.get_size() #Func to reset level def reset_level(level): player.reset(100, DISPLAY_HEIGHT - 130) blob_group.empty() platform_group.empty() lava_group.empty() exit_group.empty() coin_group.empty() world_data = [] # Initialize world_data as an empty list if path.exists(f"level{level}_data"): with open(f"level{level}_data", "rb") as pickle_in: world_data = pickle.load(pickle_in) world = World(world_data) return world class Camera: def __init__(self, width, height, zoom=1): self.camera = pygame.Rect(0, 0, width, height) self.width = width self.height = height self.zoom = zoom def apply(self, entity): # Assuming entity has a rect attribute if hasattr(entity, 'rect'): return entity.rect.move(self.camera.topleft) elif isinstance(entity, pygame.Rect): return entity.move(self.camera.topleft) else: raise TypeError("Entity must be a pygame.Rect or have a rect attribute") def scale(self, surface): if self.zoom != 1: scaled_width = int(surface.get_width() * self.zoom) scaled_height = int(surface.get_height() * self.zoom) return pygame.transform.scale(surface, (scaled_width, scaled_height)) return surface def update(self, target): x = -target.rect.centerx + int(self.width / 2) y = -target.rect.centery + int(self.height / 2) # Limit scrolling to map size x = min(0, x) # Left y = min(0, y) # Top x = max(-(self.width - DISPLAY_WIDTH), x) # Right y = max(-(self.height - DISPLAY_HEIGHT), y) # Bottom self.camera = pygame.Rect(x, y, self.width, self.height) class Button(): def __init__(self, x, y, image): self.image = image self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.clicked = False def draw(self): action = False #Get mouse pos pos = pygame.mouse.get_pos() #Check mouseover & clicked condit. if self.rect.collidepoint(pos): if pygame.mouse.get_pressed()[0] == 1 and self.clicked == False: action = True self.clicked = True if pygame.mouse.get_pressed()[0] == 0: self.clicked = False display.blit(self.image, self.rect) return action class Player(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) self.reset(x, y) def update(self, game_over): dx = 0 dy = 0 walk_cooldown = 5 # Animated Sprite framerate for player, lower number for faster framerate col_thresh = 20 if game_over == 0: # Get key presses key = pygame.key.get_pressed() if key[pygame.K_SPACE]: if not self.jumped: if self.jump_count < 2: jump_fx.play() self.vel_y = -15 self.jumped = True self.jump_count += 1 else: self.jumped = False if key[pygame.K_LEFT]: dx -= 5 self.counter += 1 self.direction = -1 if key[pygame.K_RIGHT]: dx += 5 self.counter += 1 self.direction = 1 if not key[pygame.K_LEFT] and not key[pygame.K_RIGHT]: self.counter = 0 self.index = 0 if self.direction == 1: self.image = self.images_right[self.index] if self.direction == -1: self.image = self.images_left[self.index] # Handle animation if self.counter > walk_cooldown: self.counter = 0 self.index += 1 if self.index >= len(self.images_right): self.index = 0 if self.direction == 1: self.image = self.images_right[self.index] if self.direction == -1: self.image = self.images_left[self.index] # Add gravity self.vel_y += 1 if self.vel_y > 10: self.vel_y = 10 dy += self.vel_y # Check for collision self.in_air = True for tile in world.tile_list: # Check for collision in x direction if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 # Check for collision in the y direction if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): # Check if below the ground (jumping) if self.vel_y < 0: dy = tile[1].bottom - self.rect.top self.vel_y = 0 # Check if above the ground (falling) elif self.vel_y >= 0: dy = tile[1].top - self.rect.bottom self.vel_y = 0 self.in_air = False self.jump_count = 0 # Reset jump count when player is on the ground # Check for collision with enemies collisions = pygame.sprite.spritecollide(self, blob_group, False) if collisions: for blob in collisions: # Check if the player is falling and is above the blob if self.rect.bottom <= blob.rect.top + (blob.rect.height // 2) and self.vel_y >= -10: blob.die() jump_fx.play() # Play a sound effect for killing the enemy self.vel_y = -10 # Make the player jump after killing the enemy else: game_over = -1 game_over_fx.play() break # Check collision with lava if pygame.sprite.spritecollide(self, lava_group, False): game_over = -1 game_over_fx.play() # Check collision with exit if pygame.sprite.spritecollide(self, exit_group, False): game_over = 1 # Check for collision with platforms for platform in platform_group: # Collision in the x direction if platform.rect.colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 # Collision in the y direction if platform.rect.colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): # Check if below platform if abs((self.rect.top + dy) - platform.rect.bottom) < col_thresh: self.vel_y = 0 dy = platform.rect.bottom - self.rect.top # Check if above platform elif abs((self.rect.bottom + dy) - platform.rect.top) < col_thresh: self.rect.bottom = platform.rect.top - 1 self.in_air = False self.jump_count = 0 # Reset jump count when player is on the platform dy = 0 # Move sideways with platform if platform.move_x != 0: self.rect.x += platform.move_direction # Update player coordinates self.rect.x += dx self.rect.y += dy elif game_over == -1: self.image = self.dead_image draw_text('GAME OVER!', font, blue, (DISPLAY_WIDTH // 2) - 200, DISPLAY_HEIGHT // 2) if self.rect.y > 200: self.rect.y -= 5 # Draw player onto screen display.blit(self.image, self.rect) return game_over def reset(self, x, y, start_image=5, max_images=11): self.images_right = [] self.images_left = [] self.index = 0 self.counter = 0 # Ensure that the range does not go beyond max_images for num in range(start_image, min(start_image + 10, max_images)): try: img = pygame.image.load(f"Python/Run ({num}).png") img_right = pygame.transform.scale(img, (64, 64)) self.images_right.append(img_right) img_left = pygame.transform.flip(img_right, True, False) self.images_left.append(img_left) except FileNotFoundError: print(f"File Python/Run ({num}).png not found.") break self.dead_image = pygame.image.load('Python/ghost.png') self.image = self.images_right[self.index] self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.width = self.image.get_width() self.height = self.image.get_height() self.vel_y = 0 self.jumped = False self.direction = 0 self.in_air = True self.jump_count = 0 # Initialize jump count def draw(self, surface, camera): surface.blit(self.image, camera.apply(self)) class World(): def __init__(self, data): self.tile_list = [] #load images dirt_img = pygame.image.load('Python/Dirt(16x16).png') grass_img = pygame.image.load('Python/Terrain (21x21).png') row_count = 0 for row in data: col_count = 0 for tile in row: if tile == 1: img = pygame.transform.scale(dirt_img, (tile_size, tile_size)) img_rect = img.get_rect() img_rect.x = col_count * tile_size img_rect.y = row_count * tile_size tile = (img, img_rect) self.tile_list.append(tile) if tile == 2: img = pygame.transform.scale(grass_img, (tile_size, tile_size)) img_rect = img.get_rect() img_rect.x = col_count * tile_size img_rect.y = row_count * tile_size tile = (img, img_rect) self.tile_list.append(tile) if tile == 3: blob = Enemy(col_count * tile_size, row_count * tile_size + 15) blob_group.add(blob) if tile == 4: platform = Platform(col_count * tile_size, row_count * tile_size, 1, 0) platform_group.add(platform) if tile == 5: platform = Platform(col_count * tile_size, row_count * tile_size, 0, 1) platform_group.add(platform) if tile == 6: lava = Lava(col_count * tile_size, row_count * tile_size + tile_size // 2) lava_group.add(lava) if tile == 7: coin = Coin(col_count * tile_size + (tile_size // 2), row_count * tile_size + tile_size // 2) coin_group.add(coin) if tile == 8: exit = Exit(col_count * tile_size, row_count * tile_size - (tile_size // 2)) exit_group.add(exit) col_count += 1 row_count += 1 def draw(self): for tile in self.tile_list: display.blit(tile[0], tile[1]) class Enemy(pygame.sprite.Sprite): def __init__(self, x, y, start_image=1, max_images=10, death_start_image=1, death_max_images=5): pygame.sprite.Sprite.__init__(self) self.images = [] self.death_images = [] self.index = 0 self.death_index = 0 self.counter = 0 self.death_counter = 0 self.is_dead = False # Load running animation frames for num in range(start_image, start_image + max_images): try: img = pygame.image.load(f"Python/Slime ({num}).png") img = pygame.transform.scale(img, (tile_size, tile_size - 10)) self.images.append(img) except FileNotFoundError: print(f"File Python/Slime ({num}).png not found.") break # Load death animation frames for num in range(death_start_image, death_start_image + death_max_images): try: img = pygame.image.load(f"Python/Slime_death ({num}).png") img = pygame.transform.scale(img, (tile_size, tile_size)) self.death_images.append(img) except FileNotFoundError: print(f"File Python/Slime_death ({num}).png not found.") break self.image = self.images[self.index] self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.move_direction = 1 self.move_counter = 0 self.animation_speed = 8 # Adjust this value to change the animation speed self.death_animation_speed = 8 # Speed for death animation # Projectile variables self.shoot_cooldown = random.randrange(100, 500) # Cooldown before shooting again self.shoot_timer = 0 self.projectiles = pygame.sprite.Group() def update(self): if not self.is_dead: # Move the enemy self.rect.x += self.move_direction self.move_counter += 1 if abs(self.move_counter) > 50: self.move_direction *= -1 self.move_counter *= -1 # Handle running animation self.counter += 1 if self.counter >= self.animation_speed: self.counter = 0 self.index += 1 if self.index >= len(self.images): self.index = 0 self.image = self.images[self.index] # Shoot projectiles randomly self.shoot_timer += 1 if self.shoot_timer >= self.shoot_cooldown: self.shoot_timer = 0 self.shoot_projectile() # Update projectiles self.projectiles.update() else: # Handle death animation self.death_counter += 1 if self.death_counter >= self.death_animation_speed: self.death_counter = 0 self.death_index += 1 if self.death_index < len(self.death_images): self.image = self.death_images[self.death_index] else: self.kill() # Remove the enemy from the game once the death animation is complete def shoot_projectile(self): # Randomize shooting direction # Generate 30 evenly spaced angles around a circle num_angles = 90 angle_step = 720 / num_angles shoot_angles = [math.radians(angle_step * i) for i in range(num_angles)] selected_angle = random.choice(shoot_angles) # Create a projectile instance and add to projectiles group projectile = EnemyProjectile(self.rect.centerx, self.rect.centery, selected_angle) self.projectiles.add(projectile) def die(self): self.is_dead = True self.death_index = 0 self.death_counter = 0 self.image = self.death_images[self.death_index] blob_group.remove(self) # Remove from the main group dead_blob_group.add(self) # Add to the dead group class EnemyProjectile(pygame.sprite.Sprite): def __init__(self, x, y, angle): pygame.sprite.Sprite.__init__(self) self.image = pygame.Surface((10, 10)) # Customize size and appearance as needed self.image.fill((0, 255, 0)) # Green color self.rect = self.image.get_rect() self.rect.center = (x, y) self.speed = random.randrange(1, 5) # Adjust speed as needed # Calculate movement based on angle self.dx = self.speed * math.cos(angle) * -1 self.dy = self.speed * math.sin(angle) def update(self): # Move the projectile self.rect.x += self.dx self.rect.y += self.dy # Remove the projectile if it goes off-screen if self.rect.x > DISPLAY_WIDTH or self.rect.x < 0 or self.rect.y > DISPLAY_HEIGHT or self.rect.y < 0: self.kill() # Remove from sprite groups # Collision with player if pygame.sprite.collide_rect(self, player): global game_over game_over = -1 # Player is hit, game over class Platform(pygame.sprite.Sprite): def __init__(self, x, y, move_x, move_y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load("Python/platform.png") self.image = pygame.transform.scale(img, (tile_size, tile_size // 2)) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.move_counter = 0 self.move_direction = 1 self.move_x = move_x self.move_y = move_y def update(self): self.rect.x += self.move_direction * self.move_x self.rect.y += self.move_direction * self.move_y self.move_counter += 1 if abs(self.move_counter) > 50: self.move_direction *= -1 self.move_counter *= -1 class Particle(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((5, 5)) self.image.fill((255, 69, 0)) # Lava color self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.velocity_y = random.uniform(-5, -1) self.gravity = 0.05 self.start_y = y print(f"Particle created at ({x}, {y})") def update(self): self.velocity_y += self.gravity self.rect.y += self.velocity_y if self.rect.y > self.start_y: self.kill() print(f"Particle updated to ({self.rect.x}, {self.rect.y})") class Lava(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() img = pygame.image.load("Python/lava.png") self.image = pygame.transform.scale(img, (tile_size, tile_size // 2)) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y print("Lava initialized") def update(self): if random.randint(1, 100) == 1: self.generate_particle() print(f"Lava updated with {len(lava_group)} particles") def generate_particle(self): particle = Particle(self.rect.x + random.randint(0, self.image.get_width()), self.rect.y + self.image.get_height() // 2) particle.start_y = self.rect.y + self.image.get_height() // 2 lava_group.add(particle) print("Particle generated") def draw(self, screen): screen.blit(self.image, self.rect) print("Lava drawn") class Coin(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load("Python/coin.png") self.image = pygame.transform.scale(img, (tile_size // 2, tile_size // 2)) self.rect = self.image.get_rect() self.rect.center = (x, y) class Exit(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load("Python/exit.png") self.image = pygame.transform.scale(img, (tile_size, int (tile_size * 1.5))) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y player = Player(100, DISPLAY_HEIGHT - 130) blob_group = pygame.sprite.Group() platform_group = pygame.sprite.Group() lava_group = pygame.sprite.Group() coin_group = pygame.sprite.Group() exit_group = pygame.sprite.Group() dead_blob_group = pygame.sprite.Group() #Create dummy coin for showing the score score_coin = Coin(tile_size //2, tile_size// 2) coin_group.add(score_coin) # Load level data and create world if path.exists(f"level{level}_data"): pickle_in = open(f"level{level}_data", "rb") world_data = pickle.load(pickle_in) world = World(world_data) #Create buttons restart_button = Button(DISPLAY_WIDTH//2 - 50, DISPLAY_HEIGHT // 2 + 100, restart_img) start_button = Button(DISPLAY_WIDTH // 2 - 350, DISPLAY_HEIGHT // 2, start_img) exit_button = Button(DISPLAY_WIDTH // 2 + 150, DISPLAY_HEIGHT // 2, exit_img) #Load coin png coin_img = pygame.image.load('Python/coin.png') scaled_coin_img = pygame.transform.scale(coin_img, (30, 30)) # Game loop variables FPS = 60 clock = pygame.time.Clock() run = True bg_x = 0 # Initial x coordinate of the background bg_y = 0 # Initial y coordinate of the background scroll_speed = 1 # Speed of scrolling sunset_speed = 0.1 # Rate of sunset transition (adjust as needed) bg_color = (135, 206, 235) # Initial background color lava = Lava(100, 300) # Initialize the camera camera = Camera(500, 500) #Set opacity opacity = 128 bg_img.set_alpha(opacity) # Main game loop while run: clock.tick(FPS) # Event handling for event in pygame.event.get(): if event.type == pygame.QUIT: run = False # Adjust the background position bg_x -= scroll_speed bg_y -= scroll_speed # Update sunset effect if bg_color[2] > 50: # Check if not yet fully dark (adjust threshold as needed) bg_color = (bg_color[0], bg_color[1], max(bg_color[2] - sunset_speed, 50)) # Wrap around the background to create a seamless loop bg_x %= bg_img.get_width() bg_y %= bg_img.get_height() # Draw the background with updated color display.fill(bg_color) # Blit the background image with transparency for x in range(-bg_img.get_width(), DISPLAY_WIDTH, bg_img.get_width()): for y in range(-bg_img.get_height(), DISPLAY_HEIGHT, bg_img.get_height()): display.blit(bg_img, (bg_x + x, bg_y + y)) # Draw the sun display.blit(sun_img, (100, 100)) # Adjust position based on your image if main_menu: if exit_button.draw(): run = False if start_button.draw(): main_menu = False else: world.draw() if game_over == 0: blob_group.update() platform_group.update() lava_group.update() dead_blob_group.update() # Blit the coin image in the top left corner display.blit(scaled_coin_img, (10, 10)) # Update and handle enemy projectiles for enemy in blob_group: enemy.update() enemy.projectiles.update() # Update enemy projectiles # Update score # Check if a coin has been collected if pygame.sprite.spritecollide(player, coin_group, True): score += 1 coin_fx.play() draw_text("X " + str(score), font_score, white, tile_size - 10, 10) blob_group.draw(display) dead_blob_group.draw(display) platform_group.draw(display) lava_group.draw(display) coin_group.draw(display) exit_group.draw(display) game_over = player.update(game_over) # Draw and update enemy projectiles for enemy in blob_group: enemy.projectiles.draw(display) # Player has died if game_over == -1: if restart_button.draw(): world_data = [] world = reset_level(level) game_over = 0 score = 0 # If player has completed the level if game_over == 1: # Reset game and go to next level level += 1 if level <= MAX_LEVELS: # Reset level world_data = [] world = reset_level(level) game_over = 0 else: draw_text('YOU WIN!', font, blue, (DISPLAY_WIDTH // 2) - 140, DISPLAY_HEIGHT // 2) if restart_button.draw(): level = 1 # Reset level world_data = [] world = reset_level(level) game_over = 0 score = 0 for event in pygame.event.get(): if event.type == pygame.QUIT: run = False pygame.display.update() pygame.quit()