import pygame from pygame import mixer import os import random import csv import button 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 = 80 #define game variables GRAVITY = 0.75 SCROLL_THRESH = 200 ROWS = 16 COLS = 150 TILE_SIZE = SCREEN_HEIGHT // ROWS TILE_TYPES = 21 MAX_LEVELS = 3 screen_scroll = 0 bg_scroll = 0 level = 1 start_game = False start_intro = False # Animation Action Indexes IDLE_ACTION_INDEX = 0 RUN_ACTION_INDEX = 1 JUMP_ACTION_INDEX = 2 DASH_ACTION_INDEX = 3 ATTACK1_ACTION_INDEX = 4 ATTACK2_ACTION_INDEX = 5 ATTACK3_ACTION_INDEX = 6 DEATH_ACTION_INDEX = 7 #define player action variables moving_left = False moving_right = False shoot = False grenade = False grenade_thrown = False #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) 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) #load images dash_icons = [ pygame.image.load('img/icons/dash/0.png').convert_alpha(), # full/ready pygame.image.load('img/icons/dash/1.png').convert_alpha(), # medium pygame.image.load('img/icons/dash/2.png').convert_alpha() # empty or recharging ] #button images start_img = pygame.image.load('img/start_btn.png').convert_alpha() exit_img = pygame.image.load('img/exit_btn.png').convert_alpha() restart_img = pygame.image.load('img/restart_btn.png').convert_alpha() #background TopLayer_img = pygame.image.load('img/Background/TopLayer.png').convert_alpha() TopLayer_img = pygame.transform.scale(TopLayer_img, (SCREEN_WIDTH, SCREEN_HEIGHT)) Light_img = pygame.image.load('img/Background/Light.png').convert_alpha() Light_img = pygame.transform.scale(Light_img, (SCREEN_WIDTH, SCREEN_HEIGHT)) MiddleLayer_img = pygame.image.load('img/Background/MiddleLayer.png').convert_alpha() MiddleLayer_img = pygame.transform.scale(MiddleLayer_img, (SCREEN_WIDTH, SCREEN_HEIGHT)) DownLayer_img = pygame.image.load('img/Background/DownLayer.png').convert_alpha() DownLayer_img = pygame.transform.scale(DownLayer_img, (SCREEN_WIDTH, SCREEN_HEIGHT)) sky_img = pygame.image.load('img/Background/Sky.png').convert_alpha() sky_img = pygame.transform.scale(sky_img, (SCREEN_WIDTH, SCREEN_HEIGHT)) #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) #bullet bullet_img = pygame.image.load('img/icons/bullet.png').convert_alpha() #grenade grenade_img = pygame.image.load('img/icons/grenade.png').convert_alpha() #pick up boxes health_box_img = pygame.image.load('img/icons/health_box.png').convert_alpha() ammo_box_img = pygame.image.load('img/icons/ammo_box.png').convert_alpha() grenade_box_img = pygame.image.load('img/icons/grenade_box.png').convert_alpha() dash_icon = pygame.Surface((15, 15)) dash_icon.fill((0, 255, 255)) # Cyan color for available dashes dash_icon_inactive = pygame.Surface((20, 20)) dash_icon_inactive.fill((100, 100, 100)) # gray item_boxes = { 'Health' : health_box_img, 'Ammo' : ammo_box_img, 'Grenade' : grenade_box_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_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() y = 0 # Align images to the bottom for x in range(5): screen.blit(sky_img, ((x * width) - bg_scroll * 0.3, y)) screen.blit(DownLayer_img, ((x * width) - bg_scroll * 0.4, y)) screen.blit(MiddleLayer_img, ((x * width) - bg_scroll * 0.5, y)) screen.blit(Light_img, ((x * width) - bg_scroll * 0.6, y)) screen.blit(TopLayer_img, ((x * width) - bg_scroll * 0.65, y)) print("Sky:", sky_img.get_size()) print("Downlayer:", DownLayer_img.get_size()) print("MiddleLayer:", MiddleLayer_img.get_size()) print("Light:", Light_img.get_size()) print("TopLayer:", TopLayer_img.get_size()) #function to reset level def reset_level(): enemy_group.empty() bullet_group.empty() grenade_group.empty() explosion_group.empty() item_box_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 Vergil(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 if self.char_type == 'player': animation_types = ['Idle', 'Run', 'Jump', 'Dash', 'Attack1', 'Attack2', 'Attack3'] else: animation_types = ['Idle', 'Run', 'Jump', 'Attack1', 'Attack2', 'Attack3'] for animation in animation_types: folder = f'img/{self.char_type}/{animation}' if os.path.exists(folder): num_of_frames = len(os.listdir(folder)) temp_list = [] for i in range(num_of_frames): img = pygame.image.load(f'{folder}/{i}.png').convert_alpha() # ... (scaling etc.) temp_list.append(img) self.animation_list.append(temp_list) else: print(f"Warning: Animation folder missing: {folder}") self.animation_list.append([]) # placeholder empty animation 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() # DASH-related attributes self.dash_start_time = 0 self.dash_speed = 20 # or adjust to how fast you want Vergil to dash self.max_dash_count = 3 self.dash_count = self.max_dash_count self.dash_cooldowns = [0 for _ in range(self.max_dash_count)] # cooldown timers for each dash self.dash_cooldown_time = 3000 # ms: cooldown duration self.last_dash_time = 0 # tracks dash animation duration self.dash_duration = 150 # <--- ADD THIS LINE self.is_dashing = False # is currently dashing? # AI-specific self.move_counter = 0 self.vision = pygame.Rect(0, 0, 150, 20) self.idling = False self.idling_counter = 0 # Load initial image 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() # --- ATTACK SYSTEM SETUP --- self.attack_animations = [] self.attack_index = 0 self.attack_input_buffered = False self.is_attacking = False self.idle_cycling = True self.frame_index = 0 self.frame_duration = 100 self.last_frame_time = pygame.time.get_ticks() # Load attack animations (only for player) if self.char_type == 'player': attack_count = 3 # Attack1, Attack2, Attack3 for i in range(1, attack_count + 1): temp_list = [] path = f'img/{self.char_type}/attack_{i}' try: num_of_frames = len(os.listdir(path)) for j in range(num_of_frames): img = pygame.image.load(f'{path}/{j}.png').convert_alpha() img = pygame.transform.scale(img, (int(img.get_width() * scale), int(img.get_height() * scale))) temp_list.append(img) self.attack_animations.append(temp_list) except FileNotFoundError: # Optional: Comment out or remove if you don’t want any warning print(f"[Warning] Attack animation folder not found: {path}") def update(self, moving_left=False, moving_right=False): self.check_alive() now = pygame.time.get_ticks() # Handle animations if self.is_attacking: self.update_attack_animation() else: self.update_animation() if self.is_dashing: self.update_action(DASH_ACTION_INDEX) elif self.in_air: self.update_action(JUMP_ACTION_INDEX) elif moving_left or moving_right: self.update_action(RUN_ACTION_INDEX) else: self.update_action(IDLE_ACTION_INDEX) self.update_animation() # Recharge dash self.recharge_dash() # Decrement shoot cooldown if self.shoot_cooldown > 0: self.shoot_cooldown -= 1 def trigger_attack(self): now = pygame.time.get_ticks() self.idle_cycling = False # Stop idle cycling when an actual attack starts if self.is_attacking: self.attack_input_buffered = True else: self.start_attack(now) def start_attack(self, now): self.is_attacking = True self.attack_input_buffered = False self.frame_index = 0 self.last_frame_time = now if self.is_attacking: self.update_attack_animation() else: self.update_animation() self # update cooldown if self.shoot_cooldown > 0: self.shoot_cooldown -= 1 def move(self, moving_left, moving_right): #reset movement variables screen_scroll = 0 dx = 0 dy = 0 #assign movement variables if moving left or right 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 == True and self.in_air == False: self.vel_y = -11 self.jump = False self.in_air = True # Apply gravity self.vel_y += GRAVITY if self.vel_y > 10: self.vel_y = 10 # <- Fixes your fast sinking dy += self.vel_y # Check for collisions for tile in world.obstacle_list: # Horizontal collision if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.rect.width, self.rect.height): dx = 0 if self.char_type == 'enemy': self.direction *= -1 self.move_counter = 0 # --- DASH LOGIC --- if self.is_dashing: dash_duration = 150 # milliseconds if pygame.time.get_ticks() - self.last_dash_time < dash_duration: dx = self.dash_speed * self.direction else: self.is_dashing = False # Vertical collision if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.rect.width, self.rect.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 #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 if self.rect.bottom > SCREEN_HEIGHT: 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 # Update Vergil's animation based on movement and state if not self.is_dashing: # prevent changing animation while dashing if self.in_air: self.update_action(2) # jump animation index elif dx != 0: self.update_action(1) # run animation index else: self.update_action(0) # idle animation index return screen_scroll, level_complete def check_attack_hit(self): # Define the sword's hitbox area based on direction attack_range = 20 # How far the sword reaches hitbox = pygame.Rect( self.rect.centerx + (attack_range * self.direction), self.rect.top, 40, # hitbox width self.rect.height ) # Check for collision with enemies for enemy in enemy_group: if enemy.alive and hitbox.colliderect(enemy.rect): enemy.health -= 10 # Adjust damage as needed enemy.hit_fx_timer = pygame.time.get_ticks() def draw(self): # Flash red briefly when hit if hasattr(self, 'hit_fx_timer') and pygame.time.get_ticks() - self.hit_fx_timer < 100: tinted_image = self.image.copy() tinted_image.fill((255, 0, 0, 100), special_flags=pygame.BLEND_RGBA_MULT) screen.blit(pygame.transform.flip(tinted_image, self.flip, False), self.rect) else: screen.blit(pygame.transform.flip(self.image, self.flip, False), self.rect) pygame.draw.rect(screen, (0, 255, 0), self.rect, 2) # optional hitbox outline def shoot(self): if self.char_type != 'enemy': return # Only allow enemies to shoot 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 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 in near the player if self.vision.colliderect(player.rect): #stop running and face the player self.update_action(0)#0: idle #shoot self.shoot() 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 *= -1 else: self.idling_counter -= 1 if self.idling_counter <= 0: self.idling = False #scroll self.rect.x += screen_scroll def update_attack_animation(self): now = pygame.time.get_ticks() if not self.attack_animations: return current_anim = self.attack_animations[self.attack_index] if now - self.last_frame_time >= self.frame_duration: self.last_frame_time = now self.frame_index += 1 if self.frame_index >= len(current_anim): self.frame_index = 0 if self.attack_input_buffered: self.attack_input_buffered = False self.attack_index = (self.attack_index + 1) % len(self.attack_animations) current_anim = self.attack_animations[self.attack_index] self.start_attack(now) else: self.is_attacking = False self.idle_cycling = True self.attack_index = (self.attack_index + 1) % len(self.attack_animations) if self.char_type == 'player': if self.attack_index == 0 and self.frame_index in [2, 3]: self.check_attack_hit() elif self.attack_index == 1 and self.frame_index in [2, 3]: self.check_attack_hit() elif self.attack_index == 2 and self.frame_index in [2, 3]: self.check_attack_hit() # Set sprite image and preserve rect old_midbottom = self.rect.midbottom self.image = current_anim[self.frame_index] self.rect = self.image.get_rect() self.rect.midbottom = old_midbottom self.width = self.rect.width self.height = self.rect.height def update_animation(self): ANIMATION_COOLDOWN = 100 # milliseconds now = pygame.time.get_ticks() if now - self.update_time > ANIMATION_COOLDOWN: self.update_time = now self.frame_index += 1 if self.frame_index >= len(self.animation_list[self.action]): self.frame_index = 0 old_midbottom = self.rect.midbottom self.image = self.animation_list[self.action][self.frame_index] self.rect = self.image.get_rect() self.rect.midbottom = old_midbottom 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 start_dash(self): if self.dash_count > 0 and not self.is_dashing: self.is_dashing = True self.dash_start_time = pygame.time.get_ticks() self.last_dash_time = self.dash_start_time self.dash_cooldowns[self.dash_count - 1] = self.last_dash_time self.dash_count -= 1 def recharge_dash(self): now = pygame.time.get_ticks() for i in range(self.max_dash_count): if self.dash_count < self.max_dash_count: if now - self.dash_cooldowns[i] >= self.dash_cooldown_time: self.dash_cooldowns[i] = now # reset cooldown timestamp self.dash_count += 1 def check_alive(self): if self.health <= 0: self.health = 0 self.speed = 0 self.alive = False self.update_action(3) def draw(self): # Draw the character screen.blit(pygame.transform.flip(self.image, self.flip, False), self.rect) # Draw the hitbox (green rectangle) pygame.draw.rect(screen, (0, 255, 0), self.rect, 2) 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 <= 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 <= 14: decoration = Decoration(img, x * TILE_SIZE, y * TILE_SIZE) decoration_group.add(decoration) elif tile == 15:#create player player = Vergil('player', x * TILE_SIZE, y * TILE_SIZE, 1.65, 5, 20, 5) health_bar = HealthBar(10, 10, player.health, player.health) elif tile == 16:#create enemies enemy = Vergil('enemy', x * TILE_SIZE, y * TILE_SIZE, 1.65, 2, 20, 0) enemy_group.add(enemy) elif tile == 17:#create ammo box item_box = ItemBox('Ammo', x * TILE_SIZE, y * TILE_SIZE) item_box_group.add(item_box) elif tile == 18:#create grenade box item_box = ItemBox('Grenade', x * TILE_SIZE, y * TILE_SIZE) item_box_group.add(item_box) elif tile == 19:#create health box item_box = ItemBox('Health', x * TILE_SIZE, y * TILE_SIZE) item_box_group.add(item_box) elif tile == 20:#create exit 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): #scroll self.rect.x += screen_scroll #check if the player has picked up the box if pygame.sprite.collide_rect(self, player): #check what kind of box it was if self.item_type == 'Health': player.health += 25 if player.health > player.max_health: player.health = player.max_health elif self.item_type == 'Ammo': player.ammo += 15 elif self.item_type == 'Grenade': player.grenades += 3 #delete the item box 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): #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)) 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): # Move bullet horizontally self.rect.x += (self.direction * self.speed) + screen_scroll # Remove bullet if it goes off screen if self.rect.right < 0 or self.rect.left > SCREEN_WIDTH: self.kill() # Check collision with obstacles for tile in world.obstacle_list: if tile[1].colliderect(self.rect): self.kill() # Check collision with the player (Vergil) if pygame.sprite.collide_rect(self, player): if player.alive: player.health -= 5 # Adjust damage as needed 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() 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() #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'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) # MOVE IT HERE dash = False xshoot = False if start_game == False: #draw menu screen.fill(BG) # Before game loop starts dash = False #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) current_time = pygame.time.get_ticks() for i in range(player.max_dash_count): # Default to fully ready icon_index = 2 if i >= player.dash_count: # Dash has been used; show cooldown animation elapsed = current_time - player.dash_cooldowns[i] ratio = elapsed / player.dash_cooldown_time # Set icon based on cooldown progress if ratio < 0.33: icon_index = 0 # recently used (empty icon) elif ratio < 0.66: icon_index = 1 # partially recharged else: icon_index = 2 # almost fully recharged screen.blit(dash_icons[icon_index], (115 + i * 30, 85)) player.update(moving_left, moving_right) player.draw() for enemy in enemy_group: enemy.ai() enemy.update() enemy.draw() #update and draw groups bullet_group.update() explosion_group.update() item_box_group.update() decoration_group.update() water_group.update() exit_group.update() bullet_group.draw(screen) grenade_group.draw(screen) item_box_group.draw(screen) 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: # shoot melee attack if shoot: player.trigger_attack() # dash attack instead of grenade elif dash and player.dash_count > 0 and not player.is_dashing: player.start_dash() player.dash_count -= 1 # update player action animations if player.in_air: player.update_action(2) # jump elif moving_left or moving_right: player.update_action(1) # run else: player.update_action(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: 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'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: 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'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) 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_SPACE: shoot = True player.trigger_attack() if event.key == pygame.K_q: dash = 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 if event.key == pygame.K_SPACE: shoot = False if event.key == pygame.K_q: dash = False pygame.display.update() pygame.quit()