From 654e9c66e76d30acc6201ec948a47a6df2e7d6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= Date: Thu, 10 Apr 2025 22:32:51 +0200 Subject: [PATCH] Feat(Instructions) - Add instructions screen to display game controls; implement event handling for navigation and drawing functionality. --- src/Entity/Player.py | 300 ++++++++++++++++++--------------- src/Menu/InstructionsScreen.py | 97 +++++++++++ src/handler.py | 33 +++- 3 files changed, 288 insertions(+), 142 deletions(-) create mode 100644 src/Menu/InstructionsScreen.py diff --git a/src/Entity/Player.py b/src/Entity/Player.py index 9a49322..68fa4a7 100644 --- a/src/Entity/Player.py +++ b/src/Entity/Player.py @@ -21,6 +21,8 @@ class Player(Entity): self.jump_button = 0 self.dash_button = 1 + self.attack_button = 2 + self.menu_button = 3 try: if pygame.joystick.get_count() > 0: @@ -540,152 +542,170 @@ class Player(Entity): self.is_attacking = False current_time = pygame.time.get_ticks() pressed_keys = pygame.key.get_pressed() - if pressed_keys[K_q] and pressed_keys[K_c]: - if current_time - self.last_attack_time >= self.attack_cooldown: - self.is_attacking = True - self.attack_start_time = current_time - self.last_attack_time = current_time - # Calculate direction to player - direction = vec(self.pos.x, self.pos.y) - projectile = Projectile( - pos=vec(self.pos.x, self.pos.y), - direction=direction, - speed=2, - damage=1, - color=(165, 42, 42), - enemy_proj=False, - ) - # Add projectile to the sprite group (to be placed in main.py) - pygame.event.post( - pygame.event.Event( - pygame.USEREVENT, - {"action": "create_projectile", "projectile": projectile}, - ) - ) - if pressed_keys[K_d] and pressed_keys[K_c]: - if current_time - self.last_attack_time >= self.attack_cooldown: - self.is_attacking = True - self.attack_start_time = current_time - self.last_attack_time = current_time - # Calculate direction to player - direction = vec(self.pos.x, self.pos.y) - projectile = Projectile( - pos=vec(self.pos.x, self.pos.y), - direction=direction, - speed=2, - damage=1, - color=(165, 42, 42), - enemy_proj=False, - ) - # Add projectile to the sprite group (to be placed in main.py) - pygame.event.post( - pygame.event.Event( - pygame.USEREVENT, - {"action": "create_projectile", "projectile": projectile}, - ) - ) + joystick_attack = False + if self.has_joystick and self.joystick: + try: + if self.joystick.get_numbuttons() > self.attack_button: + joystick_attack = self.joystick.get_button(self.attack_button) + except pygame.error: + pass - if pressed_keys[K_q] and pressed_keys[K_v]: - if current_time - self.last_attack_time >= self.attack_cooldown: - attack_sound = pygame.mixer.Sound("assets/sound/Boule de feu.mp3") - attack_sound.set_volume(0.4) - attack_sound.play() - self.is_attacking = True - self.attack_start_time = current_time - self.last_attack_time = current_time - # Calculate direction to player - direction = vec(-self.pos.x, 0) - projectile = Projectile( - pos=vec(self.pos.x - 50, self.pos.y - 50), - direction=direction, - speed=2, - damage=1, - color=(165, 42, 42), - enemy_proj=False, - texturePath="assets/player/Boule de feu.png", - size=(50, 50), - ) - # Add projectile to the sprite group (to be placed in main.py) - pygame.event.post( - pygame.event.Event( - pygame.USEREVENT, - {"action": "create_projectile", "projectile": projectile}, - ) - ) - if self.projectiles > 0: - self.is_attacking = True - self.attack_start_time = current_time - self.last_attack_time = current_time - # Calculate direction to player - direction = vec(-self.pos.x, 0) - projectile = Projectile( - pos=vec(self.pos.x - 50, self.pos.y - 50), - direction=direction, - speed=2, - damage=1, - color=(165, 42, 42), - enemy_proj=False, - texturePath="assets/player/Boule de feu.png", - ) - # Add projectile to the sprite group (to be placed in main.py) - pygame.event.post( - pygame.event.Event( - pygame.USEREVENT, - {"action": "create_projectile", "projectile": projectile}, - ) - ) - self.projectiles -= 1 + if ( + joystick_attack + and current_time - self.last_attack_time >= self.attack_cooldown + and self.projectiles > 0 + ): + attack_sound = pygame.mixer.Sound("assets/sound/Boule de feu.mp3") + attack_sound.set_volume(0.4) + attack_sound.play() + self.is_attacking = True + self.attack_start_time = current_time + self.last_attack_time = current_time - if pressed_keys[K_d] and pressed_keys[K_v]: - if current_time - self.last_attack_time >= self.attack_cooldown: - attack_sound = pygame.mixer.Sound("assets/sound/Boule de feu.mp3") - attack_sound.set_volume(0.4) - attack_sound.play() - self.is_attacking = True - self.attack_start_time = current_time - self.last_attack_time = current_time - # Calculate direction to player + # Direction en fonction de où le personnage est tourné + if self.facing_right: direction = vec(self.pos.x, 0) - projectile = Projectile( - pos=vec(self.pos.x + 50, self.pos.y - 50), - direction=direction, - speed=2, - damage=1, - color=(165, 42, 42), - enemy_proj=False, - texturePath="assets/player/Boule de feu.png", - size=(50, 50), + position = vec(self.pos.x + 50, self.pos.y - 50) + else: + direction = vec(-self.pos.x, 0) + position = vec(self.pos.x - 50, self.pos.y - 50) + + projectile = Projectile( + pos=position, + direction=direction, + speed=2, + damage=1, + color=(165, 42, 42), + enemy_proj=False, + texturePath="assets/player/Boule de feu.png", + size=(50, 50), + ) + + # Ajouter le projectile au groupe de sprites + pygame.event.post( + pygame.event.Event( + pygame.USEREVENT, + {"action": "create_projectile", "projectile": projectile}, ) - # Add projectile to the sprite group (to be placed in main.py) - pygame.event.post( - pygame.event.Event( - pygame.USEREVENT, - {"action": "create_projectile", "projectile": projectile}, - ) + ) + + self.projectiles -= 1 + + if ( + pressed_keys[K_q] + and pressed_keys[K_c] + and current_time - self.last_attack_time >= self.attack_cooldown + ): + self.is_attacking = True + self.attack_start_time = current_time + self.last_attack_time = current_time + # Calculate direction to player + direction = vec(self.pos.x, self.pos.y) + projectile = Projectile( + pos=vec(self.pos.x, self.pos.y), + direction=direction, + speed=2, + damage=1, + color=(165, 42, 42), + enemy_proj=False, + ) + # Add projectile to the sprite group (to be placed in main.py) + pygame.event.post( + pygame.event.Event( + pygame.USEREVENT, + {"action": "create_projectile", "projectile": projectile}, ) - if self.projectiles > 0: - self.is_attacking = True - self.attack_start_time = current_time - self.last_attack_time = current_time - # Calculate direction to player - direction = vec(self.pos.x, 0) - projectile = Projectile( - pos=vec(self.pos.x + 50, self.pos.y - 50), - direction=direction, - speed=2, - damage=1, - color=(165, 42, 42), - enemy_proj=False, - texturePath="assets/player/Boule de feu.png", - ) - pygame.event.post( - pygame.event.Event( - pygame.USEREVENT, - {"action": "create_projectile", "projectile": projectile}, - ) - ) - self.projectiles -= 1 + ) + + if ( + pressed_keys[K_d] + and pressed_keys[K_c] + and current_time - self.last_attack_time >= self.attack_cooldown + ): + self.is_attacking = True + self.attack_start_time = current_time + self.last_attack_time = current_time + # Calculate direction to player + direction = vec(self.pos.x, self.pos.y) + projectile = Projectile( + pos=vec(self.pos.x, self.pos.y), + direction=direction, + speed=2, + damage=1, + color=(165, 42, 42), + enemy_proj=False, + ) + # Add projectile to the sprite group (to be placed in main.py) + pygame.event.post( + pygame.event.Event( + pygame.USEREVENT, + {"action": "create_projectile", "projectile": projectile}, + ) + ) + + if ( + pressed_keys[K_q] + and pressed_keys[K_v] + and current_time - self.last_attack_time >= self.attack_cooldown + and self.projectiles > 0 + ): + attack_sound = pygame.mixer.Sound("assets/sound/Boule de feu.mp3") + attack_sound.set_volume(0.4) + attack_sound.play() + self.is_attacking = True + self.attack_start_time = current_time + self.last_attack_time = current_time + # Calculate direction to player + direction = vec(-self.pos.x, 0) + projectile = Projectile( + pos=vec(self.pos.x - 50, self.pos.y - 50), + direction=direction, + speed=2, + damage=1, + color=(165, 42, 42), + enemy_proj=False, + texturePath="assets/player/Boule de feu.png", + ) + # Add projectile to the sprite group (to be placed in main.py) + pygame.event.post( + pygame.event.Event( + pygame.USEREVENT, + {"action": "create_projectile", "projectile": projectile}, + ) + ) + self.projectiles -= 1 + + if ( + pressed_keys[K_d] + and pressed_keys[K_v] + and current_time - self.last_attack_time >= self.attack_cooldown + and self.projectiles > 0 + ): + attack_sound = pygame.mixer.Sound("assets/sound/Boule de feu.mp3") + attack_sound.set_volume(0.4) + attack_sound.play() + self.is_attacking = True + self.attack_start_time = current_time + self.last_attack_time = current_time + # Calculate direction to player + direction = vec(self.pos.x, 0) + projectile = Projectile( + pos=vec(self.pos.x + 50, self.pos.y - 50), + direction=direction, + speed=2, + damage=1, + color=(165, 42, 42), + enemy_proj=False, + texturePath="assets/player/Boule de feu.png", + ) + pygame.event.post( + pygame.event.Event( + pygame.USEREVENT, + {"action": "create_projectile", "projectile": projectile}, + ) + ) + self.projectiles -= 1 def add_projectiles(self): """Set player projectiles to 3 and show floating text""" diff --git a/src/Menu/InstructionsScreen.py b/src/Menu/InstructionsScreen.py new file mode 100644 index 0000000..4aa882f --- /dev/null +++ b/src/Menu/InstructionsScreen.py @@ -0,0 +1,97 @@ +import pygame +import math +from src.Menu.BackgroundManager import BackgroundManager + + +class InstructionsScreen: + def __init__(self, game_resources): + self.game_resources = game_resources + self.bg_manager = BackgroundManager(game_resources.WIDTH, game_resources.HEIGHT) + + self.title_font = pygame.font.SysFont("Arial", 72) + self.text_font = pygame.font.SysFont("Arial", 32) + + self.blink_timer = 0 + self.blink_speed = 0.5 + + def draw(self, surface): + self.bg_manager.draw(surface) + + def render_text_with_outline(text, font, text_color, outline_color): + text_surface = font.render(text, True, text_color) + outline_surface = font.render(text, True, outline_color) + + w, h = text_surface.get_size() + outline_surf = pygame.Surface((w + 2, h + 2), pygame.SRCALPHA) + + # Dessiner le contour en décalant le texte + offsets = [ + (1, 1), + (1, -1), + (-1, 1), + (-1, -1), + (1, 0), + (-1, 0), + (0, 1), + (0, -1), + ] + for dx, dy in offsets: + outline_surf.blit(outline_surface, (dx + 1, dy + 1)) + + # Dessiner le texte principal au centre + outline_surf.blit(text_surface, (1, 1)) + return outline_surf + + title_surf = render_text_with_outline( + "Game control", self.title_font, (255, 255, 255), (0, 0, 0) + ) + title_rect = title_surf.get_rect(center=(self.game_resources.WIDTH // 2, 100)) + surface.blit(title_surf, title_rect) + + instructions = [ + "Q : Move left", + "D : Move right", + "A : Dash", + "Espace : Jump", + "V: Attack", + "Escape : Pause / Menu", + "Controller : Use the left joystick to move", + "B: Dash", + "A: Jump", + "X: Attack", + "Y: Pause / Menu", + ] + + y_offset = 180 + line_spacing = 40 + + for line in instructions: + text_surf = render_text_with_outline( + line, self.text_font, (255, 255, 255), (0, 0, 0) + ) + text_rect = text_surf.get_rect( + center=(self.game_resources.WIDTH // 2, y_offset) + ) + surface.blit(text_surf, text_rect) + y_offset += line_spacing + + self.blink_timer += 0.01 + alpha = abs(math.sin(self.blink_timer * self.blink_speed)) * 255 + + skip_text = render_text_with_outline( + "Press any key to continue", self.text_font, (255, 220, 0), (0, 0, 0) + ) + skip_text.set_alpha(int(alpha)) + skip_rect = skip_text.get_rect( + center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT - 100) + ) + surface.blit(skip_text, skip_rect) + + def handle_event(self, event): + if ( + event.type == pygame.KEYDOWN + or event.type == pygame.MOUSEBUTTONDOWN + or event.type == pygame.JOYBUTTONDOWN + ): + return "menu" + return None diff --git a/src/handler.py b/src/handler.py index e886459..5357968 100644 --- a/src/handler.py +++ b/src/handler.py @@ -23,6 +23,7 @@ from src.Database.CheckpointDB import CheckpointDB from src.Map.Editor.LevelEditor import LevelEditor from src.Menu.LevelEditorSelectionMenu import LevelEditorSelectionMenu from src.Map.Speedrun.SpeedrunTimer import SpeedrunTimer +from src.Menu.InstructionsScreen import InstructionsScreen def initialize_game_resources(): @@ -63,8 +64,9 @@ def initialize_game_resources(): projectiles = pygame.sprite.Group() # Game states initialization - current_state = 0 # MENU + current_state = 5 # INSTRUCTIONS current_menu = "main" + instructions_screen = InstructionsScreen(game_resources) main_menu = Menu(game_resources) level_select_menu = None editor_select_menu = None @@ -99,6 +101,7 @@ def initialize_game_resources(): joysticks, editor_select_menu, leaderboard_db, + instructions_screen, ) @@ -134,6 +137,16 @@ def handle_system_events( ) # Update window dimensions ORIGINAL_WIDTH, ORIGINAL_HEIGHT = event.w, event.h + elif event.type == pygame.JOYBUTTONDOWN: + try: + if event.button == 4: # Triangle sur la plupart des manettes + if current_state in [1, 2]: # PLAYING, INFINITE + current_state = 0 # MENU + else: + pygame.quit() + sys.exit() + except Exception as e: + print(f"Error while handling joystick button: {e}") return current_state, fullscreen, displaysurface, ORIGINAL_WIDTH, ORIGINAL_HEIGHT @@ -645,7 +658,7 @@ def handle_death_screen( def handler(): """Main function that handles the game flow""" # Game state constants - MENU, PLAYING, INFINITE, LEADERBOARD, DEATH_SCREEN = 0, 1, 2, 3, 4 + MENU, PLAYING, INFINITE, LEADERBOARD, DEATH_SCREEN, INSTRUCTIONS = 0, 1, 2, 3, 4, 5 previous_state = None # Initialize game resources and states @@ -667,6 +680,7 @@ def handler(): joysticks, editor_select_menu, leaderboard_db, + instructions_screen, ) = initialize_game_resources() # Initialize editor variables @@ -687,6 +701,7 @@ def handler(): print(f"Error while getting events: {e}") pygame.joystick.quit() pygame.joystick.init() + events = [] continue # Process events @@ -766,6 +781,13 @@ def handler(): game_resources ) + elif current_state == INSTRUCTIONS: + for event in events: + result = instructions_screen.handle_event(event) + if result == "menu": + current_state = MENU + instructions_screen.draw(displaysurface) + # Process general game events (player death, projectiles, etc.) if event.type == USEREVENT: current_state, death_timer, checkpoint_data, projectiles = ( @@ -929,6 +951,13 @@ def handler(): elif death_result["action"] == "return_to_menu": current_state = death_result["current_state"] + elif current_state == INSTRUCTIONS: + for event in events: + result = instructions_screen.handle_event(event) + if result == "menu": + current_state = MENU + instructions_screen.draw(displaysurface) + # Update display pygame.display.update() game_resources.FramePerSec.tick(game_resources.FPS)