import pygame from pygame import mixer import os import random import csv import button import time import json def level(screen, level_end, level_num): mixer.init() pygame.init() # Set up screen SCREEN_WIDTH = 800 SCREEN_HEIGHT = 800 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption('Shooter') clock = pygame.time.Clock() FPS = 60 # Game constants GRAVITY = 0.75 #Gravity strength SCROLL_THRESH = 200 ROWS = 16 COLS = 150 TILE_SIZE = SCREEN_HEIGHT // ROWS TILE_TYPES = 21 # Camera and background start positions screen_scroll = 0 bg_scroll = 0 start_game = True start_intro = False # Score system set up start_time = time.time() current_score = 0 # Check for high score try: with open(f'hs_{level_num}.json', 'r') as f: high_score = json.load(f) if isinstance(high_score, dict): high_score = high_score.get('high_score', 0) except (FileNotFoundError, json.JSONDecodeError): high_score = 100 # If there is no high score set to 100 # Movement & action variables moving_left = False moving_right = False shoot = False # Import sound effects jump_fx = pygame.mixer.Sound('audio/jump.wav') jump_fx.set_volume(0.05) shot_fx = pygame.mixer.Sound('audio/shot.wav') shot_fx.set_volume(0.05) grenade_fx = pygame.mixer.Sound('audio/grenade.wav') grenade_fx.set_volume(0.05) # Button images restart_img = pygame.image.load('img/restart_btn.png').convert_alpha() exit_img = pygame.image.load('img/exit_btn.png').convert_alpha() resume_img = pygame.image.load('img/resume_btn.png').convert_alpha() # Background images pine1_img = pygame.image.load('img/Background/pine1.png').convert_alpha() pine2_img = pygame.image.load('img/Background/pine2.png').convert_alpha() mountain_img = pygame.image.load('img/Background/mountain.png').convert_alpha() sky_img = pygame.image.load('img/Background/sky_cloud.png').convert_alpha() # Import tile type images 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) bullet_img = pygame.image.load('img/icons/bullet.png').convert_alpha() coins = pygame.image.load('img/Tile/11.png') item_boxes = { 'Collectable': coins } # Color constants BG = (144, 201, 120) RED = (255, 0, 0) WHITE = (255, 255, 255) GREEN = (0, 255, 0) BLACK = (0, 0, 0) PINK = (235, 65, 54) # Font for text font = pygame.font.SysFont('Futura', 30) # Functions & Classes def draw_text(text, font, text_col, x, y): img = font.render(text, True, text_col) screen.blit(img, (x, y)) def draw_bg(): screen.fill(BG) width = sky_img.get_width() for x in range(5): 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() - 300)) screen.blit(pine1_img, ((x * width) - bg_scroll * 0.7, SCREEN_HEIGHT - pine1_img.get_height() - 150)) screen.blit(pine2_img, ((x * width) - bg_scroll * 0.8, SCREEN_HEIGHT - pine2_img.get_height())) data = [] for row in range(ROWS): r = [-1] * COLS data.append(r) class Soldier(pygame.sprite.Sprite): def __init__(self, char_type, x, y, scale, speed, ammo, grenades): pygame.sprite.Sprite.__init__(self) self.alive = True self.char_type = char_type self.speed = speed self.ammo = ammo self.start_ammo = ammo self.shoot_cooldown = 0 self.grenades = grenades 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.move_counter = 0 self.vision = pygame.Rect(0, 0, 400, 20) self.idling = False self.idling_counter = 0 self.coins_collected = 0 animation_types = ['Idle', 'Run', 'Jump', 'Death'] for animation in animation_types: temp_list = [] num_of_frames = len(os.listdir(f'img/{self.char_type}/{animation}')) for i in range(num_of_frames): img = pygame.image.load(f'img/{self.char_type}/{animation}/{i}.png').convert_alpha() img = pygame.transform.scale(img, (int(img.get_width() * scale), int(img.get_height() * scale))) 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() if self.shoot_cooldown > 0: self.shoot_cooldown -= 1 def move(self, moving_left, moving_right): screen_scroll = 0 dx = 0 dy = 0 if moving_left: dx = -self.speed self.flip = True self.direction = -1 if moving_right: dx = self.speed self.flip = False self.direction = 1 # Ensure player is considered in the air if they have vertical velocity if self.vel_y > 0: self.in_air = True # Only allow jump if player is not in the air if self.jump and not self.in_air: self.vel_y = -11 self.jump = False self.in_air = True self.vel_y += GRAVITY 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.move_counter += 1 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 dy = tile[1].top - self.rect.bottom if pygame.sprite.spritecollide(self, water_group, False): self.health = 0 level_complete = False if pygame.sprite.spritecollide(self, exit_group, False) and player.coins_collected == 5: level_complete = True if self.rect.bottom > SCREEN_HEIGHT: self.health = 0 if self.char_type == 'player': if self.rect.left + dx < 0 or self.rect.right + dx > SCREEN_WIDTH: dx = 0 self.rect.x += dx self.rect.y += dy 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 shoot(self): if self.shoot_cooldown == 0 and self.ammo > 0: self.shoot_cooldown = 20 bullet = Bullet(self.rect.centerx + (0.75 * self.rect.size[0] * self.direction), self.rect.centery, self.direction) bullet_group.add(bullet) self.ammo -= 1 shot_fx.play() def ai(self): if self.alive and player.alive: if not self.idling 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): if player.rect.centerx < self.rect.centerx: self.direction = -1 self.flip = True else: self.direction = 1 self.flip = False # Stop running and face the player self.update_action(0) # 0: idle # Shoot self.shoot() else: if not self.idling: if self.direction == 1: ai_moving_right = True self.flip = False else: ai_moving_right = False self.flip = True 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 *= -1 else: self.idling_counter -= 1 if self.idling_counter <= 0: self.idling = False # Scroll self.rect.x += screen_scroll def update_animation(self): ANIMATION_COOLDOWN = 110 self.image = self.animation_list[self.action][self.frame_index] if pygame.time.get_ticks() - self.update_time > ANIMATION_COOLDOWN: self.update_time = pygame.time.get_ticks() self.frame_index += 1 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): if new_action != self.action: self.action = new_action 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]) 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 <= 7: self.obstacle_list.append(tile_data) elif tile >= 8 and tile <= 10: water = Water(img, x * TILE_SIZE, y * TILE_SIZE) water_group.add(water) elif tile == 11: item_box = ItemBox('Collectable', x * TILE_SIZE, y * TILE_SIZE) item_box_group.add(item_box) elif tile >= 12 and tile <= 14: decoration = Decoration(img, x * TILE_SIZE, y * TILE_SIZE) decoration_group.add(decoration) elif tile == 15: player = Soldier('player', x * TILE_SIZE, y * TILE_SIZE, 1.65, 5, 20, 5) health_bar = HealthBar(10, 10, player.health, player.health) elif tile == 16: enemy = Soldier('enemy', x * TILE_SIZE, y * TILE_SIZE, 1.65, 2, 20, 0) enemy_group.add(enemy) elif tile == 20: exit = Exit(img, x * TILE_SIZE, y * TILE_SIZE) exit_group.add(exit) 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 ItemBox(pygame.sprite.Sprite): def __init__(self, item_type, x, y): pygame.sprite.Sprite.__init__(self) self.item_type = item_type self.image = item_boxes[self.item_type] 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 if pygame.sprite.collide_rect(self, player): if self.item_type == 'Collectable': player.coins_collected += 1 self.kill() 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): self.health = health 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)) class Bullet(pygame.sprite.Sprite): def __init__(self, x, y, direction): pygame.sprite.Sprite.__init__(self) self.speed = 10 self.image = bullet_img self.rect = self.image.get_rect() self.rect.center = (x, y) self.direction = direction def update(self): self.rect.x += (self.direction * self.speed) + screen_scroll if self.rect.right < 0 or self.rect.left > SCREEN_WIDTH: self.kill() for tile in world.obstacle_list: if tile[1].colliderect(self.rect): self.kill() if pygame.sprite.spritecollide(player, bullet_group, False): if player.alive: player.health -= 5 self.kill() for enemy in enemy_group: if pygame.sprite.spritecollide(enemy, bullet_group, False): if enemy.alive: enemy.health -= 25 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 + 10 if self.direction == 1: 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: 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 # Screen animations intro_fade = ScreenFade(1, BLACK, 4) death_fade = ScreenFade(2, PINK, 4) # Button variables & postions restart_button = button.Button(SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2 - 80, restart_img, 2) exit_button = button.Button(SCREEN_WIDTH // 2 - 50, SCREEN_HEIGHT // 2 + 10, exit_img, 2) resume_button = button.Button(SCREEN_WIDTH // 2 - 50, SCREEN_HEIGHT // 2 - 160, resume_img, 2) # Sprite groups enemy_group = pygame.sprite.Group() bullet_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() # World data list (tile sheet) world_data = [] for row in range(ROWS): r = [-1] * COLS world_data.append(r) # Import world data from levels file with open(f'level{level_num}_data.csv', newline='') as csvfile: # 'level_num' makes it so that the data can be level specific and change :) 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) # Set game as running run = True pause = False # Main game loop while run: clock.tick(FPS) # Calculate current score (time) current_score = time.time() - start_time # Loop for if game not paused if start_game and not pause: #checks start_game is True and game is not paused draw_bg() world.draw() health_bar.draw(player.health) # 'Ammo' indicator set up draw_text('PENCILS: ', font, WHITE, 10, 35) for x in range(player.ammo): screen.blit(bullet_img, (110 + (x * 10), 40)) # Text UI draw_text('COINS: ' + str(player.coins_collected) + ' / 5', font, WHITE, 10, 60) draw_text(f'TIME: {int(current_score)}', font, WHITE, 665, 40) draw_text(f'BEST TIME: {int(high_score)}', font, WHITE, 610, 10) # Call player functions player.update() player.draw() # Load enemies for enemy in enemy_group: enemy.ai() enemy.update() enemy.draw() # Update groups bullet_group.update() grenade_group.update() explosion_group.update() item_box_group.update() decoration_group.update() water_group.update() exit_group.update() bullet_group.draw(screen) item_box_group.draw(screen) decoration_group.draw(screen) water_group.draw(screen) exit_group.draw(screen) # Start animation if start_intro: if intro_fade.fade(): start_intro = False intro_fade.fade_counter = 0 # Player logic if player.alive: # Shoot if shoot: player.shoot() # Movement animations if player.in_air: # Jump player.update_action(2) elif moving_left or moving_right: # Walk player.update_action(1) else: player.update_action(0) # Idle # Have screen and background reaction to player position screen_scroll, level_complete = player.move(moving_left, moving_right) bg_scroll -= screen_scroll # Check if level is being completed for the first time if level_complete and level_num == level_end: start_intro = True if current_score < high_score: # Update high score if time better high_score = current_score with open(f'hs_{level_num}.json', 'w') as f: json.dump({"high_score": high_score}, f) level_num += 1 bg_scroll = 0 return ('level_selector', level_end + 1) # Check if level is being complete (not the first time) elif level_complete: if current_score < high_score: # Still updates high score if better high_score = current_score with open(f'hs_{level_num}.json', 'w') as f: json.dump({"high_score": high_score}, f) start_intro = True bg_scroll = 0 return ('level_selector', level_end) # Does not update level_end on completion (this is so that you can't skip levels by doing one multiple times) else: screen_scroll = 0 # When dead if death_fade.fade(): # If exit button is pressed return player to level selector (lobby) if exit_button.draw(screen): return ('level_selector', level_end) # If restart button is pressed reset the level completely if restart_button.draw(screen): death_fade.fade_counter = 0 start_intro = True bg_scroll = 0 return ('level_' + str(level_num), level_end) # If game is paused else: screen_scroll = 0 if death_fade.fade(): # Plays death fade animation to transition to menu (being multi-purposed) # If exit button is pressed return player to level selector (lobby) if exit_button.draw(screen): pause = False return ('level_selector', level_end) # If restart button is pressed reset the level completely if restart_button.draw(screen): pause = False death_fade.fade_counter = 0 start_intro = True bg_scroll = 0 return ('level_' + str(level_num), level_end) # Resume game if resume button is pressed if resume_button.draw(screen): pause = False # Unpauses game # If window is closed ('X' in top right is clicked), close game for event in pygame.event.get(): if event.type == pygame.QUIT: return ('quit', level_end) # Movement and actions when pressed if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT: moving_left = True if event.key == pygame.K_RIGHT: moving_right = True if event.key == pygame.K_SPACE: shoot = True if event.key == pygame.K_UP and player.alive and not player.in_air: player.jump = True jump_fx.play() if event.key == pygame.K_ESCAPE: pause = True # Movement when released if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT: moving_left = False if event.key == pygame.K_RIGHT: moving_right = False if event.key == pygame.K_SPACE: shoot = False pygame.display.update() pygame.quit()