Merge pull request 'menu_manager' (#23) from menu_manager into main

Reviewed-on: #23
This commit is contained in:
Yannis 2024-01-07 17:40:31 +00:00
commit abba46f034
7 changed files with 220 additions and 12 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -5,6 +5,7 @@ from src.engine.entity_manager import EntityManager
from src.engine.event_handler import EventHandler from src.engine.event_handler import EventHandler
from src.engine.event_sheduler import EventSheduler from src.engine.event_sheduler import EventSheduler
from src.engine.map_manager import MapManager from src.engine.map_manager import MapManager
from src.engine.menu_manager import MenuManager
from src.engine.renderer import Renderer from src.engine.renderer import Renderer
from src.engine.enums import GameState from src.engine.enums import GameState
import pygame import pygame
@ -35,6 +36,7 @@ class Engine:
self.boss_fight_manager = BossFightManager(self) self.boss_fight_manager = BossFightManager(self)
self.event_sheduler = EventSheduler(self) self.event_sheduler = EventSheduler(self)
self.dialogs_manager = DialogsManager(self.event_handler) self.dialogs_manager = DialogsManager(self.event_handler)
self.menu_manager = MenuManager(self)
def loop(self): def loop(self):
"""Fonction à lancer au début du programme et qui va lancer les updates dans une boucle. """Fonction à lancer au début du programme et qui va lancer les updates dans une boucle.

View file

@ -14,6 +14,7 @@ class EventHandler:
self.engine = core self.engine = core
self.key_pressed = [] self.key_pressed = []
self.buttons_area = [] self.buttons_area = []
self.hovered_area = []
@staticmethod @staticmethod
def get_click_collision(rect: tuple[float | int, float | int, float | int, float | int], point: tuple[int, int], 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], def register_button_area(self, rect: tuple[float | int, float | int, float | int, float | int],
callback: FunctionType | classmethod | staticmethod, name: str, 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 """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 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""" 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): def remove_button_area(self, name: str):
"""Supprime les boutons aux noms donnés.""" """Supprime les boutons aux noms donnés."""
@ -72,6 +74,17 @@ class EventHandler:
for area in self.buttons_area: for area in self.buttons_area:
if self.get_click_collision(area[0], e.pos, area[2]): if self.get_click_collision(area[0], e.pos, area[2]):
area[1]() 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 self.engine.entity_manager.player_entity_name:
if K_RIGHT in self.key_pressed: if K_RIGHT in self.key_pressed:

View file

@ -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

View file

@ -7,6 +7,7 @@ from pygame.locals import RESIZABLE, SRCALPHA, FULLSCREEN
import src.engine.engine as engine import src.engine.engine as engine
from src.engine.animation import Anim from src.engine.animation import Anim
from src.engine.enums import GameState from src.engine.enums import GameState
from src.engine.menu_manager import Label, Button
class Renderer: class Renderer:
@ -30,9 +31,6 @@ class Renderer:
# Boite de dialogue # Boite de dialogue
self.dialogs_box = None self.dialogs_box = None
# Variables utilisées par le menu principal
self.main_menu_assets: dict[str: Anim] = {}
# Ombres d'entités # Ombres d'entités
self.shadows = {} self.shadows = {}
@ -68,9 +66,6 @@ class Renderer:
# Le 0 correspond au temps de vie depuis la création de la particule # 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]) 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): 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.""" """Charge le jeu de tuiles en utilisant le fichier donné et la taille donnée."""
tile_set = image.load(file_path).convert_alpha() tile_set = image.load(file_path).convert_alpha()
@ -117,6 +112,9 @@ class Renderer:
self.render_boss_fight_scene(delta) self.render_boss_fight_scene(delta)
self.render_boss_fight_gui() self.render_boss_fight_gui()
# Rend les menus
self.render_menus()
# Conteur de FPS en mode DEBUG # Conteur de FPS en mode DEBUG
if self.engine.DEBUG_MODE: if self.engine.DEBUG_MODE:
self.window.blit(font.SysFont("Arial", 20).render(f"FPS: {self.engine.clock.get_fps()}", True, (255, 0, 0)), 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 # Apres avoir tout rendu, on met à jour l'écran
display.update() 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): def render_dialogs_box(self):
"""Rend la boite de dialogue lorsqu'un dialogue est lancé.""" """Rend la boite de dialogue lorsqu'un dialogue est lancé."""
@ -362,9 +443,6 @@ class Renderer:
entity.collision_rect[3] - entity.collision_rect[1]), entity.collision_rect[3] - entity.collision_rect[1]),
width=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): def render_layer(self, layer_id: int, rendered_surface: surface.Surface):
"""Rend la map.""" """Rend la map."""
# On calcule le nombre de tiles à mettre sur notre écran en prenant en compte le zoom # On calcule le nombre de tiles à mettre sur notre écran en prenant en compte le zoom

View file

@ -4,6 +4,7 @@ from src.custom_AI import WolfAI
from src.engine.animation import Anim from src.engine.animation import Anim
from src.engine.engine import Engine from src.engine.engine import Engine
from src.engine.enums import GameState from src.engine.enums import GameState
from src.engine.menu_manager import Menu, Label, Button
class Game(Engine): class Game(Engine):
@ -20,7 +21,7 @@ class Game(Engine):
self.DEBUG_MODE = True 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) 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.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): def create_player_entity(self):
"""Crée une entité joueur.""" """Crée une entité joueur."""
anim = Anim(0.5) anim = Anim(0.5)