import pygame from pygame import mixer import os import random import csv import button import time mixer.init() pygame.init() SCREEN_WIDTH = 800 SCREEN_HEIGHT = int(SCREEN_WIDTH * 0.8) screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption('Shooter') #set framerate clock = pygame.time.Clock() FPS = 60 #define game variables GRAVITY = 0.75 SCROLL_THRESH = 200 ROWS = 16 COLS = 150 TILE_SIZE = SCREEN_HEIGHT // ROWS TILE_TYPES = 25 MAX_LEVELS = 3 screen_scroll = 0 bg_scroll = 0 level = 1 start_game = False start_intro = False #define player action variables moving_left = False moving_right = False shoot = False # Timer variables timer_start = None current_time = 0 best_times = {1: None, 2: None, 3: None} #load music and sounds pygame.mixer.music.load('audio/music2.mp3') pygame.mixer.music.set_volume(0.3) pygame.mixer.music.play(-1, 0.0, 5000) jump_fx = pygame.mixer.Sound('audio/jump.wav') jump_fx.set_volume(0.05) # Define screen dimensions as constants SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 # Load images start_img = pygame.image.load('PLATFORMERS/img/start_btn.png').convert_alpha() exit_img = pygame.image.load('PLATFORMERS/img/exit_btn.png').convert_alpha() restart_img = pygame.image.load('PLATFORMERS/img/restart_btn.png').convert_alpha() mountain_img = pygame.image.load('PLATFORMERS/img/Background/middleground.png').convert_alpha() sky_img = pygame.image.load('PLATFORMERS/img/Background/background.png').convert_alpha() # Adjusted heights new_mountain_height = 700 new_sky_height = 600 mountain_img = pygame.transform.scale(mountain_img, (int((new_mountain_height / mountain_img.get_height()) * mountain_img.get_width()), new_mountain_height)) sky_img = pygame.transform.scale(sky_img, (int((new_sky_height / sky_img.get_height()) * sky_img.get_width()), new_sky_height)) # Calculate the Y position adjustment for both backgrounds mountain_y_adjustment = -100 # Adjust this value as needed sky_y_adjustment = 50 # Adjust this value as needed # Update the Y positions of the background images mountain_y = SCREEN_HEIGHT - new_mountain_height + mountain_y_adjustment sky_y = SCREEN_HEIGHT - new_sky_height + sky_y_adjustment # Draw the background images at their updated positions screen.blit(sky_img, (0, sky_y)) screen.blit(mountain_img, (0, mountain_y)) #store tiles in a list img_list = [] for x in range(TILE_TYPES): img = pygame.image.load(f'img/Tile/{x}.png') img = pygame.transform.scale(img, (TILE_SIZE, TILE_SIZE)) img_list.append(img) #define colours BG = (144, 201, 120) RED = (255, 0, 0) WHITE = (255, 255, 255) GREEN = (0, 255, 0) BLACK = (0, 0, 0) PINK = (235, 65, 54) #define font font = pygame.font.SysFont('Futura', 30) def draw_timer(): global current_time, timer_start if timer_start is not None: current_time = pygame.time.get_ticks() - timer_start minutes = current_time // 60000 seconds = (current_time % 60000) // 1000 milliseconds = (current_time % 1000) // 10 timer_text = f'{minutes:02}:{seconds:02}:{milliseconds:02}' draw_text(timer_text, font, WHITE, SCREEN_WIDTH // 2 - 50, 10) if best_times[level] is not None: best_minutes = best_times[level] // 60000 best_seconds = (best_times[level] % 60000) // 1000 best_milliseconds = (best_times[level] % 1000) // 10 best_time_text = f'Best: {best_minutes:02}:{best_seconds:02}:{best_milliseconds:02}' draw_text(best_time_text, font, WHITE, SCREEN_WIDTH // 2 - 50, 40) def save_best_times(): with open('best_times.txt', 'w') as file: for level, time in best_times.items(): if time is not None: minutes = time // 60000 seconds = (time % 60000) // 1000 milliseconds = (time % 1000) // 10 time_str = f'{level}:{minutes:02}:{seconds:02}:{milliseconds:02}\n' file.write(time_str) def draw_text(text, font, text_col, x, y): img = font.render(text, True, text_col) screen.blit(img, (x, y)) def crop_image(image): # Get the bounding box of the non-transparent part of the image rect = image.get_bounding_rect() # Crop the image to the bounding box cropped_image = image.subsurface(rect).copy() return cropped_image def scale_image(image, scale): # Get the size of the image width, height = image.get_size() # Calculate new size new_width = int(width * scale) new_height = int(height * scale) # Scale the image scaled_image = pygame.transform.scale(image, (new_width, new_height)) return scaled_image def draw_bg(): screen.fill(BG) width = sky_img.get_width() for x in range(8): screen.blit(sky_img, ((x * width) - bg_scroll * 0.5, 0)) screen.blit(mountain_img, ((x * width) - bg_scroll * 0.6, SCREEN_HEIGHT - mountain_img.get_height() + 100)) #function to reset level def reset_level(): enemy_group.empty() decoration_group.empty() water_group.empty() exit_group.empty() #create empty tile list data = [] for row in range(ROWS): r = [-1] * COLS data.append(r) return data class Soldier(pygame.sprite.Sprite): def __init__(self, char_type, x, y, scale, 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 = 1 self.vel_y = 0 self.jump = False self.in_air = True self.flip = False self.animation_list = [] self.frame_index = 0 self.action = 0 self.update_time = pygame.time.get_ticks() self.shoot_cooldown = 0 # ai specific variables self.move_counter = 0 self.vision = pygame.Rect(0, 0, 600, 20) self.idling = False self.idling_counter = 0 self.change_dir_cooldown = 0 # Cooldown for changing direction self.attack_cooldown = 0 # Cooldown for melee attack # load all images for the players animation_types = ['Idle', 'Run', 'Jump', 'Death'] for animation in animation_types: temp_list = [] num_of_frames = len(os.listdir(f'PLATFORMERS/img/{self.char_type}/{animation}')) for i in range(num_of_frames): img = pygame.image.load(f'PLATFORMERS/img/{self.char_type}/{animation}/{i}.png').convert_alpha() img = crop_image(img) # Remove blank space img = scale_image(img, scale) # Scale image temp_list.append(img) self.animation_list.append(temp_list) self.image = self.animation_list[self.action][self.frame_index] self.rect = self.image.get_rect() self.rect.center = (x, y) self.width = self.image.get_width() self.height = self.image.get_height() def update(self): self.update_animation() self.check_alive() # Update cooldowns if self.shoot_cooldown > 0: self.shoot_cooldown -= 1 if self.attack_cooldown > 0: self.attack_cooldown -= 1 if self.change_dir_cooldown > 0: self.change_dir_cooldown -= 1 def move(self, moving_left, moving_right): global timer_start # reset movement variables screen_scroll = 0 dx = 0 dy = 0 # Check if the player is on the ground if self.char_type == 'player' and not self.in_air: # Increase speed if the player is on the ground self.speed = 8 # Adjust this value as needed # assign movement variables if moving left or right if moving_left or moving_right: if timer_start is None: timer_start = pygame.time.get_ticks() if moving_left: dx = -self.speed self.flip = True self.direction = -1 if moving_right: dx = self.speed self.flip = False self.direction = 1 # jump if self.jump and not self.in_air: self.vel_y = -13 self.jump = False self.in_air = True # apply gravity self.vel_y += GRAVITY if self.vel_y > 10: self.vel_y = 10 dy += self.vel_y # check for collision for tile in world.obstacle_list: # check collision in the x direction if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 # if the AI has hit a wall then make it turn around if self.char_type == 'enemy' and self.change_dir_cooldown == 0: self.direction *= -1 self.move_counter = 0 self.change_dir_cooldown = 50 # Cooldown period after changing direction # 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, i.e. jumping if self.vel_y < 0: self.vel_y = 0 dy = tile[1].bottom - self.rect.top # check if above the ground, i.e. falling elif self.vel_y >= 0: self.vel_y = 0 self.in_air = False dy = tile[1].top - self.rect.bottom # check for collision with water if pygame.sprite.spritecollide(self, water_group, False): self.health = 0 # check for collision with exit level_complete = False if pygame.sprite.spritecollide(self, exit_group, False): level_complete = True # check if fallen off the map margin = 5 if self.rect.bottom > SCREEN_HEIGHT + margin: self.health = 0 # check if going off the edges of the screen if self.char_type == 'player': if self.rect.left + dx < 0 or self.rect.right + dx > SCREEN_WIDTH: dx = 0 # update rectangle position self.rect.x += dx self.rect.y += dy # update scroll based on player position if self.char_type == 'player': if (self.rect.right > SCREEN_WIDTH - SCROLL_THRESH and bg_scroll < (world.level_length * TILE_SIZE) - SCREEN_WIDTH)\ or (self.rect.left < SCROLL_THRESH and bg_scroll > abs(dx)): self.rect.x -= dx screen_scroll = -dx return screen_scroll, level_complete def ai(self): if self.alive and player.alive: if self.idling == False and random.randint(1, 200) == 1: self.update_action(0) # 0: idle self.idling = True self.idling_counter = 50 # Check if the AI is near the player if self.vision.colliderect(player.rect): # Move towards the player if player.rect.x > self.rect.x: self.move(False, True) # Move right else: self.move(True, False) # Move left self.update_action(1) # 1: run self.vision.center = (self.rect.centerx + 75 * self.direction, self.rect.centery) # Perform melee attack if close enough self.melee_attack() else: if self.idling == False: if self.direction == 1: ai_moving_right = True else: ai_moving_right = False ai_moving_left = not ai_moving_right self.move(ai_moving_left, ai_moving_right) self.update_action(1) # 1: run self.move_counter += 1 # Update AI vision as the enemy moves self.vision.center = (self.rect.centerx + 75 * self.direction, self.rect.centery) if self.move_counter > TILE_SIZE: self.direction *= -1 self.move_counter = 0 self.change_dir_cooldown = 50 # Cooldown period after changing direction else: self.idling_counter -= 1 if self.idling_counter <= 0: self.idling = False # Scroll self.rect.x += screen_scroll def melee_attack(self): if self.attack_cooldown == 0: if self.rect.colliderect(player.rect): player.health -= 10 self.attack_cooldown = 50 # Cooldown for the next attack def update_animation(self): # Update animation ANIMATION_COOLDOWN = 100 # Update image depending on current frame self.image = self.animation_list[self.action][self.frame_index] # Check if enough time has passed since the last update if pygame.time.get_ticks() - self.update_time > ANIMATION_COOLDOWN: self.update_time = pygame.time.get_ticks() self.frame_index += 1 # If the animation has run out, then reset back to the start if self.frame_index >= len(self.animation_list[self.action]): if self.action == 3: self.frame_index = len(self.animation_list[self.action]) - 1 else: self.frame_index = 0 def update_action(self, new_action): # Check if the new action is different from the previous one if new_action != self.action: self.action = new_action # Update the animation settings self.frame_index = 0 self.update_time = pygame.time.get_ticks() def check_alive(self): if self.health <= 0: self.health = 0 self.speed = 0 self.alive = False self.update_action(3) def draw(self): screen.blit(pygame.transform.flip(self.image, self.flip, False), self.rect) class World(): def __init__(self): self.obstacle_list = [] def process_data(self, data): self.level_length = len(data[0]) #iterate through each value in level data file 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 <= 15: self.obstacle_list.append(tile_data) elif tile >= 16 and tile <= 18: decoration = Decoration(img, x * TILE_SIZE, y * TILE_SIZE) decoration_group.add(decoration) elif tile == 20: # create player player = Soldier('player', x * TILE_SIZE, y * TILE_SIZE, 2.5, 5) # Increase scale from 1.65 to 1.85 health_bar = HealthBar(10, 10, player.health, player.health) elif tile == 21: # create enemies enemy = Soldier('enemy', x * TILE_SIZE, y * TILE_SIZE, 2, 2) enemy_group.add(enemy) elif tile == 22: # create exit exit = Exit(img, x * TILE_SIZE, y * TILE_SIZE) exit_group.add(exit) elif tile >= 23 and tile <= 24: water = Water(img, x * TILE_SIZE, y * TILE_SIZE) water_group.add(water) return player, health_bar def draw(self): for tile in self.obstacle_list: tile[1][0] += screen_scroll screen.blit(tile[0], tile[1]) class Decoration(pygame.sprite.Sprite): def __init__(self, img, x, y): 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): self.rect.x += screen_scroll class Water(pygame.sprite.Sprite): def __init__(self, img, x, y): 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): self.rect.x += screen_scroll class Exit(pygame.sprite.Sprite): def __init__(self, img, x, y): 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): self.rect.x += screen_scroll class HealthBar(): def __init__(self, x, y, health, max_health): self.x = x self.y = y self.health = health self.max_health = max_health def draw(self, health): #update with new health self.health = health #calculate health ratio ratio = self.health / self.max_health pygame.draw.rect(screen, BLACK, (self.x - 2, self.y - 2, 154, 24)) pygame.draw.rect(screen, RED, (self.x, self.y, 150, 20)) pygame.draw.rect(screen, GREEN, (self.x, self.y, 150 * ratio, 20)) def update(self): #check for collision with level for tile in world.obstacle_list: if tile[1].colliderect(self.rect): self.kill() class ScreenFade(): def __init__(self, direction, colour, speed): self.direction = direction self.colour = colour self.speed = speed self.fade_counter = 0 def fade(self): fade_complete = False self.fade_counter += self.speed if self.direction == 1:#whole screen fade pygame.draw.rect(screen, self.colour, (0 - self.fade_counter, 0, SCREEN_WIDTH // 2, SCREEN_HEIGHT)) pygame.draw.rect(screen, self.colour, (SCREEN_WIDTH // 2 + self.fade_counter, 0, SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.draw.rect(screen, self.colour, (0, 0 - self.fade_counter, SCREEN_WIDTH, SCREEN_HEIGHT // 2)) pygame.draw.rect(screen, self.colour, (0, SCREEN_HEIGHT // 2 +self.fade_counter, SCREEN_WIDTH, SCREEN_HEIGHT)) if self.direction == 2:#vertical screen fade down pygame.draw.rect(screen, self.colour, (0, 0, SCREEN_WIDTH, 0 + self.fade_counter)) if self.fade_counter >= SCREEN_WIDTH: fade_complete = True return fade_complete #create screen fades intro_fade = ScreenFade(1, BLACK, 4) death_fade = ScreenFade(2, PINK, 4) #create buttons start_button = button.Button(SCREEN_WIDTH // 2 - 130, SCREEN_HEIGHT // 2 - 150, start_img, 1) exit_button = button.Button(SCREEN_WIDTH // 2 - 110, SCREEN_HEIGHT // 2 + 50, exit_img, 1) restart_button = button.Button(SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2 - 50, restart_img, 2) #create sprite groups enemy_group = pygame.sprite.Group() decoration_group = pygame.sprite.Group() water_group = pygame.sprite.Group() exit_group = pygame.sprite.Group() #create empty tile list world_data = [] for row in range(ROWS): r = [-1] * COLS world_data.append(r) #load in level data and create world with open(f'PLATFORMERS/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) run = True while run: clock.tick(FPS) if start_game == False: #draw menu screen.fill(BG) #add buttons if start_button.draw(screen): start_game = True start_intro = True if exit_button.draw(screen): run = False else: #update background draw_bg() #draw world map world.draw() #show player health health_bar.draw(player.health) # Draw timer draw_timer() player.update() player.draw() for enemy in enemy_group: enemy.ai() enemy.update() enemy.draw() #update and draw groups decoration_group.update() water_group.update() exit_group.update() decoration_group.draw(screen) water_group.draw(screen) exit_group.draw(screen) #show intro if start_intro == True: 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)#2: jump elif moving_left or moving_right: player.update_action(1)#1: run else: player.update_action(0)#0: idle screen_scroll, level_complete = player.move(moving_left, moving_right) bg_scroll -= screen_scroll #check if player has completed the level if level_complete: # Update best time if best_times[level] is None or current_time < best_times[level]: best_times[level] = current_time save_best_times() start_intro = True level += 1 bg_scroll = 0 world_data = reset_level() if level <= MAX_LEVELS: #load in level data and create world with open(f'PLATFORMERS/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) # Reset timer timer_start = None current_time = 0 else: screen_scroll = 0 if death_fade.fade(): if restart_button.draw(screen): death_fade.fade_counter = 0 start_intro = True bg_scroll = 0 world_data = reset_level() #load in level data and create world with open(f'PLATFORMERS/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) # Reset timer timer_start = None current_time = 0 for event in pygame.event.get(): #quit game if event.type == pygame.QUIT: run = False #keyboard presses 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_w and player.alive: player.jump = True jump_fx.play() if event.key == pygame.K_ESCAPE: run = False #keyboard button released 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() pygame.quit()