# -------------------------------------- # Project: Platformer # Standard: 91896 # School: Tauranga Boys College # Author: Hayden De Rohan # Date: April 28th, 2025 # Python: 3.11.9 # Description: A 2D platformer game with player movement, enemies, collectibles, and level progression. # -------------------------------------- # Controls: # Movement: # Left - A # Right - D # Jump - Space # Interact - E # Import required libraries import pygame import math from pygame import mixer import os import random import csv import button from debug import debug from support import import_folder # Initialize Pygame and mixer mixer.init() pygame.init() # Set up display constants SCREEN_W = 800 SCREEN_H = int(SCREEN_W * 0.9) # Create game window screen = pygame.display.set_mode((SCREEN_W, SCREEN_H)) pygame.display.set_caption('Platformer') # Set game framerate clock = pygame.time.Clock() FPS = 60 # Define game constants GRAVITY = 0.6 SCROLL_THRESH = 200 ROWS = 16 COLS = 150 TILE_SIZE = SCREEN_H // ROWS TILE_TYPES = 21 MAX_LEVELS = 3 screen_scroll = 0 bg_scroll = 0 level = 0 start_game = False start_intro = False level_complete = False score = 0 high_score = 0 music_started = False # Define player action variables moving_left = False moving_right = False # Load tile images img_list = [] for x in range(TILE_TYPES): # Load and scale each tile image img = pygame.image.load(f'Assets/Tiles/{x}.png').convert_alpha() img = pygame.transform.scale(img, (TILE_SIZE, TILE_SIZE)) img_list.append(img) # Load projectile image Projectile_img = pygame.image.load('Assets/Enemy2/attack/fire.png').convert_alpha() # Load background image bg_img = pygame.image.load('Assets/bg.png').convert_alpha() # Load button images start_img = pygame.image.load('Assets/Buttons/start_img.png').convert_alpha() exit_img = pygame.image.load('Assets/Buttons/exit_img.png').convert_alpha() restart_img = pygame.image.load('Assets/Buttons/restart_img.png').convert_alpha() interact_img = pygame.image.load('Assets/Buttons/interactE.png').convert_alpha() # Scale interact button image interact_img = pygame.transform.scale( interact_img, (int(interact_img.get_width() * 0.4), int(interact_img.get_height() * 0.4)) ) # Load sound effects shoot = pygame.mixer.Sound("Assets/Audio/shoot.mp3") coin_get = pygame.mixer.Sound("Assets/Audio/coin.mp3") coin_get.set_volume(0.3) hurt = pygame.mixer.Sound("Assets/Audio/hurt.mp3") jump = pygame.mixer.Sound("Assets/Audio/jump.mp3") jump.set_volume(0.5) key_get = pygame.mixer.Sound("Assets/Audio/key.mp3") # Load background music bg_music = pygame.mixer.Sound("Assets/Audio/bg_music.mp3") # Define colors BG = (144, 201, 120) # Background color RED = (255, 0, 0) # Red for damage and debugging WHITE = (255, 255, 255) # White for text GREEN = (0, 255, 0) # Green for health bar BLACK = (0, 0, 0) # Black for borders PINK = (235, 65, 54) # Pink for death fade # Define font for text rendering font = pygame.font.SysFont('Futura', 30) # Load high score from file def load_score(): """Load the high score from score.txt. Returns 0 if file doesn't exist.""" try: with open("score.txt", "r") as file: value = int(file.read()) print(f"High score loaded: {value}") return value except FileNotFoundError: return 0 # Save high score to file def save_score(score): """Save the given score to score.txt.""" with open("score.txt", "w") as file: file.write(str(score)) # Render text on screen def draw_text(text, font, text_col, x, y): """Render text at specified coordinates with given font and color.""" img = font.render(text, True, text_col) screen.blit(img, (x, y)) # Draw background def draw_bg(): """Fill screen with background color and draw scrolling background image.""" screen.fill(BG) width = bg_img.get_width() for x in range(5): screen.blit(bg_img, ((x * width) - bg_scroll * 0.5, 0)) # Set up game elements and music def game_setup(): """Update and draw sprite groups, and start background music if not already playing.""" global music_started if not music_started: pygame.mixer.music.load("Assets/Audio/bg_music.mp3") pygame.mixer.music.set_volume(0.4) pygame.mixer.music.play(loops=-1) music_started = True # Update sprite groups Projectile_group.update() grenade_group.update() explosion_group.update() item_box_group.update() decoration_group.update() water_group.update() exit_group.update() key_group.update() coin_group.update() sign_group.update() # Draw sprite groups Projectile_group.draw(screen) grenade_group.draw(screen) explosion_group.draw(screen) item_box_group.draw(screen) decoration_group.draw(screen) water_group.draw(screen) exit_group.draw(screen) key_group.draw(screen) coin_group.draw(screen) sign_group.draw(screen) # Reset level data def reset_level(): """Clear sprite groups and reset level data to empty tiles.""" enemy_group.empty() Projectile_group.empty() decoration_group.empty() water_group.empty() exit_group.empty() coin_group.empty() key_group.empty() sign_group.empty() Player.picked_up_key = False # Create empty tile list data = [] for row in range(ROWS): r = [-1] * COLS data.append(r) return data # Character class for Player and Enemy1 class Character(pygame.sprite.Sprite): def __init__(self, char_type, x, y, scale, speed): """Initialize character with type, position, scale, and speed.""" pygame.sprite.Sprite.__init__(self) self.alive = True self.char_type = char_type self.speed = speed self.shoot_cooldown = 0 self.health = 100 self.max_health = self.health self.direction = pygame.math.Vector2(0, 0) self.vel_y = 0 self.jump = False self.flip = False self.action = 0 self.update_time = pygame.time.get_ticks() self.in_air = False self.on_ground = False self.scale = scale self.picked_up_key = False self.import_character_assets() # Animation variables self.frame_index = 0 self.animation_speed = 0.15 self.image = self.animations['idle'][self.frame_index] self.rect = self.image.get_rect() self.rect.midbottom = (x, y) self.width = self.image.get_width() * scale self.height = self.image.get_height() * scale self.collision_rect = pygame.Rect(self.rect.x, self.rect.y, self.width, self.height) # Status variables self.facing_right = True self.status = 'idle' # AI-specific variables self.move_counter = 0 self.vision = pygame.Rect(0, 0, 225, 40) self.idling = False self.idling_counter = 0 self.player_found = False self.Can_damage_player = False # Damage cooldown variables self.damage_cooldown = 0 self.damage_cooldown_time = 1000 self.damage_amount = 20 # Damage animation variables self.damage_timer = 0 self.damage_duration = 750 self.is_damaged = False # Red flash effect variables self.red_flash_active = False self.red_flash_timer = 0 self.red_flash_duration = 200 def import_character_assets(self): """Load and scale character animation frames from asset folder.""" Path = f'Assets/{self.char_type}/' self.animations = {'idle': [], 'walk': [], 'jump': [], 'death': [], 'fall': [], 'damaged': []} for animation in self.animations.keys(): full_path = Path + animation loaded_frames = import_folder(full_path) scaled_frames = [] for image in loaded_frames: width = int(image.get_width() * self.scale) height = int(image.get_height() * self.scale) scaled_image = pygame.transform.scale(image, (width, height)) scaled_frames.append(scaled_image) self.animations[animation] = scaled_frames def get_status(self): """Determine character's animation status based on movement.""" if self.is_damaged: self.status = 'damaged' else: if self.direction.y < 0: self.status = 'jump' elif self.direction.y > 0: self.status = 'fall' else: if self.direction.x != 0: self.status = 'walk' else: self.status = 'idle' def animate(self): """Update character animation based on status and frame index.""" if self.status == "damaged": self.animation_speed = 0.2 elif self.status == "idle": self.animation_speed = 0.05 elif self.status == "fall": self.animation_speed = 0.45 else: self.animation_speed = 0.15 animation = self.animations[self.status] self.frame_index += self.animation_speed if self.frame_index >= len(animation): if self.status == "damaged" and not self.is_damaged: self.frame_index = 0 self.get_status() else: self.frame_index = 0 image = animation[int(self.frame_index)] if self.facing_right: self.image = image else: self.image = pygame.transform.flip(image, True, False) old_midbottom = self.rect.midbottom self.rect = self.image.get_rect(midbottom=old_midbottom) def move(self, moving_left, moving_right): """Handle character movement, collisions, and screen scrolling.""" screen_scroll = 0 dx = 0 dy = 0 # Handle horizontal movement if moving_left: dx = -self.speed self.facing_right = False self.flip = True self.direction.x = -1 elif moving_right: dx = self.speed self.facing_right = True self.flip = False self.direction.x = 1 else: self.direction.x = 0 # Handle jumping if self.jump and not self.in_air: jump.play() self.vel_y = -13 self.in_air = True self.jump = False # Apply gravity self.vel_y += GRAVITY if self.vel_y > 10: self.vel_y = 10 dy += self.vel_y # Reset ground state self.on_ground = False # Handle X-axis collisions self.rect.x += dx for tile in world.obstacle_list: if tile[1].colliderect(self.rect): if dx > 0: self.rect.right = tile[1].left elif dx < 0: self.rect.left = tile[1].right if self.char_type == 'enemy': self.direction.x *= -1 self.move_counter = 0 # Handle Y-axis collisions self.rect.y += dy for tile in world.obstacle_list: if tile[1].colliderect(self.rect): if self.vel_y < 0: self.rect.top = tile[1].bottom self.vel_y = 0 elif self.vel_y >= 0: self.rect.bottom = tile[1].top self.vel_y = 0 self.in_air = False self.on_ground = True # Prevent falling through floor if self.rect.y > SCREEN_H: self.rect.bottom = SCREEN_H self.vel_y = 0 self.in_air = False self.on_ground = True self.health = 0 # Handle screen scrolling for player if self.char_type == 'Player': if self.rect.right > SCREEN_W - SCROLL_THRESH and bg_scroll < (world.level_length * TILE_SIZE) - SCREEN_W: screen_scroll = -dx self.rect.x -= dx elif self.rect.left < SCROLL_THRESH and bg_scroll > 0: screen_scroll = -dx self.rect.x -= dx # Handle coin collisions if self.char_type == "Player": collided_coins = pygame.sprite.spritecollide(self, coin_group, True) if collided_coins: global score coin_get.play() score += 1 # Handle water collisions if self.char_type == "Player": if pygame.sprite.spritecollide(self, water_group, False): self.health = 0 elif self.char_type == "Enemy": if pygame.sprite.spritecollide(self, water_group, False): self.kill() # Update direction for status if self.on_ground: self.direction.y = 0 elif self.vel_y > 0: self.direction.y = 1 elif self.vel_y < 0: self.direction.y = -1 self.get_status() # Align with tile grid when on ground if self.on_ground: self.rect.bottom = (self.rect.bottom // TILE_SIZE) * TILE_SIZE return screen_scroll def ai(self): """Control enemy AI behavior, including chasing player and dealing damage.""" if self.damage_cooldown > 0: self.damage_cooldown -= pygame.time.get_ticks() - self.update_time if self.damage_cooldown < 0: self.damage_cooldown = 0 if self.alive and Player.alive: # Handle idle behavior if self.idling == False and random.randint(1, 100) == 1: self.update_action(0) self.idling = True self.idling_counter = 50 # Detect player within vision if self.vision.colliderect(Player.rect): self.player_found = True self.update_action(1) self.speed = 4 # Chase player ai_moving_right = self.direction.x == 1 ai_moving_left = not ai_moving_right self.move(ai_moving_left, ai_moving_right) self.vision.center = (self.rect.centerx + 115 * self.direction.x, self.rect.centery) else: # Patrol when player not found self.speed = 3 self.player_found = False if self.idling == False: ai_moving_right = self.direction.x == 1 ai_moving_left = not ai_moving_right self.move(ai_moving_left, ai_moving_right) self.update_action(1) self.move_counter += 1 self.vision.center = (self.rect.centerx + 115 * self.direction.x, self.rect.centery) if self.move_counter > TILE_SIZE: self.direction.x *= -1 self.move_counter *= -1 else: self.idling_counter -= 1 if self.idling_counter <= 0: self.idling = False # Deal damage if touching player if self.rect.colliderect(Player.rect): if self.damage_cooldown <= 0: Player.take_damage(self.damage_amount) self.damage_cooldown = self.damage_cooldown_time if Player.health <= 0: self.check_alive() self.rect.x += screen_scroll self.update_time = pygame.time.get_ticks() def update_action(self, new_action): """Update character's current action and reset animation frame.""" if new_action != self.action: self.action = new_action self.frame_index = 0 self.update_time = pygame.time.get_ticks() def check_alive(self): """Check if character is alive and update status if dead.""" if self.health <= 0: self.health = 0 self.speed = 0 self.alive = False self.status = 'death' self.update_action(3) def draw(self): """Draw character sprite on screen.""" screen.blit(self.image, self.rect) def take_damage(self, amount): """Apply damage, trigger damage animation, and red flash effect.""" if self.alive and not self.is_damaged: self.health -= amount self.is_damaged = True self.damage_timer = self.damage_duration self.status = 'damaged' self.update_action(5) hurt.play() self.red_flash_active = True self.red_flash_timer = self.red_flash_duration def update(self): """Update character state, animations, and timers.""" if self.is_damaged: self.damage_timer -= pygame.time.get_ticks() - self.update_time if self.damage_timer <= 0: self.is_damaged = False if self.red_flash_active: self.red_flash_timer -= pygame.time.get_ticks() - self.update_time if self.red_flash_timer <= 0: self.red_flash_active = False self.animate() self.check_alive() self.update_time = pygame.time.get_ticks() # Enemy2 class (shooting enemy) class Enemy2(pygame.sprite.Sprite): def __init__(self, char_type, x, y, scale, speed): """Initialize shooting enemy with position, scale, and speed.""" pygame.sprite.Sprite.__init__(self) self.alive = True self.char_type = char_type self.speed = speed self.health = 100 self.max_health = self.health self.direction = pygame.math.Vector2(0, 0) self.vel_y = 0 self.jump = False self.flip = False self.action = 0 self.update_time = pygame.time.get_ticks() self.in_air = False self.on_ground = True self.scale = scale self.picked_up_key = False self.import_character_assets() # Animation variables self.frame_index = 0 self.animation_speed = 0.15 self.image = self.animations['idle'][self.frame_index] self.rect = self.image.get_rect() self.rect.midbottom = (x, y) self.width = self.image.get_width() * scale self.height = self.image.get_height() * scale # Status variables self.facing_right = True self.status = 'idle' # AI-specific variables self.move_counter = 0 self.vision = pygame.Rect(0, 0, 500, 500) self.idling = False self.idling_counter = 0 self.player_found = False # Shooting variables self.shoot_cooldown = 0 self.shoot_cooldown_time = 1500 self.projectile_speed = 0.5 self.last_shot_time = 0 def import_character_assets(self): """Load and scale Enemy2 animation frames.""" Path = f'Assets/{self.char_type}/' self.animations = {'idle': [], 'walk': [], 'jump': [], 'death': [], 'fall': [], 'damaged': []} for animation in self.animations.keys(): full_path = Path + animation loaded_frames = import_folder(full_path) scaled_frames = [] for image in loaded_frames: width = int(image.get_width() * self.scale) height = int(image.get_height() * self.scale) scaled_image = pygame.transform.scale(image, (width, height)) scaled_frames.append(scaled_image) self.animations[animation] = scaled_frames def get_status(self): """Determine Enemy2's animation status based on movement.""" if self.direction.y < 0: self.status = 'jump' elif self.direction.y > 0: self.status = 'fall' else: if self.direction.x != 0: self.status = 'walk' else: self.status = 'idle' def animate(self): """Update Enemy2 animation based on status.""" if self.status == "damaged": self.animation_speed = 0.2 elif self.status == "idle": self.animation_speed = 0.05 elif self.status == "fall": self.animation_speed = 0.45 else: self.animation_speed = 0.15 animation = self.animations[self.status] self.frame_index += self.animation_speed if self.frame_index >= len(animation): if self.status == "damaged": self.frame_index = 0 self.get_status() else: self.frame_index = 0 image = animation[int(self.frame_index)] if self.facing_right: self.image = image else: self.image = pygame.transform.flip(image, True, False) old_midbottom = self.rect.midbottom self.rect = self.image.get_rect(midbottom=old_midbottom) def move(self, moving_left, moving_right): """Handle Enemy2 movement and collisions.""" screen_scroll = 0 dx = 0 dy = 0 if moving_left: dx = -self.speed self.facing_right = False self.flip = True self.direction.x = -1 if moving_right: dx = self.speed self.facing_right = True self.flip = False self.direction.x = 1 if self.jump and not self.in_air: self.vel_y = -13 self.on_ground = False self.in_air = True self.jump = False self.direction.y = -1 dy += self.vel_y if self.in_air: self.vel_y += GRAVITY if self.vel_y > 10: self.vel_y = 10 dy += self.vel_y for tile in world.obstacle_list: if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 if self.char_type == 'enemy': self.direction.x *= -1 self.move_counter = 0 if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): if self.vel_y < 0: self.vel_y = 0 dy = tile[1].bottom - self.rect.top elif self.vel_y >= 0: self.vel_y = 0 self.in_air = False self.on_ground = True dy = tile[1].top - self.rect.bottom if self.rect.bottom > SCREEN_H: self.health = 0 self.rect.x += dx self.rect.y += dy if self.on_ground: self.direction.y = 0 elif self.vel_y > 0: self.direction.y = 1 elif self.vel_y < 0: self.direction.y = -1 if not moving_left and not moving_right: self.direction.x = 0 self.get_status() return screen_scroll def ai(self): """Control Enemy2 AI, including shooting at player.""" if self.alive and Player.alive: if self.idling == False and random.randint(1, 1000) == 1: self.update_action(0) self.idling = True self.idling_counter = random.randint(20, 100) if self.vision.colliderect(Player.rect): self.player_found = True self.update_action(1) current_time = pygame.time.get_ticks() if current_time - self.last_shot_time >= self.shoot_cooldown_time: player_pos = pygame.math.Vector2(Player.rect.center) enemy_pos = pygame.math.Vector2(self.rect.center) direction_to_player = (player_pos - enemy_pos).normalize() projectile = Projectile(self.rect.centerx, self.rect.centery, direction_to_player) Projectile_group.add(projectile) self.last_shot_time = current_time else: self.player_found = False if self.idling == False: ai_moving_right = self.direction.x == 1 ai_moving_left = not ai_moving_right self.move(ai_moving_left, ai_moving_right) self.update_action(1) self.move_counter += 1 self.vision.center = (self.rect.centerx + 265 * self.direction.x, self.rect.centery) if self.move_counter > TILE_SIZE: self.direction.x *= -1 self.move_counter *= -1 else: self.idling_counter -= 1 if self.idling_counter <= 0: self.idling = False self.rect.x += screen_scroll self.update_time = pygame.time.get_ticks() def update_action(self, new_action): """Update Enemy2's current action and reset animation frame.""" if new_action != self.action: self.action = new_action self.frame_index = 0 self.update_time = pygame.time.get_ticks() def check_alive(self): """Check if Enemy2 is alive and update status if dead.""" if self.health <= 0: self.health = 0 self.speed = 0 self.alive = False self.status = 'death' self.update_action(3) def draw(self): """Draw Enemy2 sprite on screen.""" screen.blit(self.image, self.rect) def update(self): """Update Enemy2 state, animations, and shooting cooldown.""" self.animate() self.check_alive() if self.shoot_cooldown > 0: self.shoot_cooldown -= pygame.time.get_ticks() - self.update_time if self.shoot_cooldown < 0: self.shoot_cooldown = 0 self.update_time = pygame.time.get_ticks() # HealthBar class for player class HealthBar: def __init__(self, x, y, health, max_health): """Initialize health bar with position, health, and max health.""" self.x = x self.y = y self.health = health self.max_health = max_health def draw(self, health): """Draw health bar with updated health value.""" self.health = health ratio = self.health / self.max_health border_width = int(154 * 1.5) border_height = int(24 * 1.5) bar_width = int(150 * 1.5) bar_height = int(20 * 1.5) border_offset = int(2 * 1.5) pygame.draw.rect(screen, BLACK, (self.x - border_offset, self.y - border_offset, border_width, border_height)) pygame.draw.rect(screen, RED, (self.x, self.y, bar_width, bar_height)) pygame.draw.rect(screen, GREEN, (self.x, self.y, bar_width * ratio, bar_height)) # World class for level data class World: def __init__(self): """Initialize world with empty obstacle list.""" self.obstacle_list = [] def process_data(self, data): """Process level data to create tiles, characters, and items.""" self.level_length = len(data[0]) Player = None health_bar = None for y, row in enumerate(data): for x, tile in enumerate(row): if tile >= 0: img = img_list[tile] img_rect = img.get_rect() img_rect.x = x * TILE_SIZE img_rect.y = y * TILE_SIZE tile_data = (img, img_rect) if tile >= 0 and tile <= 8: self.obstacle_list.append(tile_data) elif tile >= 9 and tile <= 10: water = Water(img, x * TILE_SIZE, y * TILE_SIZE) water_group.add(water) elif tile >= 11 and tile <= 13: decoration = Decoration(img, x * TILE_SIZE, y * TILE_SIZE) decoration_group.add(decoration) elif tile == 14: Player = Character('Player', x * TILE_SIZE, y * TILE_SIZE + TILE_SIZE, 1.3, 4) health_bar = HealthBar(15, 15, Player.health, Player.max_health) elif tile == 15: enemy = Character('Enemy', x * TILE_SIZE, y * TILE_SIZE + TILE_SIZE, 1.3, 3) enemy_group.add(enemy) elif tile == 16: exit = Exit(img, x * TILE_SIZE, y * TILE_SIZE) exit_group.add(exit) elif tile == 17: enemy = Enemy2('Enemy2', x * TILE_SIZE, y * TILE_SIZE + TILE_SIZE, 0.9, 1) enemy_group.add(enemy) elif tile == 18: key = Key(img, x * TILE_SIZE, y * TILE_SIZE) key_group.add(key) elif tile == 19: coin = Coin(img, x * TILE_SIZE, y * TILE_SIZE) coin_group.add(coin) elif tile == 20: sign = Sign(img, x * TILE_SIZE, y * TILE_SIZE) sign_group.add(sign) return Player, health_bar def draw(self): """Draw world tiles with screen scroll offset.""" for tile in self.obstacle_list: tile[1][0] += screen_scroll screen.blit(tile[0], tile[1]) # Key class for collectible keys class Key(pygame.sprite.Sprite): def __init__(self, img, x, y): """Initialize key sprite with position and bobbing animation.""" pygame.sprite.Sprite.__init__(self) scale_factor = 0.8 new_width = int(img.get_width() * scale_factor * 0.6) new_height = int(img.get_height() * scale_factor) self.image = pygame.transform.scale(img, (new_width, new_height)) self.rect = self.image.get_rect() self.rect.midtop = (x + TILE_SIZE // 2, y + (TILE_SIZE - self.image.get_height())) self.base_y = self.rect.y self.bob_timer = 0 self.bob_amplitude = 5 self.bob_speed = 0.06 def update(self): """Update key position with bobbing animation and screen scroll.""" self.bob_timer += self.bob_speed y_offset = math.sin(self.bob_timer) * self.bob_amplitude self.rect.y = self.base_y + y_offset self.rect.x += screen_scroll # Decoration class for level aesthetics class Decoration(pygame.sprite.Sprite): def __init__(self, img, x, y): """Initialize decoration sprite with position.""" pygame.sprite.Sprite.__init__(self) self.image = img self.rect = self.image.get_rect() self.rect.midtop = (x + TILE_SIZE // 2, y + (TILE_SIZE - self.image.get_height())) def update(self): """Update decoration position with screen scroll.""" self.rect.x += screen_scroll # Water class for hazardous areas class Water(pygame.sprite.Sprite): def __init__(self, img, x, y): """Initialize water sprite with position.""" pygame.sprite.Sprite.__init__(self) self.image = img self.rect = self.image.get_rect() self.rect.midtop = (x + TILE_SIZE // 2, y + (TILE_SIZE - self.image.get_height())) def update(self): """Update water position with screen scroll.""" self.rect.x += screen_scroll # Exit class for level completion class Exit(pygame.sprite.Sprite): def __init__(self, img, x, y): """Initialize exit sprite with position.""" pygame.sprite.Sprite.__init__(self) self.image = img self.rect = self.image.get_rect() self.rect.midtop = (x + TILE_SIZE // 2, y + (TILE_SIZE - self.image.get_height())) def update(self): """Update exit position with screen scroll.""" self.rect.x += screen_scroll # Sign class for information signs class Sign(pygame.sprite.Sprite): def __init__(self, img, x, y): """Initialize sign sprite with position.""" pygame.sprite.Sprite.__init__(self) self.image = img self.rect = self.image.get_rect() self.rect.midtop = (x + TILE_SIZE // 2, y + (TILE_SIZE - self.image.get_height())) def update(self): """Update sign position with screen scroll.""" self.rect.x += screen_scroll # Coin class for collectible coins class Coin(pygame.sprite.Sprite): def __init__(self, img, x, y): """Initialize coin sprite with position and bobbing animation.""" pygame.sprite.Sprite.__init__(self) scale_factor = 0.6 new_width = int(img.get_width() * scale_factor) new_height = int(img.get_height() * scale_factor) self.image = pygame.transform.scale(img, (new_width, new_height)) self.rect = self.image.get_rect() self.rect.midtop = (x + TILE_SIZE // 2, y + (TILE_SIZE - self.image.get_height())) self.base_y = self.rect.y self.bob_timer = 0 self.bob_amplitude = 5 self.bob_speed = 0.08 def update(self): """Update coin position with bobbing animation and screen scroll.""" self.bob_timer += self.bob_speed y_offset = math.sin(self.bob_timer) * self.bob_amplitude self.rect.y = self.base_y + y_offset self.rect.x += screen_scroll # Projectile class for Enemy2 attacks class Projectile(pygame.sprite.Sprite): def __init__(self, x, y, direction): """Initialize projectile with position, direction, and rotation.""" pygame.sprite.Sprite.__init__(self) self.speed = 5 self.original_image = Projectile_img self.direction = direction angle = math.degrees(math.atan2(-direction.y, direction.x)) + 90 self.image = pygame.transform.rotate(self.original_image, angle) self.rect = self.image.get_rect() self.rect.center = (x, y) shoot.play() def update(self): """Update projectile position and handle collisions.""" self.rect.x += (self.direction.x * self.speed) + screen_scroll self.rect.y += (self.direction.y * self.speed) if self.rect.right < 0 or self.rect.left > SCREEN_W or self.rect.bottom < 0 or self.rect.top > SCREEN_H: self.kill() for tile in world.obstacle_list: if tile[1].colliderect(self.rect): self.kill() if pygame.sprite.spritecollide(Player, Projectile_group, False): if Player.alive: Player.take_damage(10) self.kill() # Screen fade effect class class ScreenFade: def __init__(self, direction, colour, speed): """Initialize screen fade with direction, color, and speed.""" self.direction = direction self.colour = colour self.speed = speed self.fade_counter = 0 def fade(self): """Apply fade effect and return completion status.""" fade_complete = False self.fade_counter += self.speed if self.direction == 1: pygame.draw.rect(screen, self.colour, (0 - self.fade_counter, 0, SCREEN_W // 2, SCREEN_H)) pygame.draw.rect(screen, self.colour, (SCREEN_W // 2 + self.fade_counter, 0, SCREEN_W, SCREEN_H)) pygame.draw.rect(screen, self.colour, (0, 0 - self.fade_counter, SCREEN_W, SCREEN_H // 2)) pygame.draw.rect(screen, self.colour, (0, SCREEN_H // 2 + self.fade_counter, SCREEN_W, SCREEN_H)) if self.direction == 2: pygame.draw.rect(screen, self.colour, (0, 0, SCREEN_W, 0 + self.fade_counter)) if self.fade_counter >= SCREEN_W: fade_complete = True return fade_complete # Create screen fade instances intro_fade = ScreenFade(1, BLACK, 8) death_fade = ScreenFade(2, PINK, 12) # Create button instances start_button = button.Button(SCREEN_W // 2 - 75, SCREEN_H // 2 - 50, start_img, 1) exit_button = button.Button(SCREEN_W // 2 - 75, SCREEN_H // 2 + 50, exit_img, 1) restart_button = button.Button(SCREEN_W // 2 - 75, SCREEN_H // 2 - 50, restart_img, 1) # Create sprite groups enemy_group = pygame.sprite.Group() Projectile_group = pygame.sprite.Group() grenade_group = pygame.sprite.Group() explosion_group = pygame.sprite.Group() item_box_group = pygame.sprite.Group() decoration_group = pygame.sprite.Group() water_group = pygame.sprite.Group() exit_group = pygame.sprite.Group() key_group = pygame.sprite.Group() coin_group = pygame.sprite.Group() sign_group = pygame.sprite.Group() # Initialize level data world_data = [] for row in range(ROWS): r = [-1] * COLS world_data.append(r) # Load level data from CSV with open(f'level{level}_data.csv', newline='') as csvfile: reader = csv.reader(csvfile, delimiter=',') for x, row in enumerate(reader): for y, tile in enumerate(row): world_data[x][y] = int(tile) # Create world and initialize player and health bar world = World() Player, health_bar = world.process_data(world_data) # Load initial high score high_score = load_score() # Main game loop run = True while run: clock.tick(FPS) if not start_game: # Display main menu screen.blit(bg_img, (0, 0)) if start_button.draw(screen): start_game = True start_intro = True if exit_button.draw(screen): run = False else: # Draw game elements draw_bg() health_bar.draw(Player.health) world.draw() Player.update() Player.draw() # Update and draw enemies for enemy in enemy_group: enemy.ai() enemy.update() enemy.draw() # Update and draw sprite groups game_setup() # Display game information draw_text(f"Level: {level}", font, WHITE, 15, 50) draw_text(f"High Score: {high_score}", font, WHITE, 15, 80) draw_text(f"Score: {score}", font, WHITE, 15, 110) # Handle intro fade if start_intro: if intro_fade.fade(): start_intro = False intro_fade.fade_counter = 0 # Update player actions if Player.alive: if Player.in_air: Player.update_action(2) elif moving_left or moving_right: Player.update_action(1) else: Player.update_action(0) screen_scroll = Player.move(moving_left, moving_right) bg_scroll -= screen_scroll # Update high score if score > high_score: high_score = score save_score(high_score) # Handle level completion if level_complete: start_intro = True level += 1 bg_scroll = 0 world_data = reset_level() level_complete = False if level <= MAX_LEVELS: with open(f'level{level}_data.csv', newline='') as csvfile: reader = csv.reader(csvfile, delimiter=',') for x, row in enumerate(reader): for y, tile in enumerate(row): world_data[x][y] = int(tile) world = World() Player, health_bar = world.process_data(world_data) else: print("YOU FINISHED THE GAME") run = False # Handle key interactions collided_keys = pygame.sprite.spritecollide(Player, key_group, False) if collided_keys: key = collided_keys[0] interact_x = key.rect.centerx - interact_img.get_width() // 2 interact_y = key.rect.top - interact_img.get_height() - 10 screen.blit(interact_img, (interact_x, interact_y)) # Handle exit interactions collide_exit = pygame.sprite.spritecollide(Player, exit_group, False) if collide_exit and Player.picked_up_key: exit = collide_exit[0] interact_x = exit.rect.centerx - interact_img.get_width() // 2 interact_y = exit.rect.top - interact_img.get_height() - 10 screen.blit(interact_img, (interact_x, interact_y)) else: # Handle player death screen_scroll = 0 if death_fade.fade(): if restart_button.draw(screen): death_fade.fade_counter = 0 start_intro = True bg_scroll = 0 score = 0 world_data = reset_level() with open(f'level0_data.csv', newline='') as csvfile: reader = csv.reader(csvfile, delimiter=',') for x, row in enumerate(reader): for y, tile in enumerate(row): world_data[x][y] = int(tile) world = World() level = 0 Player, health_bar = world.process_data(world_data) # Apply red flash effect if Player.red_flash_active: red_surface = pygame.Surface((SCREEN_W, SCREEN_H), pygame.SRCALPHA) red_surface.fill((255, 0, 0)) red_surface.set_alpha(155) screen.blit(red_surface, (0, 0)) # Handle Pygame events for event in pygame.event.get(): if event.type == pygame.QUIT: run = False if event.type == pygame.KEYDOWN: if event.key == pygame.K_a: moving_left = True if event.key == pygame.K_d: moving_right = True if event.key == pygame.K_SPACE and Player.alive: Player.jump = True if event.key == pygame.K_ESCAPE: run = False if event.key == pygame.K_e and Player.alive: collided_keys = pygame.sprite.spritecollide(Player, key_group, True) if collided_keys: Player.picked_up_key = True key_get.play() if Player.picked_up_key: collide_exit = pygame.sprite.spritecollide(Player, exit_group, True) if collide_exit: level_complete = True if event.type == pygame.KEYUP: if event.key == pygame.K_a: moving_left = False if event.key == pygame.K_d: moving_right = False pygame.display.update() # Quit Pygame pygame.quit()