""" ----------------------------------------- Project: Escape Drake Standard: 91896 (2.7) School: Tauranga Boys' Collage Auther: Hunter Moss Date: May 2024 Python: 3.11.2 ----------------------------------------- """ #Imports import pygame import os import pickle from os import path from pygame import mixer pygame.mixer.pre_init(48000, -16, 2, 512) mixer.init pygame.init() # Screen Size screen_width = 1000 screen_height = 1000 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption('Escape Drake') # Load images background_img = pygame.image.load('Images/Background.png') mute_img = pygame.image.load('Images/mute_icn.png') unmute_img = pygame.image.load('Images/unmute_icn.png') mute_img = pygame.transform.scale(mute_img, (50, 50)) unmute_img = pygame.transform.scale(unmute_img, (50, 50)) # Buttons mute_button = mute_img.get_rect() mute_button = mute_img.get_rect(topleft=(screen_width - 50, 0)) # Colours Red = (255, 0, 0) Black = (0, 0, 0) # Constants tile_size = 50 fps = 60 # Game variables level = 1 font_size = 60 #Fonts monsterfnt = pygame.font.Font('Fonts/Melted Monster.ttf', 60) #Various sizes of fonts monsterfnt2 = pygame.font.Font('Fonts/Melted Monster.ttf', 120) monsterfnt3 = pygame.font.Font('Fonts/Melted Monster.ttf', 80) monsterfnt4 = pygame.font.Font('Fonts/Melted Monster.ttf', 40) #load music bg_music = pygame.mixer.music.load('Music/Not Like Us.mp3') # Sets the background music pygame.mixer.music.play(-1, 0.0, 0) pygame.mixer.music.set_volume(0.5) #Halve the volume of the music # Load player images function def load_images(folder, prefix, count): #Loads the images in away that allows for plently of frames for the player animations return [pygame.transform.scale(pygame.image.load(os.path.join(folder, f"{prefix} ({i}).png")), (tile_size, tile_size)) for i in range(1, count + 1)] class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.images = { #Various animation images getting loaded 'idle': load_images('Images/Player', 'idle', 16), 'run': load_images('Images/Player', 'run', 20), 'jump': load_images('Images/Player', 'jump', 30), 'dead': load_images('Images/Player', 'dead', 30) } self.image = self.images['idle'][0] #Makes sure that idle is the defualt animation self.rect = self.image.get_rect(topleft=(50, screen_height - 50)) #Player rect self.vel = pygame.math.Vector2(0, 0) #Positions the sprites rect at the bottom self.speed = 5 self.gravity = 0.5 self.jump_power = -10 #Smaller jumps since its a little girl self.on_ground = False self.jumping = False self.facing_right = True #Makes sure sprite faces right upon starting self.frame = 0 self.idle_animation_speed = 0.2 self.animation_speed = 0.6 self.alive = True self.keys_collected = 0 self.moving = False self.facing_before_death = True #Saves the facing direction before death def reset(self): self.rect.topleft = (10, screen_height - 50) # Player start co-ordernaces self.alive = True self.keys_collected = 0 self.moving = False self.frame = 0 def update(self, tiles, door, drakes, trogs, keys, boss_drake): # Block updating if self.alive: keys_pressed = pygame.key.get_pressed() if keys_pressed[pygame.K_LEFT] or keys_pressed[pygame.K_RIGHT]: # Allows for imputs from the user self.moving = True if keys_pressed[pygame.K_LEFT]: # Move left self.vel.x = -self.speed self.facing_right = False elif keys_pressed[pygame.K_RIGHT]: # Move right self.vel.x = self.speed self.facing_right = True else: self.vel.x = 0 # Stationary if (keys_pressed[pygame.K_UP]) and self.on_ground: self.vel.y = self.jump_power self.on_ground = False # Stops the player from jumping while in mid air self.jumping = True self.vel.y += self.gravity self.rect.x += self.vel.x self.check_collision_x(tiles) # X tile collision self.rect.y += self.vel.y self.check_collision_y(tiles) # Y tile collision self.rect.clamp_ip(pygame.Rect(0, 0, screen_width, screen_height)) # Important line of code that stops the player moving off of the screen if door and self.rect.colliderect(door.rect): # Door collision door.open = True if level < 3: # Next level if under level is under 3 increase_level() else: end_game() # Once level 3 is completed this trigger end game for drake in drakes: if self.rect.colliderect(drake.rect): # Drake collision self.alive = False self.facing_before_death = self.facing_right # This allows the direction of death to be saved allow the death animation to play facing the correct way for trog in trogs: if self.rect.colliderect(trog.rect): # Trog collision self.alive = False self.facing_before_death = self.facing_right for key in keys: # Key collision if self.rect.colliderect(key.rect): keys.remove(key) # Removes the key once collected self.keys_collected += 1 if boss_drake and self.rect.colliderect(boss_drake.rect) and level == 3: # Triggers Boss Drakes collision, this is what needed editiing to allow for drake to ghost self.alive = False self.facing_before_death = self.facing_right if self.jumping: # Cycle jump animation self.image = self.images['jump'][0] elif self.vel.x != 0: self.frame = (self.frame + self.animation_speed) % len(self.images['run']) # Cycles running animtion at a set variable stated before self.image = self.images['run'][int(self.frame)] else: self.frame = (self.frame + self.idle_animation_speed) % len(self.images['idle']) # Cycles idle animation at an increased speed self.image = self.images['idle'][int(self.frame)] if not self.facing_right: # Flips the animtions if needed self.image = pygame.transform.flip(self.image, True, False) if self.rect.bottom >= screen_height: # Makes sure player is on ground self.on_ground = True self.jumping = False self.vel.y = 0 else: self.frame = (self.frame + self.animation_speed) % len(self.images['dead']) # Death animation self.image = self.images['dead'][int(self.frame)] if not self.facing_before_death: # Flip death animation self.image = pygame.transform.flip(self.image, True, False) if self.frame >= len(self.images['dead']) - 1: # Trips retry screen upon death show_retry_screen() if boss_drake: boss_drake.reset() # Reset boss drake position when player dies def check_collision_x(self, tiles): # Allows the player to collide with the X axis for tile in tiles: if self.rect.colliderect(tile.rect): if self.vel.x > 0: self.rect.right = tile.rect.left elif self.vel.x < 0: self.rect.left = tile.rect.right def check_collision_y(self, tiles): # Allows the player to collide with the Y axis for tile in tiles: if self.rect.colliderect(tile.rect): if self.vel.y > 0: self.rect.bottom = tile.rect.top self.on_ground = True self.jumping = False # Needed to allow player to jump while moving up self.vel.y = 0 elif self.vel.y < 0: self.rect.top = tile.rect.bottom self.vel.y = 0 # Block Classes class Block(pygame.sprite.Sprite): def __init__(self, x, y, image, scale_to_tile=True): super().__init__() if scale_to_tile: self.image = pygame.transform.scale(image, (tile_size, tile_size)) else: self.image = image self.rect = self.image.get_rect(topleft=(x, y)) class Brick0(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/brick_dark0.png') super().__init__(x, y, image) class Brick1(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/brick_dark1.png') super().__init__(x, y, image) class Brick2(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/brick_dark2.png') super().__init__(x, y, image) class Brick3(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/brick_dark3.png') super().__init__(x, y, image) class Cobbweb(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/Cobbwebs.png') super().__init__(x, y, image) class Grill(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/Grill.png') super().__init__(x, y, image) class Skulls(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/skulls.png') super().__init__(x, y, image) class Platform(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/platform.png') original_width, original_height = image.get_size() resized_image = pygame.transform.scale(image, (tile_size, original_height + 5)) adjusted_y = y + tile_size - original_height - 5 # Position at the bottom of the tile super().__init__(x, adjusted_y, resized_image, scale_to_tile=False) class Platform_horz(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/platform_horz.png') original_width, original_height = image.get_size() resized_image = pygame.transform.scale(image, (tile_size, original_height + 5)) adjusted_y = y + tile_size - original_height - 5 # Position at the bottom of the tile super().__init__(x, adjusted_y, resized_image, scale_to_tile=False) self.move_speed = 2 self.range = 150 # Adjust this value to change the platform's movement range self.start_x = x self.direction = 1 def update(self): # Updates the players direction when moving on a horzontal tile self.rect.x += self.move_speed * self.direction if self.direction == 1: if self.rect.x - self.start_x >= self.range: self.direction = -1 else: if self.start_x - self.rect.x >= self.range: self.direction = 1 class Platform_vert(Block): def __init__(self, x, y): image = pygame.image.load('Images/Level/platform_vert.png') original_width, original_height = image.get_size() resized_image = pygame.transform.scale(image, (tile_size, original_height + 5)) # Resize the platform adjusted_y = y + tile_size - original_height - 5 # Position at the bottom of the tile super().__init__(x, adjusted_y, resized_image, scale_to_tile=False) self.move_speed = 3 self.range = 150 # Adjust this value to change the platform's movement range self.start_y = y # Remember the starting y position self.direction = 1 def update(self): # Updates the players direction when moving on a vertical tile self.rect.y += self.move_speed * self.direction if self.direction == 1: if self.rect.y - self.start_y >= self.range: # Moving down self.direction = -1 else: if self.start_y - self.rect.y >= self.range: # Moving up self.direction = 1 class Trog(Block): def __init__(self, x, y): image = pygame.image.load('Images/dngn_altar_trog.png') # Loads the image image = pygame.transform.scale(image, (tile_size, tile_size)) # Resizses image super().__init__(x, y, image) class Key(Block): def __init__(self, x, y): image = pygame.image.load('Images/Key.png') # Loads the image resized_image = pygame.transform.scale(image, (50, 20)) # Resize key image to 50x20 pixels adjusted_y = y + (tile_size - resized_image.get_height()) // 2 # Defines how the key needs to be centered super().__init__(x, adjusted_y, resized_image, scale_to_tile=False) # Centers the key in the grid class Drake(Block): def __init__(self, x, y): image = pygame.image.load('Images/DRAKE.png') # Loads the image image = pygame.transform.scale(image, (tile_size, tile_size)) # Resizses image super().__init__(x, y, image) class BossDrake(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() image = pygame.image.load('Images/DRAKE.png') # Loads the image self.image = pygame.transform.scale(image, (tile_size * 2, tile_size * 2)) # Make Boss Drake twice the size of a normal one self.rect = self.image.get_rect(topleft=(x, y)) # Creates a rect for boss drakes collision self.starting_position = (0 + (tile_size * 2), 0) # Sets the starting image self.speed = 0.6 # Speed at which the boss drake moves def reset(self): self.rect.topleft = self.starting_position # Defines the starting position for boss drake def update(self, player): #Allows drake to follow the player in the boss level if self.rect.x < player.rect.x: self.rect.x += self.speed elif self.rect.x > player.rect.x: self.rect.x -= self.speed if self.rect.y < player.rect.y: self.rect.y += self.speed elif self.rect.y > player.rect.y: self.rect.y -= self.speed class Door(Block): def __init__(self, x, y): self.closed_image = pygame.image.load('Images/dngn_closed_door.png') # Loads the first image self.open_image = pygame.image.load('Images/dngn_open_door.png') # Loads the second image self.closed_image = pygame.transform.scale(self.closed_image, (tile_size, tile_size)) # Resize the first image to match the tile size self.open_image = pygame.transform.scale(self.open_image, (tile_size, tile_size)) # Resizse the second image to match the tile size super().__init__(x, y, self.closed_image) self.open = False # Sets defualt position def update(self): # Creates the animtion for the door opening if self.open: self.image = self.open_image else: self.image = self.closed_image # World class class World: def __init__(self, data): self.tile_list = [] # Creates a chain for world blocks from level editor self.floor_group = pygame.sprite.Group() # Group for floor tiles self.drake_group = pygame.sprite.Group() # Group for drakes self.trog_group = pygame.sprite.Group() # Group for trog self.key_group = pygame.sprite.Group() # Group for keys self.platform_vert_group = pygame.sprite.Group() # Group for vertical platforms self.platform_horz_group = pygame.sprite.Group() # Group for horizontal platforms self.door_group = pygame.sprite.GroupSingle() # Group for the door self.load_world(data) # Loads the world data for the bricks and other map peices def load_world(self, data): block_types = { # Gives each block a group 'brick0': Brick0, 'brick1': Brick1, 'brick2': Brick2, 'brick3': Brick3, 'cobbweb': Cobbweb, 'grill': Grill, 'skulls': Skulls, 'platform': Platform, 'platform_vert': Platform_vert, 'platform_horz': Platform_horz, 'trog': Trog, 'key': Key, 'drake': Drake, 'door': Door } for block in data: x, y, block_type = block if block_type in block_types: # Creates the instances for each group of blocks block_class = block_types[block_type] block_instance = block_class(x, y) if block_type == 'door': self.door_group.add(block_instance) elif block_type == 'drake': self.drake_group.add(block_instance) elif block_type == 'trog': self.trog_group.add(block_instance) elif block_type == 'key': self.key_group.add(block_instance) elif block_type == 'platform_vert': self.platform_vert_group.add(block_instance) elif block_type == 'platform_horz': self.platform_horz_group.add(block_instance) else: self.tile_list.append(block_instance) self.floor_group.add(block_instance) def draw(self): # Draws each group on the screen for tile in self.tile_list: screen.blit(tile.image, tile.rect) self.door_group.draw(screen) self.drake_group.draw(screen) self.trog_group.draw(screen) self.key_group.draw(screen) self.platform_vert_group.draw(screen) self.platform_horz_group.draw(screen) def get_tiles(self): # Holds the position for the return self.floor_group def get_keys(self): # Holds the position for the keys return self.key_group def increase_level(): # Defines what to do in the case of the level variable increasing global world global player global level level += 1 print(f"Loading level {level}") # Prints the level staus in the console if path.exists(f'level_data/level{level}_data'): # Checks for the level data with open(f'level_data/level{level}_data', 'rb') as pickle_in: # Opens the level data world_data = pickle.load(pickle_in) # loads the level data player.reset() # Reset player state world = World(world_data) # Re-initialize the world else: end_game() def reset_game(): # Resets variables when the game is reset global level, world, player level = 1 player.reset() reset_level() def reset_level(): global world global player global level print(f"Resetting level {level}") # Prints restting level in the console if path.exists(f'level_data/level{level}_data'): # Reloads the level with open(f'level_data/level{level}_data', 'rb') as pickle_in: world_data = pickle.load(pickle_in) world = World(world_data) else: world = World([]) # Default to an empty world if level data is not found to avoid crashing the game player.reset() # Reset player state def end_game(): # Draws the victory screen global run, retry_screen text = monsterfnt3.render('You Escaped Drake', True, (255, 255, 255)) # Draws vicotry text text_rect = text.get_rect(center=(screen_width // 2, screen_height // 2)) # Postions the text retry_button_rect = pygame.Rect(0, 0, 900, 80) #Draws a rect around the retry button retry_button_rect.center = (screen_width // 2, screen_height // 2 + 200) # Postions that rect retry_screen = True while retry_screen: for event in pygame.event.get(): # Input handling for the menu if event.type == pygame.QUIT: # If player chooses to quit pygame.quit() # Quits game quit() if event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: # Detect when space is pressed reset_game() # Reset the game state start_menu() # Return to the main menu retry_screen = False screen.blit(text, text_rect) # Draws the text on screen pygame.draw.rect(screen, (255, 0, 0), retry_button_rect) # Draws the rect on screen retry_text = monsterfnt4.render('Press Space to Escape Again With a Twist', True, (255, 255, 255)) # The text shown on screen retry_text_rect = retry_text.get_rect(center=retry_button_rect.center) # Positions the rect on the screen screen.blit(retry_text, retry_text_rect) #Draws text and rect on screen pygame.display.flip() # Updates screen def show_retry_screen(): # Death screen global run, retry_screen text = monsterfnt.render('You failed to Escape Drake!', True, (255, 255, 255)) # Screen text text_rect = text.get_rect(center=(screen_width // 2, screen_height // 2)) # Postions the text retry_button_rect = pygame.Rect(0, 0, 650, 80) # Draws a rect around the text retry_button_rect.center = (screen_width // 2, screen_height // 2 + 100) # Positions the rect on the screen retry_screen = True while retry_screen: for event in pygame.event.get(): # Allows the player to quit if event.type == pygame.QUIT: pygame.quit() quit() if event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE: # Detect space being pressed reset_level() # Restarts the level retry_screen = False screen.blit(text, text_rect) # Draws the text and rect on screen pygame.draw.rect(screen, (255, 0, 0), retry_button_rect) retry_text = monsterfnt.render('Press Space to Retry', True, (0, 0, 0)) retry_text_rect = retry_text.get_rect(center=retry_button_rect.center) screen.blit(retry_text, retry_text_rect) # Draws the text and rect on screen pygame.display.flip() # Updates the screen # Start Menu function def start_menu(): # Start menu global run menu = True while menu: title = monsterfnt2.render('Escape Drake', True, (255, 0, 0)) # Creates the the game title screen.fill((0, 0, 0)) # Fills the background with block for the menu screen screen.blit(title, (screen_width // 2 - title.get_width() // 2, screen_height // 4)) # Sets the location of the title on screen start_button = pygame.Rect(screen_width // 2 - 250, screen_height // 2 - 50, 500, 100) # Sets the location of the start button pygame.draw.rect(screen, (0, 255, 0), start_button) # Draws a rectangle around the start button start_text = monsterfnt.render('Start Running!', True, (0, 0, 0)) # Text on the start button screen.blit(start_text, (screen_width // 2 - start_text.get_width() // 2, screen_height // 2 - start_text.get_height() // 2)) # Locates the start button on the screen quit_button = pygame.Rect(screen_width // 2 - 250, screen_height // 2 + 125, 500, 100) # Sets the location of the quit button pygame.draw.rect(screen, (255, 0, 0), quit_button) # Draws the rect on screen and gives it a colour quit_text = monsterfnt.render('Nevermind :(', True, (0, 0, 0)) # Quit button text screen.blit(quit_text, (screen_width // 2 - quit_text.get_width() // 2, screen_height // 2 + 100 + quit_text.get_height() // 2)) # Text location pygame.display.flip() # Updates the screen for event in pygame.event.get(): if event.type == pygame.QUIT: # Qutting stuff pygame.quit() exit() if event.type == pygame.MOUSEBUTTONDOWN: # Allows the button to be clicked if start_button.collidepoint(event.pos): reset_game() # Reset the game state when starting a new game menu = False if quit_button.collidepoint(event.pos): # Mosue clicking on the quit button pygame.quit() exit() def main(): # Main loop global world global player global level global run run = True clock = pygame.time.Clock() player = Player() player_group = pygame.sprite.Group() # Creates player group player_group.add(player) # Added sprite into player group is_muted = False # Sets mute button to unmuted by defualt boss_drake = None # Initialize boss_drake variable # Draw mute button mute_image = mute_img if is_muted else unmute_img screen.blit(mute_image, mute_button.topleft) # Load the first level start_menu() reset_level() while run: # Event handling for event in pygame.event.get(): #Qutting stuff if event.type == pygame.QUIT: run = False if event.type == pygame.MOUSEBUTTONDOWN: # Makes the mute button clickable if mute_button.collidepoint(event.pos): is_muted = not is_muted if is_muted: pygame.mixer.music.pause() # Pauses the music else: pygame.mixer.music.unpause() # Unpauses the music # Check if in the final level and player has started moving if level == 3 and player.moving and boss_drake is None: boss_drake = BossDrake(0 + (tile_size * 2), 0) # Makes Boss Drake twice the size of the rest # Update player and check collisions player.update(world.get_tiles(), world.door_group.sprite, world.drake_group, world.trog_group, world.get_keys(), boss_drake) if world.door_group.sprite: # Updates the door for the door animation world.door_group.update() # Update moving platforms for platform in world.platform_vert_group: # Updates the vertical platforms platform.update() for platform in world.platform_horz_group: # Updates the horzontal platforms platform.update() # Collision detection with moving platforms platform_groups = pygame.sprite.Group(world.platform_vert_group, world.platform_horz_group) # Makes a general platform group platform_collisions = pygame.sprite.spritecollide(player, platform_groups, False) # Links the collsion for the platofrm group in for platform in platform_collisions: if player.vel.y > 0: # Moving down # Calculate the difference in the platform's vertical position before and after moving platform_movement = platform.rect.top - platform.rect.y player.rect.bottom = platform.rect.top player.vel.y = 0 player.on_ground = True # Allows player to jump while standing on the platform player.jumping = False # Update player's position by the same amount as the platform's movement player.rect.y += platform_movement elif player.vel.y < 0: # Moving up player.rect.top = platform.rect.bottom player.vel.y = 0 if player.rect.colliderect(platform.rect): # Check if still colliding after being adjusted if player.vel.x > 0: # Moving right player.rect.right = platform.rect.left elif player.vel.x < 0: # Moving left player.rect.left = platform.rect.right # Drawing screen.blit(background_img, (0, 0)) # Draw background world.draw() # Draws world class player_group.draw(screen) # Draws the player group world.door_group.draw(screen) # Draws the door group world.key_group.draw(screen) # Draws the key group world.drake_group.draw(screen) # Draws the Drake group world.trog_group.draw(screen) # Draws the trog group world.platform_vert_group.draw(screen) # Draws the vertical platform group world.platform_horz_group.draw(screen) # Draws the horzonal platform group # Draw mute button mute_image = mute_img if is_muted else unmute_img # Checks which mute icon to draw screen.blit(mute_image, mute_button.topleft) # Draws the mute button if boss_drake: boss_drake.update(player) # Update Boss Drake for its movement screen.blit(boss_drake.image, boss_drake.rect.topleft) # Draws Boss Drake on the screen when called # Update display pygame.display.flip() # Updates the screen clock.tick(fps) # Sets the game speed pygame.quit() # Quits the game if __name__ == "__main__": # Ensures main loop in run currectly main()