diff --git a/assets/textures/GUI/button_1.png b/assets/textures/GUI/button_1.png new file mode 100644 index 0000000..2373576 Binary files /dev/null and b/assets/textures/GUI/button_1.png differ diff --git a/assets/textures/GUI/button_2.png b/assets/textures/GUI/button_2.png new file mode 100644 index 0000000..600a10c Binary files /dev/null and b/assets/textures/GUI/button_2.png differ diff --git a/src/engine/engine.py b/src/engine/engine.py index 0d67740..0b86865 100644 --- a/src/engine/engine.py +++ b/src/engine/engine.py @@ -5,6 +5,7 @@ from src.engine.entity_manager import EntityManager from src.engine.event_handler import EventHandler from src.engine.event_sheduler import EventSheduler from src.engine.map_manager import MapManager +from src.engine.menu_manager import MenuManager from src.engine.renderer import Renderer from src.engine.enums import GameState import pygame @@ -35,6 +36,7 @@ class Engine: self.boss_fight_manager = BossFightManager(self) self.event_sheduler = EventSheduler(self) self.dialogs_manager = DialogsManager(self.event_handler) + self.menu_manager = MenuManager(self) def loop(self): """Fonction à lancer au début du programme et qui va lancer les updates dans une boucle. diff --git a/src/engine/event_handler.py b/src/engine/event_handler.py index c4c976d..1568ed0 100644 --- a/src/engine/event_handler.py +++ b/src/engine/event_handler.py @@ -14,6 +14,7 @@ class EventHandler: self.engine = core self.key_pressed = [] self.buttons_area = [] + self.hovered_area = [] @staticmethod def get_click_collision(rect: tuple[float | int, float | int, float | int, float | int], point: tuple[int, int], @@ -37,11 +38,12 @@ class EventHandler: def register_button_area(self, rect: tuple[float | int, float | int, float | int, float | int], callback: FunctionType | classmethod | staticmethod, name: str, - is_window_relative: int = -1): + is_window_relative: int = -1, + hover_callback: FunctionType | classmethod | staticmethod = None): """Enregistre une zone comme bouton. La fonction donnée sera donc executé lorsque la zone sur la fenêtre sera cliqué. is_window_relative doit être 0 pour que le rect soit multipliée par la largeur de la fenêtre et 1 pour qu'elle soit multipliée par la hauteur""" - self.buttons_area.append((rect, callback, is_window_relative, name)) + self.buttons_area.append((rect, callback, is_window_relative, name, hover_callback)) def remove_button_area(self, name: str): """Supprime les boutons aux noms donnés.""" @@ -72,6 +74,17 @@ class EventHandler: for area in self.buttons_area: if self.get_click_collision(area[0], e.pos, area[2]): area[1]() + elif e.type == MOUSEMOTION: + for area in self.buttons_area: + if area[4] is not None: + if self.get_click_collision(area[0], e.pos, area[2]): + if area not in self.hovered_area: + area[4](True) + self.hovered_area.append(area) + else: + if area in self.hovered_area: + area[4](False) + self.hovered_area.remove(area) if self.engine.entity_manager.player_entity_name: if K_RIGHT in self.key_pressed: diff --git a/src/engine/menu_manager.py b/src/engine/menu_manager.py new file mode 100644 index 0000000..e972a35 --- /dev/null +++ b/src/engine/menu_manager.py @@ -0,0 +1,95 @@ +from types import FunctionType + +import pygame + +import src.engine.engine + + +class Widget: + """Classe parente des widgets de menu.""" + def __init__(self, x, y, is_window_relative): + self.x = x + self.y = y + self.is_window_relative = is_window_relative + + +class Label(Widget): + """Un widget de texte.""" + def __init__(self, x: int | float, y: int | float, text: str, size: int | float, color: tuple[int, int, int], + centered: bool = False, is_window_relative: int = -1): + super().__init__(x, y, is_window_relative) + self.text = text + self.size = size + self.centered = centered + self.color = color + + +class Button(Widget): + """Un widget de bouton.""" + def __init__(self, x: int | float, y: int | float, text: str, size: int | float, color: tuple[int, int, int], + callback: FunctionType | classmethod | staticmethod, base_image: pygame.Surface, + hover_image: pygame.Surface, centered: bool = False, is_window_relative: int = -1, + area_name: str = "menu_button"): + super().__init__(x, y, is_window_relative) + self.text = text + self.size = size + self.color = color + self.callback = callback + self.base_image = base_image + self.hover_image = hover_image + self.centered = centered + self.area_name = area_name + self.hovered = False + + def set_hover_state(self, state: bool): + """Modifie la valeur du hover.""" + self.hovered = state + + +class Menu: + """Un menu contenant des widgets.""" + def __init__(self): + self.widgets: list[Widget] = [] + + def add_widget(self, widget: Widget): + """Ajoute le widget donné au menu.""" + self.widgets.append(widget) + + +class MenuManager: + """Classe qui gère les menus.""" + + def __init__(self, engine: 'src.engine.engine.Engine'): + self.menus = {} + self.active_menu: Menu | None = None + self.engine = engine + + def register_menu(self, menu: Menu, name: str): + """Ajoute le menu donné au manager de menu avec le nom donné.""" + self.menus[name] = menu + + def show(self, name: str): + """Affiche le menu au nom donné.""" + self.active_menu = self.menus[name] + + # On itère dans tous les bouttons pour leur ajouter une interaction + for btn in self.active_menu.widgets: + if isinstance(btn, Button): + width = btn.base_image.get_width() / self.engine.renderer.window_size[0] + height = btn.base_image.get_height() / self.engine.renderer.window_size[1] + area_x = btn.x + area_y = btn.y + if btn.centered: + area_x -= width / 2 + area_y -= height / 2 + self.engine.event_handler.register_button_area((area_x, area_y, width, height), btn.callback, + btn.area_name, + btn.is_window_relative, btn.set_hover_state) + + def hide(self): + """Affiche le menu actuelement à l'écran.""" + # On itère dans tous les bouttons pour retirer l'interaction + for btn in self.active_menu.widgets: + if isinstance(btn, Button): + self.engine.event_handler.remove_button_area(btn.area_name) + self.active_menu = None diff --git a/src/engine/renderer.py b/src/engine/renderer.py index 804fa29..5e53da0 100644 --- a/src/engine/renderer.py +++ b/src/engine/renderer.py @@ -7,6 +7,7 @@ from pygame.locals import RESIZABLE, SRCALPHA, FULLSCREEN import src.engine.engine as engine from src.engine.animation import Anim from src.engine.enums import GameState +from src.engine.menu_manager import Label, Button class Renderer: @@ -30,9 +31,6 @@ class Renderer: # Boite de dialogue self.dialogs_box = None - # Variables utilisées par le menu principal - self.main_menu_assets: dict[str: Anim] = {} - # Ombres d'entités self.shadows = {} @@ -68,9 +66,6 @@ class Renderer: # Le 0 correspond au temps de vie depuis la création de la particule self.particles.append([part_x, part_y, part_size, part_speed_x, part_speed_y, 0., part_life_time, color]) - def load_main_menu_assets(self, path: str): - """Charge les assets du menu principal depuis le dossier donné.""" - def load_tile_set(self, file_path: str, tile_size: int): """Charge le jeu de tuiles en utilisant le fichier donné et la taille donnée.""" tile_set = image.load(file_path).convert_alpha() @@ -117,6 +112,9 @@ class Renderer: self.render_boss_fight_scene(delta) self.render_boss_fight_gui() + # Rend les menus + self.render_menus() + # Conteur de FPS en mode DEBUG if self.engine.DEBUG_MODE: self.window.blit(font.SysFont("Arial", 20).render(f"FPS: {self.engine.clock.get_fps()}", True, (255, 0, 0)), @@ -152,6 +150,89 @@ class Renderer: # Apres avoir tout rendu, on met à jour l'écran display.update() + def render_menus(self): + """Rend le menu enregistré comme visible.""" + window_size = display.get_window_size() + + # Si un menu est affiché, on itère dans tous ses widgets + if self.engine.menu_manager.active_menu is not None: + for widget in self.engine.menu_manager.active_menu.widgets: + # On multiplie les coordonnées par la taille de la fenetre si besoin + if widget.is_window_relative == 0: + x = widget.x * window_size[0] + y = widget.y * window_size[0] + elif widget.is_window_relative == 1: + x = widget.x * window_size[1] + y = widget.y * window_size[1] + elif widget.is_window_relative == 2: + x = widget.x * window_size[0] + y = widget.y * window_size[1] + else: + x = widget.x + y = widget.y + + # On vérifie quel est le widget + if isinstance(widget, Label): + # On multiplie la taille du texte si besoin + if widget.is_window_relative == 0: + size = widget.size*window_size[0] + elif widget.is_window_relative == 1: + size = widget.size*window_size[1] + elif widget.is_window_relative == 2: + size = widget.size*min(window_size[0], window_size[1]) + else: + size = widget.size + + text_font = font.SysFont("Arial", round(size)) + rendered_text = text_font.render(widget.text, True, widget.color) + if widget.centered: + self.window.blit(rendered_text, (x-rendered_text.get_width()//2, + y-rendered_text.get_height()//2)) + else: + self.window.blit(rendered_text, (x, y)) + elif isinstance(widget, Button): + # On multiplie la taille du texte si besoin + if widget.is_window_relative == 0: + size = widget.size*window_size[0] + elif widget.is_window_relative == 1: + size = widget.size*window_size[1] + elif widget.is_window_relative == 2: + size = widget.size*min(window_size[0], window_size[1]) + else: + size = widget.size + + text_font = font.SysFont("Arial", round(size)) + + rendered_text = text_font.render(widget.text, True, widget.color) + + if widget.hovered: + btn_image = widget.hover_image + else: + btn_image = widget.base_image + + if widget.is_window_relative == 0: + btn_image = transform.scale(btn_image, (btn_image.get_width()*window_size[0]/self.window_size[0], + btn_image.get_height()*window_size[0]/self.window_size[0])) + elif widget.is_window_relative == 1: + btn_image = transform.scale(btn_image, (btn_image.get_width()*window_size[1]/self.window_size[1], + btn_image.get_height()*window_size[1]/self.window_size[1])) + elif widget.is_window_relative == 2: + btn_image = transform.scale(btn_image, (btn_image.get_width()*window_size[0]/self.window_size[0], + btn_image.get_height()*window_size[1]/self.window_size[1])) + + # On affiche l'image du boutton + if widget.centered: + self.window.blit(btn_image, (x-btn_image.get_width()//2, + y-btn_image.get_height()//2)) + + self.window.blit(rendered_text, (x-rendered_text.get_width()//2, + y-rendered_text.get_height()//2)) + + else: + self.window.blit(btn_image, (x, y)) + + self.window.blit(rendered_text, (x, y)) + def render_dialogs_box(self): """Rend la boite de dialogue lorsqu'un dialogue est lancé.""" @@ -362,9 +443,6 @@ class Renderer: entity.collision_rect[3] - entity.collision_rect[1]), width=1) - def render_main_menu(self): - """Rend le menu principal du jeu.""" - def render_layer(self, layer_id: int, rendered_surface: surface.Surface): """Rend la map.""" # On calcule le nombre de tiles à mettre sur notre écran en prenant en compte le zoom diff --git a/src/main.py b/src/main.py index 414b3a2..24ccfbd 100644 --- a/src/main.py +++ b/src/main.py @@ -4,6 +4,7 @@ from src.custom_AI import WolfAI from src.engine.animation import Anim from src.engine.engine import Engine from src.engine.enums import GameState +from src.engine.menu_manager import Menu, Label, Button class Game(Engine): @@ -20,7 +21,7 @@ class Game(Engine): self.DEBUG_MODE = True - self.game_state = GameState.NORMAL + self.game_state = GameState.MAIN_MENU self.event_sheduler.register_area((64, 64, 32, 32), lambda _: self.dialogs_manager.start_dialog("test"), ["player"], False, True) @@ -28,6 +29,25 @@ class Game(Engine): self.event_handler.register_button_area((0, 0, 0.1, 0.1), lambda : print("salut"), 0) + self.setup_main_menu() + + def start_game(self): + self.game_state = GameState.NORMAL + self.menu_manager.hide() + + def setup_main_menu(self): + """Crée les éléments du menu principal.""" + menu = Menu() + menu.add_widget(Label(0.5, 0.1, "The Forest's Secret", 0.1, (0, 0, 0), True, 0)) + + base_image = pygame.image.load("assets/textures/GUI/button_1.png").convert_alpha() + hover_image = pygame.image.load("assets/textures/GUI/button_2.png").convert_alpha() + + menu.add_widget(Button(0.5, 0.3, "play", 0.08, (0, 0, 0), self.start_game, base_image, hover_image, True, 0)) + self.menu_manager.register_menu(menu, "main") + + self.menu_manager.show("main") + def create_player_entity(self): """Crée une entité joueur.""" anim = Anim(0.5)