diff --git a/.gitignore b/.gitignore index bae480b..39666b5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ checkpoint.db checkpoint.db-journal game.db -map/infinite/* \ No newline at end of file +map/infinite/* + +temp_audio.mp3 \ No newline at end of file diff --git a/assets/map/collectibles/jump.png b/assets/map/collectibles/jump.png new file mode 100644 index 0000000..688d176 Binary files /dev/null and b/assets/map/collectibles/jump.png differ diff --git a/assets/map/collectibles/speed.png b/assets/map/collectibles/speed.png new file mode 100644 index 0000000..9446c08 Binary files /dev/null and b/assets/map/collectibles/speed.png differ diff --git a/assets/player/Sanic.gif b/assets/player/Sanic.gif new file mode 100644 index 0000000..c6994e8 Binary files /dev/null and b/assets/player/Sanic.gif differ diff --git a/map/levels/2.json b/map/levels/2.json index 2ac8c3e..17b1c72 100644 --- a/map/levels/2.json +++ b/map/levels/2.json @@ -166,7 +166,22 @@ "sprite": "assets/map/exit/Zeldo.png" } ], - "collectibles": [], + "collectibles": [ + { + "id": "jump", + "type": "jump", + "x": 1500, + "y": 400, + "sprite": "assets/map/collectibles/jump.png" + }, + { + "id": "speed", + "type": "speed", + "x": 1000, + "y": 400, + "sprite": "assets/map/collectibles/speed.png" + } + ], "spawn_point": { "x": 50.0, "y": 350.0 diff --git a/src/Entity/JumpBoost.py b/src/Entity/JumpBoost.py new file mode 100644 index 0000000..1bfb23b --- /dev/null +++ b/src/Entity/JumpBoost.py @@ -0,0 +1,112 @@ +import os +import pygame +import time +from src.Entity.Entity import Entity + + +class JumpBoost(Entity): + """ + A collectible that temporarily increases the player's jump power + for 3 seconds when collected. + """ + + def __init__(self, pos, size=(30, 30), color=(0, 255, 0), texturePath=""): + super().__init__(pos=pos, size=size, color=color, texturePath=texturePath) + self.collected = False + + # Jump boost properties + self.boost_factor = 1.5 # 50% increase in jump power + self.boost_duration = 3 # Duration in seconds + + # Create initial surface + self.surf = pygame.Surface(size, pygame.SRCALPHA) + + # Load and scale texture + if texturePath: + try: + if os.path.exists(texturePath): + texture = pygame.image.load(texturePath).convert_alpha() + textureSize = (size[0] * 1.5, size[1] * 1.5) + self.surf = pygame.transform.scale(texture, textureSize) + else: + self.draw_fallback(color, size) + except Exception as e: + self.draw_fallback(color, size) + else: + self.draw_fallback(color, size) + + # Set rect + self.rect = self.surf.get_rect() + self.rect.topleft = pos + + # Animation properties + self.animation_frame = 0 + self.last_update = 0 + + def draw_fallback(self, color, size): + """Draw a green arrow pointing up as fallback""" + self.surf.fill((0, 0, 0, 0)) + + # Draw an arrow pointing up + half_width = size[0] // 2 + height = size[1] + + # Arrow head + head_points = [ + (half_width, 0), + (half_width - 8, 10), + (half_width + 8, 10), + ] + + # Arrow body + body_rect = pygame.Rect(half_width - 4, 10, 8, height - 10) + + pygame.draw.polygon(self.surf, color, head_points) + pygame.draw.rect(self.surf, color, body_rect) + + def update(self): + """Update the jump boost animation""" + now = pygame.time.get_ticks() + if now - self.last_update > 200: + self.last_update = now + self.animation_frame = (self.animation_frame + 1) % 4 + # Simple floating animation + self.rect.y += [-1, 0, 1, 0][self.animation_frame] + + def on_collision(self, player): + """ + Handle jump boost collision with player + + Args: + player: The player object to apply the boost to + """ + if not self.collected: + self.collected = True + + # Store original jump power + original_jump_power = player.jump_power + + # Apply boost effect + player.jump_power *= self.boost_factor + + # Set visual feedback + player.jump_boost_active = True + + # Schedule effect removal + pygame.time.set_timer( + pygame.USEREVENT + 2, # Custom event ID for jump boost expiration + self.boost_duration * 1000, # Convert to milliseconds + 1, # Only trigger once + ) + + # Store reference to restore original jump power + player.active_jump_boost = { + "original_power": original_jump_power, + "boost_object": self, + } + + # Remove the collectible from display + self.kill() + + return True + return False diff --git a/src/Entity/Player.py b/src/Entity/Player.py index 8db3db7..691fa0a 100644 --- a/src/Entity/Player.py +++ b/src/Entity/Player.py @@ -2,6 +2,7 @@ from src.Entity.Entity import Entity from pygame import * import pygame import os +from PIL import Image, ImageSequence from pygame.math import Vector2 as vec from src.Entity.Projectile import Projectile @@ -45,10 +46,17 @@ class Player(Entity): self.dash_start_time = 0 self.dash_duration = 500 # 1/2 second activation time self.dash_cooldown = 3000 # 3 seconds cooldown + self.speed_boost_active = False + self.active_speed_boost = None + + # Jump mechanics + self.jump_power = 30 + self.jump_boost_active = False + self.active_jump_boost = None # Life system - self.max_lives = 2 - self.lives = 2 + self.max_lives = 5 + self.lives = 3 self.invulnerable = False self.invulnerable_timer = 0 self.invulnerable_duration = 1.5 @@ -76,86 +84,125 @@ class Player(Entity): self.attack_cooldown = 2000 def load_images(self): + """Load images for the player""" try: - # Load static image - if os.path.isfile("assets/player/Sanic Base.png"): - self.static_image = pygame.image.load( - "assets/player/Sanic Base.png" - ).convert_alpha() - self.static_image = pygame.transform.scale( - self.static_image, (100, 100) - ) - - # Load regular animation sprite sheet - if os.path.isfile("assets/player/Sanic Annimate.png"): - sprite_sheet = pygame.image.load( - "assets/player/Sanic Annimate.png" - ).convert_alpha() - - # Extract the 4 frames - frame_height = sprite_sheet.get_height() - frame_width = sprite_sheet.get_width() // 4 - - for i in range(4): - # Cut out a region of the sprite sheet - frame = sprite_sheet.subsurface( - (i * 2290, 0, frame_width, frame_height) - ) - # Resize the frame - frame = pygame.transform.scale(frame, (100, 100)) - self.animation_frames.append(frame) - - # Load jump animation sprite sheet - if os.path.isfile("assets/player/Sanic Boule.png"): - self.jump_frames.append( - pygame.transform.scale( - pygame.image.load( - "assets/player/Sanic Boule.png" - ).convert_alpha(), - (80, 80), - ) - ) - - # Load dash animation sprite sheet - if os.path.isfile("assets/player/Sanic Boule Annimate.png"): - dash_sheet = pygame.image.load( - "assets/player/Sanic Boule Annimate.png" - ).convert_alpha() - - dash_frame_height = dash_sheet.get_height() - - for i in range(4): - frame = dash_sheet.subsurface( - (i * 2000, 0, dash_frame_height, dash_frame_height) - ) - frame = pygame.transform.scale(frame, (80, 80)) - self.dash_frames.append(frame) - - # Load life icon - if os.path.isfile("assets/player/Sanic Head.png"): - self.life_icon = pygame.image.load( - "assets/player/Sanic Head.png" - ).convert_alpha() - self.life_icon = pygame.transform.scale( - self.life_icon, - ( - self.game_resources.life_icon_width, - self.game_resources.life_icon_width, - ), - ) + # Load the 7 frames of the GIF + if os.path.isfile("assets/player/Sanic.gif"): + self.load_gif_frames("assets/player/Sanic.gif") + self.animation_speed = 0.05 else: - # Backup: use a red square - self.life_icon = pygame.Surface( - ( - self.game_resources.life_icon_width, - self.game_resources.life_icon_width, - ) - ) - self.life_icon.fill((255, 0, 0)) + # Fallback to static image if GIF is not found + self.load_static_and_sprite_sheets() + + # Load special animations (jump, dash, ...) + self.load_special_animations() except Exception as e: print(f"Error loading player images: {e}") + def load_gif_frames(self, gif_path): + """Load frames from a GIF file""" + try: + gif = Image.open(gif_path) + + self.animation_frames = [] + + for frame in ImageSequence.Iterator(gif): + # Convert the frame to a format compatible with Pygame + frame_rgb = frame.convert("RGBA") + raw_str = frame_rgb.tobytes("raw", "RGBA") + + pygame_surface = pygame.image.fromstring( + raw_str, frame_rgb.size, "RGBA" + ) + + pygame_surface = pygame.transform.scale(pygame_surface, (125, 125)) + + self.animation_frames.append(pygame_surface) + + # Use the first frame as the static image + if self.animation_frames: + self.static_image = self.animation_frames[0] + + except Exception as e: + print(f"Error while loading the GIF: {e}") + + def load_static_and_sprite_sheets(self): + """Previous method to load static image and sprite sheets""" + # Load static image + if os.path.isfile("assets/player/Sanic Base.png"): + self.static_image = pygame.image.load( + "assets/player/Sanic Base.png" + ).convert_alpha() + self.static_image = pygame.transform.scale(self.static_image, (100, 100)) + + # Load regular animation sprite sheet + if os.path.isfile("assets/player/Sanic Annimate.png"): + sprite_sheet = pygame.image.load( + "assets/player/Sanic Annimate.png" + ).convert_alpha() + + # Extract the 4 frames + frame_height = sprite_sheet.get_height() + frame_width = sprite_sheet.get_width() // 4 + + for i in range(4): + # Cut out a region of the sprite sheet + frame = sprite_sheet.subsurface( + (i * 2290, 0, frame_width, frame_height) + ) + # Resize the frame + frame = pygame.transform.scale(frame, (100, 100)) + self.animation_frames.append(frame) + + def load_special_animations(self): + """Load special animations for jump and dash""" + # Load jump animation sprite sheet + if os.path.isfile("assets/player/Sanic Boule.png"): + self.jump_frames.append( + pygame.transform.scale( + pygame.image.load("assets/player/Sanic Boule.png").convert_alpha(), + (80, 80), + ) + ) + + # Load dash animation sprite sheet + if os.path.isfile("assets/player/Sanic Boule Annimate.png"): + dash_sheet = pygame.image.load( + "assets/player/Sanic Boule Annimate.png" + ).convert_alpha() + + dash_frame_height = dash_sheet.get_height() + + for i in range(4): + frame = dash_sheet.subsurface( + (i * 2000, 0, dash_frame_height, dash_frame_height) + ) + frame = pygame.transform.scale(frame, (80, 80)) + self.dash_frames.append(frame) + + # Load life icon + if os.path.isfile("assets/player/Sanic Head.png"): + self.life_icon = pygame.image.load( + "assets/player/Sanic Head.png" + ).convert_alpha() + self.life_icon = pygame.transform.scale( + self.life_icon, + ( + self.game_resources.life_icon_width, + self.game_resources.life_icon_width, + ), + ) + else: + # Backup: use a red square + self.life_icon = pygame.Surface( + ( + self.game_resources.life_icon_width, + self.game_resources.life_icon_width, + ) + ) + self.life_icon.fill((255, 0, 0)) + def update_animation(self): current_time = pygame.time.get_ticks() @@ -250,7 +297,7 @@ class Player(Entity): if jump and not self.jumping: jump_sound = pygame.mixer.Sound("assets/sound/Jump.mp3") jump_sound.play() - self.vel.y = -30 + self.vel.y = -self.jump_power self.jumping = True # Apply friction @@ -435,14 +482,16 @@ class Player(Entity): surface.blit(coin_text, (text_x, text_y)) - def collect_coin(self, surface): + def collect_coin(self, surface, speedrun_timer=None): """Increment coin counter when collecting a coin""" coin_sound = pygame.mixer.Sound("assets/sound/Coin.mp3") coin_sound.play() self.coins += 1 - if self.lives == 1: + if self.lives < self.max_lives: self.lives += 1 self.draw_lives(surface) + if speedrun_timer: + speedrun_timer.collected_items += 1 def attack(self): """Do an attack action on the player""" diff --git a/src/Entity/SpeedBoost.py b/src/Entity/SpeedBoost.py new file mode 100644 index 0000000..bbc4714 --- /dev/null +++ b/src/Entity/SpeedBoost.py @@ -0,0 +1,110 @@ +import os +import pygame +import time +from src.Entity.Entity import Entity + + +class SpeedBoost(Entity): + """ + A collectible that temporarily increases the player's movement speed + for 3 seconds when collected. + """ + + def __init__(self, pos, size=(30, 30), color=(0, 0, 255), texturePath=""): + super().__init__(pos=pos, size=size, color=color, texturePath=texturePath) + self.collected = False + + # Speed boost properties + self.boost_factor = 2 + self.boost_duration = 3 + + # Create initial surface + self.surf = pygame.Surface(size, pygame.SRCALPHA) + + # Load and scale texture + if texturePath: + try: + if os.path.exists(texturePath): + texture = pygame.image.load(texturePath).convert_alpha() + textureSize = (size[0] * 3, size[1] * 3) + self.surf = pygame.transform.scale(texture, textureSize) + else: + self.draw_fallback(color, size) + except Exception as e: + self.draw_fallback(color, size) + else: + self.draw_fallback(color, size) + + # Set rect + self.rect = self.surf.get_rect() + self.rect.topleft = pos + + # Animation properties + self.animation_frame = 0 + self.last_update = 0 + + def draw_fallback(self, color, size): + """Draw a blue lightning bolt as fallback""" + self.surf.fill((0, 0, 0, 0)) + + # Draw a lightning bolt symbol + width, height = size + points = [ + (width // 2, 0), + (width // 4, height // 2), + (width // 2 - 2, height // 2), + (width // 3, height), + (width // 2 + 5, height // 2 + 5), + (width // 2 + 2, height // 2), + (3 * width // 4, height // 2), + ] + + pygame.draw.polygon(self.surf, color, points) + + def update(self): + """Update the speed boost animation""" + now = pygame.time.get_ticks() + if now - self.last_update > 200: + self.last_update = now + self.animation_frame = (self.animation_frame + 1) % 4 + # Simple floating animation + self.rect.y += [-1, 0, 1, 0][self.animation_frame] + + def on_collision(self, player, game_resources): + """ + Handle speed boost collision with player + + Args: + player: The player object to apply the boost to + game_ressources: Game resources object containing player speed + """ + if not self.collected: + self.collected = True + + # Store original movement speed + original_ACC = game_ressources.ACC + + # Apply boost effect + game_ressources.ACC *= self.boost_factor + + # Set visual feedback + player.speed_boost_active = True + + # Schedule effect removal + pygame.time.set_timer( + pygame.USEREVENT + 3, # Custom event ID for speed boost expiration + self.boost_duration * 1000, # Convert to milliseconds + 1, # Only trigger once + ) + + # Store reference to restore original speed + player.active_speed_boost = { + "original_ACC": original_ACC, + "boost_object": self, + } + + # Remove the collectible from display + self.kill() + + return True + return False diff --git a/src/Map/Editor/LevelEditor.py b/src/Map/Editor/LevelEditor.py index 79f15b5..0b325cb 100644 --- a/src/Map/Editor/LevelEditor.py +++ b/src/Map/Editor/LevelEditor.py @@ -611,7 +611,7 @@ class LevelEditor: self.selected_object, EditorCollectible ): if event.key == K_t: - types = ["coin"] + types = ["coin", "jump", "speed"] current_index = ( types.index(self.selected_object.collectible_type) if self.selected_object.collectible_type in types @@ -623,6 +623,10 @@ class LevelEditor: # Update appearance based on type if self.selected_object.collectible_type == "coin": self.selected_object.image.fill((255, 215, 0)) + elif self.selected_object.collectible_type == "jump": + self.selected_object.image.fill((0, 255, 0)) + elif self.selected_object.collectible_type == "speed": + self.selected_object.image.fill((0, 0, 255)) elif self.selected_object and isinstance(self.selected_object, EditorExit): if event.key == K_n: diff --git a/src/Map/Speedrun/SpeedrunTimer.py b/src/Map/Speedrun/SpeedrunTimer.py index 0a39bd2..67f0c20 100644 --- a/src/Map/Speedrun/SpeedrunTimer.py +++ b/src/Map/Speedrun/SpeedrunTimer.py @@ -14,6 +14,8 @@ class SpeedrunTimer: self.best_time = self._get_best_time() self.color = (0, 255, 0) # Green by default self.font = pygame.font.Font(None, 36) + self.collected_items = 0 + self.total_items = 0 def start(self): """Start the timer""" @@ -38,7 +40,7 @@ class SpeedrunTimer: else: self.color = (255, 0, 0) # Red if behind - def save_time(self): + def save_time(self, collected_items=0, total_items=0): """Save the current time in the database""" if not self.is_running and self.current_time > 0: conn = sqlite3.connect(self.db_path) @@ -51,6 +53,8 @@ class SpeedrunTimer: id INTEGER PRIMARY KEY AUTOINCREMENT, level_id TEXT NOT NULL, time REAL NOT NULL, + collected_items INTEGER DEFAULT 0, + total_items INTEGER DEFAULT 0, date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ @@ -58,8 +62,8 @@ class SpeedrunTimer: # Insert the new time cursor.execute( - "INSERT INTO speedrun (level_id, time) VALUES (?, ?)", - (self.level_id, self.current_time), + "INSERT INTO speedrun (level_id, time, collected_items, total_items) VALUES (?, ?, ?, ?)", + (self.level_id, self.current_time, collected_items, total_items), ) conn.commit() diff --git a/src/Map/parser.py b/src/Map/parser.py index 407ea4a..697d479 100644 --- a/src/Map/parser.py +++ b/src/Map/parser.py @@ -8,6 +8,8 @@ from src.Entity.Enemy import Enemy from src.Entity.Checkpoint import Checkpoint from src.Entity.Exit import Exit from src.Entity.Coin import Coin +from src.Entity.JumpBoost import JumpBoost +from src.Entity.SpeedBoost import SpeedBoost class MapParser: @@ -137,6 +139,23 @@ class MapParser: ) self.collectibles.add(coin) self.all_sprites.add(coin) + elif collectible_data["type"] == "jump": + sprite_path = collectible_data.get("sprite", "") + jump = JumpBoost( + pos=(collectible_data["x"], collectible_data["y"]), + texturePath=sprite_path, + ) + self.collectibles.add(jump) + self.all_sprites.add(jump) + elif collectible_data["type"] == "speed": + sprite_path = collectible_data.get("sprite", "") + speed = SpeedBoost( + pos=(collectible_data["x"], collectible_data["y"]), + texturePath=sprite_path, + ) + self.collectibles.add(speed) + self.all_sprites.add(speed) + # Create background image if "background" in map_data: if os.path.isfile(map_data["background"]): diff --git a/src/Menu/BackgroundManager.py b/src/Menu/BackgroundManager.py new file mode 100644 index 0000000..65a6b07 --- /dev/null +++ b/src/Menu/BackgroundManager.py @@ -0,0 +1,45 @@ +import pygame +import random +import math + + +class BackgroundManager: + def __init__(self, width, height): + self.width = width + self.height = height + self.backgrounds = [ + "assets/map/background/forest_bg.jpg", + "assets/map/background/desert_bg.jpg", + "assets/map/background/mountain_bg.jpg", + "assets/map/background/cave_bg.png", + ] + + self.background_path = random.choice(self.backgrounds) + self.init_time = pygame.time.get_ticks() + + try: + # Load the background image + self.background = pygame.image.load(self.background_path).convert() + bg_width = width * 3 + bg_height = height * 3 + self.background = pygame.transform.scale( + self.background, (bg_width, bg_height) + ) + except Exception as e: + print(f"Erreur lors du chargement du fond d'écran: {e}") + self.background = None + + def draw(self, surface): + if self.background: + parallax_factor = 0.4 + time_factor = pygame.time.get_ticks() / 1000 + + center_x = (self.background.get_width() - surface.get_width()) / 2 + center_y = (self.background.get_height() - surface.get_height()) / 2 + + bg_x = -center_x + math.sin(time_factor) * 50 * parallax_factor + bg_y = -center_y + math.cos(time_factor) * 30 * parallax_factor + + surface.blit(self.background, (bg_x, bg_y)) + else: + surface.fill((0, 0, 0)) diff --git a/src/Menu/Leaderboard.py b/src/Menu/Leaderboard.py index eedbfc4..c66fa1c 100644 --- a/src/Menu/Leaderboard.py +++ b/src/Menu/Leaderboard.py @@ -1,6 +1,9 @@ import pygame import sqlite3 +import os from datetime import datetime + +from src.Menu.BackgroundManager import BackgroundManager from src.Menu.Button import Button from src.Database.LevelDB import LevelDB @@ -17,6 +20,8 @@ class Leaderboard: self.levels = self.get_available_levels() self.level_tabs = [f"Level {level}" for level in self.levels] + self.bg_manager = BackgroundManager(WIDTH, HEIGHT) + # Define the tabs (levels + infinite mode) self.tabs = self.level_tabs + ["Infinite mode"] self.current_tab = 0 @@ -64,7 +69,7 @@ class Leaderboard: cursor.execute( """ - SELECT time, date + SELECT time, date, collected_items, total_items FROM speedrun WHERE level_id = ? ORDER BY time ASC @@ -78,10 +83,10 @@ class Leaderboard: # Format results formatted_results = [] - for time, date in results: + for time, date, collected, total in results: date_obj = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") formatted_date = date_obj.strftime("%d/%m/%Y") - formatted_results.append((formatted_date, time)) + formatted_results.append((formatted_date, time, collected, total)) return formatted_results except (sqlite3.Error, Exception) as e: @@ -99,10 +104,23 @@ class Leaderboard: def draw(self, surface): """Draw the leaderboard on the given surface.""" - title = pygame.font.SysFont("Arial", 48).render( - "Classement", True, (0, 191, 255) + self.bg_manager.draw(surface) + + # Draw a semi-transparent panel + panel_rect = pygame.Rect(self.WIDTH // 2 - 250, 130, 500, self.HEIGHT - 200) + panel_surface = pygame.Surface( + (panel_rect.width, panel_rect.height), pygame.SRCALPHA ) + panel_surface.fill((10, 10, 40, 180)) + surface.blit(panel_surface, panel_rect) + + title_font = pygame.font.SysFont("Arial", 48, bold=True) + title = title_font.render("Leaderboard", True, (255, 255, 255)) + title_shadow = title_font.render("Leaderboard", True, (0, 0, 0)) + title_rect = title.get_rect(center=(self.WIDTH // 2, 40)) + shadow_rect = title_shadow.get_rect(center=(self.WIDTH // 2 + 2, 42)) + surface.blit(title_shadow, shadow_rect) surface.blit(title, title_rect) font = pygame.font.SysFont("Arial", 20) @@ -114,8 +132,22 @@ class Leaderboard: pygame.draw.rect(surface, (100, 100, 255), button.rect) button.draw(surface, font) + # Draw column headers + headers = ["Rank", "Date", "Time", "Collected"] + header_positions = [ + self.WIDTH // 2 - 150, + self.WIDTH // 2 - 100, + self.WIDTH // 2 + 50, + self.WIDTH // 2 + 150, + ] + + y_pos = 200 + + for i, header in enumerate(headers): + header_text = font.render(header, True, (200, 200, 200)) + surface.blit(header_text, (header_positions[i], y_pos - 30)) + # Draw scores - y_pos = 150 scores_for_tab = self.scores.get(self.current_tab, []) if not scores_for_tab: @@ -127,18 +159,38 @@ class Leaderboard: (self.WIDTH // 2 - no_scores_text.get_width() // 2, y_pos + 40), ) else: - for i, (date, time) in enumerate(scores_for_tab): + for i, (date, time, collected, total) in enumerate(scores_for_tab): + row_bg = (30, 30, 60, 150) if i % 2 == 0 else (40, 40, 80, 150) + row_rect = pygame.Rect(self.WIDTH // 2 - 200, y_pos - 5, 400, 30) + row_surface = pygame.Surface( + (row_rect.width, row_rect.height), pygame.SRCALPHA + ) + row_surface.fill(row_bg) + surface.blit(row_surface, row_rect) + # Rank rank_text = self.font.render(f"{i+1}.", True, (255, 255, 255)) - surface.blit(rank_text, (self.WIDTH // 2 - 150, y_pos)) + surface.blit(rank_text, (header_positions[0], y_pos)) + # Date date_text = self.font.render(date, True, (255, 255, 255)) - surface.blit(date_text, (self.WIDTH // 2 - 100, y_pos)) + surface.blit(date_text, (header_positions[1], y_pos)) + # Time time_text = self.font.render( self.format_time(time), True, (255, 255, 255) ) - surface.blit(time_text, (self.WIDTH // 2 + 50, y_pos)) + surface.blit(time_text, (header_positions[2], y_pos)) + + # Collected items + collected_color = (255, 255, 255) + if collected == total: + collected_color = (0, 255, 0) + + collected_text = self.font.render( + f"{collected}/{total}", True, collected_color + ) + surface.blit(collected_text, (header_positions[3], y_pos)) y_pos += 40 @@ -155,3 +207,25 @@ class Leaderboard: if action and action.startswith("tab_"): self.current_tab = int(action.split("_")[1]) return None + + def refresh_scores(self, previous_level=""): + """Refresh scores from the database.""" + if previous_level != "LEADERBOARD": + self.scores = {} + + # Get the list of levels from the directory + level_dir = "map/levels/" + try: + levels = [ + f.replace(".json", "") + for f in os.listdir(level_dir) + if f.endswith(".json") + ] + + # Get the scores for each level + for level in levels: + scores = self.get_level_scores(level) + if scores: + self.scores[int(level) - 1] = scores + except Exception as e: + print(f"Error while refreshing the score: {e}") diff --git a/src/Menu/LevelEditorSelectionMenu.py b/src/Menu/LevelEditorSelectionMenu.py index 56f5a32..b73243d 100644 --- a/src/Menu/LevelEditorSelectionMenu.py +++ b/src/Menu/LevelEditorSelectionMenu.py @@ -2,6 +2,7 @@ import pygame import os import re +from src.Menu.BackgroundManager import BackgroundManager from src.Menu.Button import Button @@ -21,6 +22,8 @@ class LevelEditorSelectionMenu: self.buttons = [] self.levels = [] + self.bg_manager = BackgroundManager(game_resources.WIDTH, game_resources.HEIGHT) + # Button dimensions self.button_width = 250 self.button_height = 60 @@ -131,6 +134,7 @@ class LevelEditorSelectionMenu: Args: surface: Pygame surface to draw on """ + self.bg_manager.draw(surface) # Draw title title = pygame.font.SysFont("Arial", 48).render( "Level Editor", True, (0, 191, 255) diff --git a/src/Menu/LevelSelectMenu.py b/src/Menu/LevelSelectMenu.py index 6b47595..80ae0e4 100644 --- a/src/Menu/LevelSelectMenu.py +++ b/src/Menu/LevelSelectMenu.py @@ -3,6 +3,7 @@ import os import re from src.Database.LevelDB import LevelDB +from src.Menu.BackgroundManager import BackgroundManager from src.Menu.Button import Button from src.game import clear_checkpoint_database, clear_level_progress @@ -24,6 +25,8 @@ class LevelSelectMenu: self.buttons = [] self.levels = [] + self.bg_manager = BackgroundManager(game_resources.WIDTH, game_resources.HEIGHT) + # Button dimensions self.button_width = 250 self.button_height = 60 @@ -168,6 +171,7 @@ class LevelSelectMenu: Args: surface: Pygame surface to draw on """ + self.bg_manager.draw(surface) # Draw title title = pygame.font.SysFont("Arial", 48).render( "Select Level", True, (0, 191, 255) diff --git a/src/Menu/Menu.py b/src/Menu/Menu.py index 287cb52..bfa3ef0 100644 --- a/src/Menu/Menu.py +++ b/src/Menu/Menu.py @@ -1,6 +1,8 @@ import pygame import random import math + +from src.Menu.BackgroundManager import BackgroundManager from src.Menu.Button import Button @@ -13,32 +15,12 @@ class Menu: button_spacing = 20 start_y = self.game_resources.HEIGHT // 2 - 100 - self.backgrounds = [ - "assets/map/background/forest_bg.jpg", - "assets/map/background/desert_bg.jpg", - "assets/map/background/mountain_bg.jpg", - "assets/map/background/cave_bg.png", - ] - - self.background_path = random.choice(self.backgrounds) - - try: - # Load the background image - self.background = pygame.image.load(self.background_path).convert() - - bg_width = game_resources.WIDTH * 3 - bg_height = game_resources.HEIGHT * 3 - self.background = pygame.transform.scale( - self.background, (bg_width, bg_height) - ) - except Exception as e: - print(f"Error while loading menu background: {e}") - self.background = None + self.bg_manager = BackgroundManager(game_resources.WIDTH, game_resources.HEIGHT) # Create buttons centered horizontally self.buttons.append( Button( - "Jouer", + "Play", self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, @@ -50,7 +32,7 @@ class Menu: start_y += button_height + button_spacing self.buttons.append( Button( - "Jouer en mode infini", + "Play in infinite mode", self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, @@ -62,7 +44,7 @@ class Menu: start_y += button_height + button_spacing self.buttons.append( Button( - "Classement", + "Leaderboard", self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, @@ -74,7 +56,7 @@ class Menu: start_y += button_height + button_spacing self.buttons.append( Button( - "Quitter", + "Quit", self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, @@ -84,23 +66,11 @@ class Menu: ) def draw(self, surface): - if self.background: - parallax_factor = 0.4 - time_factor = pygame.time.get_ticks() / 1000 - - center_x = (self.background.get_width() - surface.get_width()) / 2 - center_y = (self.background.get_height() - surface.get_height()) / 2 - - bg_x = -center_x + math.sin(time_factor) * 50 * parallax_factor - bg_y = -center_y + math.cos(time_factor) * 30 * parallax_factor - - surface.blit(self.background, (bg_x, bg_y)) - else: - surface.fill((0, 0, 0)) + self.bg_manager.draw(surface) # Draw title title = pygame.font.SysFont("Arial", 72).render( - "Sanic et la princesse Zeldo", True, (0, 191, 255) + "Sanic and the princess Zeldo", True, (0, 191, 255) ) title_rect = title.get_rect( center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT // 4) diff --git a/src/handler.py b/src/handler.py index ff55a24..73f6f85 100644 --- a/src/handler.py +++ b/src/handler.py @@ -212,6 +212,15 @@ def handle_menu_events( level_id = level_file.split("/")[-1].split(".")[0] speedrun_timer = SpeedrunTimer(level_id) speedrun_timer.start() + speedrun_timer.total_items = len( + [ + c + for c in collectibles + if hasattr(c, "__class__") + and c.__class__.__name__ not in ["JumpBoost", "SpeedBoost"] + ] + ) + speedrun_timer.collected_items = 0 projectiles = pygame.sprite.Group() current_state = 1 # PLAYING @@ -446,13 +455,28 @@ def draw_playing_state( checkpoint.activate() # Handle exit collisions - result = handle_exits(P1, exits, game_resources, level_file, speedrun_timer) + result = handle_exits( + P1, exits, game_resources, level_file, speedrun_timer, collectibles + ) # Handle collectibles - coins_hit = pygame.sprite.spritecollide(P1, collectibles, False) - for coin in coins_hit: - coin.on_collision() - P1.collect_coin(displaysurface) + collectibles_hit = pygame.sprite.spritecollide(P1, collectibles, False) + for collectible in collectibles_hit: + # Vérifier le type de collectible et appeler la méthode appropriée + if ( + hasattr(collectible, "__class__") + and collectible.__class__.__name__ == "JumpBoost" + ): + collectible.on_collision(P1) + elif ( + hasattr(collectible, "__class__") + and collectible.__class__.__name__ == "SpeedBoost" + ): + collectible.on_collision(P1, game_resources) + else: + # Pour les pièces standard et autres collectibles + collectible.on_collision() + P1.collect_coin(displaysurface, speedrun_timer) # Draw UI elements draw_ui_elements(displaysurface, P1, FramePerSec, font, speedrun_timer) @@ -460,14 +484,18 @@ def draw_playing_state( return result -def handle_exits(P1, exits, game_resources, level_file, speedrun_timer=None): +def handle_exits( + P1, exits, game_resources, level_file, speedrun_timer=None, collectibles=[] +): """Handle collisions with level exits""" exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else [] for exit in exits_hit: - if speedrun_timer: - speedrun_timer.stop() - speedrun_timer.save_time() + if speedrun_timer and speedrun_timer.is_running: + collected_coins = speedrun_timer.collected_items + total_coins = speedrun_timer.total_items + speedrun_timer.stop() + speedrun_timer.save_time(collected_coins, total_coins) if hasattr(game_resources, "infinite_mode") and game_resources.infinite_mode: # Infinite mode: load the next level without going back to menu result = handle_exit_collision(exit, game_resources, level_file) @@ -578,6 +606,7 @@ def handler(): """Main function that handles the game flow""" # Game state constants MENU, PLAYING, INFINITE, LEADERBOARD, DEATH_SCREEN = 0, 1, 2, 3, 4 + previous_state = None # Initialize game resources and states ( @@ -710,6 +739,21 @@ def handler(): ) ) + elif event.type == pygame.USEREVENT + 2: + if hasattr(P1, "active_jump_boost") and P1.active_jump_boost: + P1.jump_power = P1.active_jump_boost["original_power"] + P1.jump_boost_active = False + P1.active_jump_boost = None + + elif event.type == pygame.USEREVENT + 3: # Speed boost expiration + if hasattr(P1, "active_speed_boost") and P1.active_speed_boost: + # Restore original movement speed + game_resources.ACC = P1.active_speed_boost["original_ACC"] + # Remove visual feedback + P1.speed_boost_active = False + # Clear boost data + P1.active_speed_boost = None + # Clear screen displaysurface.fill((0, 0, 0)) @@ -732,9 +776,13 @@ def handler(): level_editor.draw(displaysurface) elif current_state == LEADERBOARD: + if previous_state != "LEADERBOARD": + leaderboard.refresh_scores(previous_state) + previous_state = "LEADERBOARD" leaderboard.draw(displaysurface) elif current_state == PLAYING: + previous_state = "PLAYING" # Update game state update_playing_state( P1, @@ -794,6 +842,7 @@ def handler(): ) = infinite_result elif current_state == INFINITE: + previous_state = "INFINITE" # Start infinite mode and switch to playing ( P1,