diff --git a/assets/textures/GUI/slider_cursor_1.png b/assets/textures/GUI/slider_cursor_1.png new file mode 100644 index 0000000..bd5477f Binary files /dev/null and b/assets/textures/GUI/slider_cursor_1.png differ diff --git a/assets/textures/GUI/slider_cursor_2.png b/assets/textures/GUI/slider_cursor_2.png new file mode 100644 index 0000000..1cb7999 Binary files /dev/null and b/assets/textures/GUI/slider_cursor_2.png differ diff --git a/assets/textures/GUI/slider_rail_1.png b/assets/textures/GUI/slider_rail_1.png new file mode 100644 index 0000000..26bf1e1 Binary files /dev/null and b/assets/textures/GUI/slider_rail_1.png differ diff --git a/src/custom_AI.py b/src/custom_AI.py index 038540c..ef3a1d3 100644 --- a/src/custom_AI.py +++ b/src/custom_AI.py @@ -32,23 +32,23 @@ class WolfAI(MobAI): # On vérifie que le loup peut voir le joueur if player_distance <= self.ATTACK_DISTANCE: - # On rétablit la vitesse du loup à 1 - self.entity.max_speed = 1. + # On rétablit la vitesse du loup à 60 + self.entity.max_speed = 60. # Si le loup touche le joueur, il lui inflige des dégats if player.get_collisions_with_entity(self.entity): player.take_damages(1) # Si le loup n'est pas déja sur le joueur, on le fait s'en raprocher - if player_distance > self.entity.max_speed: + if player_distance > 1.: self.entity.move(x_distance / player_distance*self.entity.max_speed, - y_distance / player_distance*self.entity.max_speed, self.map_manager) + y_distance / player_distance*self.entity.max_speed, self.map_manager, delta) else: # Comportement d'attente # On diminue la vitesse - self.entity.max_speed = 0.5 + self.entity.max_speed = 30. self.timer -= delta # Si le timer est fini et que le loup était en train d'attendre, il commence à marcher @@ -66,4 +66,4 @@ class WolfAI(MobAI): # On fait avancer le loup quand il le doit if self.comportment == 1: - self.entity.move(self.walk_x, self.walk_y, self.map_manager) + self.entity.move(self.walk_x, self.walk_y, self.map_manager, delta) diff --git a/src/engine/camera.py b/src/engine/camera.py index 672d7a9..a4601f5 100644 --- a/src/engine/camera.py +++ b/src/engine/camera.py @@ -17,11 +17,11 @@ class Camera: self.target_y = self.y self.target_zoom = self.zoom - self.smoothness = 20. + self.smoothness = 0.5 self.followed_entity: Entity | None = None - def update(self, zoom: float): + def update(self, delta: float, zoom: float): """Met à jour la caméra. Permet, par exemple, de faire le scrolling.""" if not self.DEBUG_MODE: @@ -34,9 +34,9 @@ class Camera: self.target_y = (self.followed_entity.y + self.followed_entity.mouvements[1] * self.player_moving_offset / self.zoom) - self.x += (self.target_x - self.x) / self.smoothness - self.y += (self.target_y - self.y) / self.smoothness - self.zoom += (self.target_zoom - self.zoom) / self.smoothness + self.x += (self.target_x - self.x)*delta / self.smoothness + self.y += (self.target_y - self.y)*delta / self.smoothness + self.zoom += (self.target_zoom - self.zoom)*delta / self.smoothness def follow_entity(self, entity: Entity | None): """Active le suivit de l'entité donnée. Mettre `None` pour retirer le suivit.""" diff --git a/src/engine/dialogs_manager.py b/src/engine/dialogs_manager.py index 0faec80..f32d605 100644 --- a/src/engine/dialogs_manager.py +++ b/src/engine/dialogs_manager.py @@ -1,14 +1,11 @@ import json from types import FunctionType - -from src.engine.event_handler import EventHandler - +import src.engine.engine as engine class DialogsManager: """Classe qui gère la lecture des dialogues.""" - def __init__(self, event_handler: EventHandler): - self.event_handler = event_handler - + def __init__(self, engine: 'engine.Engine'): + self.engine = engine self.current_dialogs = [] self.current_dialog_id = -1 self.dialogs = {} @@ -31,8 +28,6 @@ class DialogsManager: else: self.next_dialog() - print("next") - def next_dialog(self): """Passe au dialogue suivant. Appelle le callback si le dialogue est fini.""" self.current_dialog_id += 1 @@ -43,7 +38,8 @@ class DialogsManager: self.current_dialog_id = -1 self.writing_dialog = False self.reading_dialog = False - self.event_handler.remove_button_area("next_dialog") + self.engine.entity_manager.resume() + self.engine.event_handler.remove_button_area("next_dialog") if self.dialogue_finished_callback is not None: self.dialogue_finished_callback() @@ -52,7 +48,9 @@ class DialogsManager: # Si un dialogue n'est pas déja lancé, on lance le dialogue au nom donné if not self.reading_dialog: - self.event_handler.register_button_area((0, 0, 1, 1), self.next_signal, "next_dialog", 2) + self.engine.entity_manager.pause() + + self.engine.event_handler.register_button_area((0, 0, 1, 1), self.next_signal, "next_dialog", 2) self.current_dialogs = self.dialogs[name] self.current_dialog_id = 0 diff --git a/src/engine/engine.py b/src/engine/engine.py index cde31fa..b17d9f3 100644 --- a/src/engine/engine.py +++ b/src/engine/engine.py @@ -1,3 +1,5 @@ +import time + from src.engine.boss_fight_manager import BossFightManager from src.engine.camera import Camera from src.engine.dialogs_manager import DialogsManager @@ -25,8 +27,6 @@ class Engine: # Etat courant du jeu self.game_state = GameState.NORMAL - self.clock = pygame.time.Clock() - self.running = False # Composants du moteur de jeu @@ -38,7 +38,7 @@ class Engine: self.entity_manager = EntityManager(self.map_manager) self.boss_fight_manager = BossFightManager(self) self.event_sheduler = EventSheduler(self) - self.dialogs_manager = DialogsManager(self.event_handler) + self.dialogs_manager = DialogsManager(self) self.menu_manager = MenuManager(self) self.sound_manager = SoundManager(self.settings_manager.get_music_master_volume(), self.settings_manager.get_sound_global_master_volume(), @@ -49,20 +49,26 @@ class Engine: """Fonction à lancer au début du programme et qui va lancer les updates dans une boucle. Attend jusqu'à la fin du jeu.""" self.running = True - while self.running: - self.update() - self.clock.tick(60.) - def update(self): + delta = 1. # Le delta est le temps depuis la dernière image + last_time = time.time_ns()/10E8 + while self.running: + self.update(delta) + + new_time = time.time_ns()/10E8 + delta = new_time-last_time + last_time = new_time + + def update(self, delta: float): """Fonction qui regroupe toutes les updates des composants. Elle permet de mettre à jour le jeu quand on l'appelle.""" - self.camera.update(self.settings_manager.get_zoom()) - self.entity_manager.update(0.016666666) - self.renderer.update(0.016666666) - self.event_handler.update() + self.camera.update(delta, self.settings_manager.get_zoom()) + self.entity_manager.update(delta) + self.renderer.update(delta) + self.event_handler.update(delta) self.event_sheduler.update() - self.dialogs_manager.update(0.016666666) - self.sound_manager.update(1/60, self.settings_manager.get_music_master_volume(), + self.dialogs_manager.update(delta) + self.sound_manager.update(delta, self.settings_manager.get_music_master_volume(), self.settings_manager.get_sound_global_master_volume(), self.settings_manager.get_sound_master_volume()) diff --git a/src/engine/entity.py b/src/engine/entity.py index 392a884..86a2f0a 100644 --- a/src/engine/entity.py +++ b/src/engine/entity.py @@ -11,6 +11,8 @@ class Entity: self.x = 8 self.y = 8 + self.locked = False # Variable définissant si l'entité est bloqué ou non (.lock() et .unlock()) + self.direction = 0 # 0 : tourné vers la droite (ou sens par défaut), 1 : tourné vers la gauche (ou retourné) # Variables utilisées pour détecter les mouvements @@ -63,8 +65,23 @@ class Entity: # Si les coordonnées ont changé, l'entité a bougé - self.mouvements[0] = (self.x - self.last_x) / self.max_speed - self.mouvements[1] = (self.y - self.last_y) / self.max_speed + x_motion = (self.x - self.last_x) + + if x_motion > 0: + self.mouvements[0] = 1 + elif x_motion < 0: + self.mouvements[0] = -1 + else: + self.mouvements[0] = 0 + + y_motion = (self.y - self.last_y) + + if y_motion > 0: + self.mouvements[1] = 1 + elif y_motion < 0: + self.mouvements[1] = -1 + else: + self.mouvements[1] = 0 self.last_x = self.x self.last_y = self.y @@ -114,55 +131,67 @@ class Entity: return collision - def move(self, x: float, y: float, map_manager: MapManager): + def move(self, x: float, y: float, map_manager: MapManager, delta: float): """Fait bouger l'entité en tenant compte des collisions.""" - # On vérifie le sens du mouvement pour changer self.direction - if x > 0: - self.direction = 0 - elif x < 0: - self.direction = 1 - # On ne met pas de else car si x = 0, on ne change pas de direction - - # On normalise la vitesse - initial_speed = math.sqrt(x**2+y**2) - - x = x/initial_speed*self.max_speed - y = y/initial_speed*self.max_speed - - # On simule le mouvement. Si on ne rencontre pas de collision, on applique le mouvement - if not self.get_collisions(self.x + x, self.y, map_manager): - self.x += x - else: - # Si on a une collision, on avance pixel par pixel jusqu'à atteindre la collision - i = 0 + if not self.locked: # Si l'entité n'est pas verrouillée on calcul le mouvement + + # On vérifie le sens du mouvement pour changer self.direction if x > 0: - while not self.get_collisions(self.x + i, self.y, map_manager): - i += 1 - i -= 1 + self.direction = 0 + elif x < 0: + self.direction = 1 + # On ne met pas de else car si x = 0, on ne change pas de direction + + # On normalise la vitesse + initial_speed = math.sqrt(x**2+y**2) + + x = x*delta/initial_speed*self.max_speed + y = y*delta/initial_speed*self.max_speed + + # On simule le mouvement. Si on ne rencontre pas de collision, on applique le mouvement + + if not self.get_collisions(self.x + x, self.y, map_manager): + self.x += x else: - while not self.get_collisions(self.x + i, self.y, map_manager): + # Si on a une collision, on avance pixel par pixel jusqu'à atteindre la collision + i = 0 + if x > 0: + while not self.get_collisions(self.x + i, self.y, map_manager): + i += 1 i -= 1 - i += 1 - - self.x += i - - # On répète le procédé avec l'ordonnée - if not self.get_collisions(self.x, self.y + y, map_manager): - self.y += y - else: - i = 0 - if y > 0: - while not self.get_collisions(self.x, self.y + i, map_manager): + else: + while not self.get_collisions(self.x + i, self.y, map_manager): + i -= 1 i += 1 - i -= 1 - else: - while not self.get_collisions(self.x, self.y + i, map_manager): - i -= 1 - i += 1 - self.y += i + self.x += i + + # On répète le procédé avec l'ordonnée + if not self.get_collisions(self.x, self.y + y, map_manager): + self.y += y + else: + i = 0 + if y > 0: + while not self.get_collisions(self.x, self.y + i, map_manager): + i += 1 + i -= 1 + else: + while not self.get_collisions(self.x, self.y + i, map_manager): + i -= 1 + i += 1 + + self.y += i def link_animation(self, name: str): - """Met à jour l'animation en cours de l'entité.""" + """Met à jour l'animation en cours de l'entitée.""" self.animation_name = name + + + def lock(self): + """Bloque tout les mouvements de l'entitée""" + self.locked = True + + def unlock(self): + """Débloque tout les mouvements de l'entitée""" + self.locked = False \ No newline at end of file diff --git a/src/engine/entity_manager.py b/src/engine/entity_manager.py index 72e59a6..f7ac98c 100644 --- a/src/engine/entity_manager.py +++ b/src/engine/entity_manager.py @@ -8,6 +8,8 @@ class EntityManager: self.entities: dict[str:Entity] = {} self.player_entity_name = "" self.map_manager = map_manager + self.locked_before_pause: list[Entity] = [] + self.paused = False def register_entity(self, name: str) -> Entity: """Crée une entité et l'enregistre dans un dictionnaire.""" @@ -19,10 +21,10 @@ class EntityManager: """Définit l'entité donnée comme le joueur. Elle peut donc être controlée.""" self.player_entity_name = name - def move_player_controls(self, x: float, y: float): + def move_player_controls(self, x: float, y: float, delta: float): """Bouge le joueur. X et y doivent être compris entre 0 et 1""" player: Entity = self.get_by_name(self.player_entity_name) - player.move(x, y, self.map_manager) + player.move(x, y, self.map_manager, delta) def update(self, delta: float): """Met à jour toutes les entités enregistrées.""" @@ -32,7 +34,7 @@ class EntityManager: if entity.life_points == 0: self.entities.pop(entity_name) - if entity.brain is not None: + if entity.brain is not None and not self.paused: entity.brain.update(delta) if self.player_entity_name: @@ -49,3 +51,21 @@ class EntityManager: def get_by_name(self, name: str) -> Entity: """Donne l'entité avec le nom donné.""" return self.entities[name] + + def pause(self): + """Met en pause tout les mouvements de toutes les entitées""" + for e in self.get_all_entities(): + if e.locked: + self.locked_before_pause.append(e) + else: + e.lock() + self.paused = True + + def resume(self): + """Reprend les mouvement de toutes les entitées qui n'étaient pas lock avant l'appel de .pause()""" + for e in self.get_all_entities(): + if not e in self.locked_before_pause: + e.unlock() + + self.paused = False + self.locked_before_pause = [] diff --git a/src/engine/event_handler.py b/src/engine/event_handler.py index 9aad6af..a302b2f 100644 --- a/src/engine/event_handler.py +++ b/src/engine/event_handler.py @@ -14,7 +14,9 @@ class EventHandler: self.engine = core self.key_pressed = [] self.buttons_area = [] - self.hovered_area = [] + self.hovered_buttons_area = [] + self.hovered_sliders_area = [] + self.sliders_area = [] @staticmethod def get_click_collision(rect: tuple[float | int, float | int, float | int, float | int], point: tuple[int, int], @@ -40,7 +42,7 @@ class EventHandler: callback: FunctionType | classmethod | staticmethod, name: str, 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ée 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, hover_callback)) @@ -56,9 +58,51 @@ class EventHandler: self.buttons_area = cleared_list - def update(self): + def register_slider_area(self, size: tuple[float | int, float | int], + motion_rect: tuple[float | int, float | int, float | int, float | int], + motion_axes: tuple[bool, bool], + name: str, + is_window_relative: int = -1, + clicked_callback: FunctionType | classmethod | staticmethod = None, + released_callback: FunctionType | classmethod | staticmethod = None, + motion_callback: FunctionType | classmethod | staticmethod = None, + hover_callback: FunctionType | classmethod | staticmethod = None): + """Enregistre une zone comme une zone déplaçable à l'écran.""" + self.sliders_area.append([[motion_rect[0], motion_rect[1], *size], is_window_relative, False, (0, 0), + motion_axes, motion_rect, + clicked_callback, released_callback, hover_callback, motion_callback, name]) + # Le premier booléen correspond à l'état de suivi de la souris + + def remove_slider_area(self, name: str): + """Supprime les sliders aux noms donnés.""" + + # On itère dans toute la liste et on ne garde que les éléments ne portant pas le nom cherché + cleared_list = [] + for area in self.sliders_area: + if area[10] != name: + cleared_list.append(area) + + self.sliders_area = cleared_list + + @staticmethod + def get_slider_area_values(slider: list): + """Donne la valeur de la zone de slider donnée.""" + if slider[5][2]: + x_value = round((slider[0][0]-slider[5][0])/slider[5][2], 5) + else: + x_value = -1 + if slider[5][3]: + y_value = round((slider[0][1]-slider[5][1])/slider[5][3], 5) + else: + y_value = -1 + return x_value, y_value + + + def update(self, delta: float): """Vérifie s'il y a de nouvelles interactions et les traites.""" + window_size = display.get_window_size() + # Récupère les événements for e in event.get(): if e.type == QUIT: @@ -74,27 +118,98 @@ class EventHandler: for area in self.buttons_area: if self.get_click_collision(area[0], e.pos, area[2]): area[1]() + + + for area in self.sliders_area: + if self.get_click_collision( + (area[0][0]-area[0][2]/2, area[0][1]-area[0][3]/2, area[0][2], area[0][3]), + e.pos, area[1]): + area[2] = True + if area[1] == 0: + area[3] = (e.pos[0]/window_size[0] - area[0][0], e.pos[1]/window_size[0] - area[0][1]) + elif area[1] == 1: + area[3] = (e.pos[0]/window_size[1] - area[0][0], e.pos[1]/window_size[1] - area[0][1]) + elif area[1] == 2: + area[3] = (e.pos[0]/window_size[0] - area[0][0], e.pos[1]/window_size[1] - area[0][1]) + else: + area[3] = (e.pos[0] - area[0][0], e.pos[1] - area[0][1]) + + if area[6] is not None: + area[6](self.get_slider_area_values(area)) + + elif e.type == MOUSEBUTTONUP: + for area in self.sliders_area: + if area[2]: + area[2] = False + if area[7] is not None: + area[7](self.get_slider_area_values(area)) + 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: + if area not in self.hovered_buttons_area: area[4](True) - self.hovered_area.append(area) + self.hovered_buttons_area.append(area) else: - if area in self.hovered_area: + if area in self.hovered_buttons_area: area[4](False) - self.hovered_area.remove(area) + self.hovered_buttons_area.remove(area) + + for area in self.sliders_area: + if area[2]: + if area[4][0]: + if area[1] == 0: + area[0][0] = e.pos[0]/window_size[0]-area[3][0] + elif area[1] == 1: + area[0][0] = e.pos[0]/window_size[1]-area[3][0] + elif area[1] == 2: + area[0][0] = e.pos[0]/window_size[0]-area[3][0] + else: + area[0][0] = e.pos[0] - area[3][0] + if area[4][1]: + if area[1] == 0: + area[0][1] = e.pos[1]/window_size[0]-area[3][1] + elif area[1] == 1: + area[0][1] = e.pos[1]/window_size[1]-area[3][1] + elif area[1] == 2: + area[0][1] = e.pos[1]/window_size[1]-area[3][1] + else: + area[0][1] = e.pos[1]-area[3][1] + + if area[0][0] < area[5][0]: + area[0][0] = area[5][0] + if area[0][0] > area[5][0]+area[5][2]: + area[0][0] = area[5][0]+area[5][2] + + if area[0][1] < area[5][1]: + area[0][1] = area[5][1] + if area[0][1] > area[5][1]+area[5][3]: + area[0][1] = area[5][1]+area[5][3] + + if area[9] is not None: + area[9](self.get_slider_area_values(area)) + if area[8] is not None: + if self.get_click_collision( + (area[0][0] - area[0][2] / 2, area[0][1] - area[0][3] / 2, area[0][2], area[0][3]), + e.pos, area[1]): + if area not in self.hovered_sliders_area: + area[8](True) + self.hovered_sliders_area.append(area) + else: + if area in self.hovered_sliders_area: + area[8](False) + self.hovered_sliders_area.remove(area) if self.engine.entity_manager.player_entity_name: if K_RIGHT in self.key_pressed: - self.engine.entity_manager.move_player_controls(1, 0) + self.engine.entity_manager.move_player_controls(1, 0, delta) if K_LEFT in self.key_pressed: - self.engine.entity_manager.move_player_controls(-1, 0) + self.engine.entity_manager.move_player_controls(-1, 0, delta) if K_UP in self.key_pressed: - self.engine.entity_manager.move_player_controls(0, -1) + self.engine.entity_manager.move_player_controls(0, -1, delta) if K_DOWN in self.key_pressed: - self.engine.entity_manager.move_player_controls(0, 1) + self.engine.entity_manager.move_player_controls(0, 1, delta) if K_SPACE in self.key_pressed: self.engine.dialogs_manager.next_signal() @@ -106,7 +221,7 @@ class EventHandler: if K_p in self.key_pressed: self.engine.renderer.emit_particles(math.floor(self.engine.entity_manager.get_by_name("player").x), math.floor(self.engine.entity_manager.get_by_name("player").y), - 16, 16, 16, 1, 8, 0, 1, 0.2, 1., (0, 200, 200)) + 16, 16, 16, 1, 8, 0, 60., 0.2, 1., (0, 200, 200)) if K_o in self.key_pressed: print(f"Player pos: X = {self.engine.entity_manager.get_by_name('player').x} " f"Y = {self.engine.entity_manager.get_by_name('player').y}") diff --git a/src/engine/menu_manager.py b/src/engine/menu_manager.py index e972a35..5065549 100644 --- a/src/engine/menu_manager.py +++ b/src/engine/menu_manager.py @@ -7,30 +7,74 @@ import src.engine.engine class Widget: """Classe parente des widgets de menu.""" - def __init__(self, x, y, is_window_relative): + def __init__(self, x, y, is_window_relative, widget_name): self.x = x self.y = y self.is_window_relative = is_window_relative + self.widget_name = widget_name 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) + widget_name: str, centered: bool = False, is_window_relative: int = -1): + super().__init__(x, y, is_window_relative, widget_name) self.text = text self.size = size self.centered = centered self.color = color +class Slider(Widget): + """Un widget pouvant être glissé pour récupérer une valeur.""" + + def __init__(self, cursor_size: tuple[int | float, int | float], + area_rect: tuple[int | float, int | float], + width: int | float, + base_image: pygame.Surface, + hover_image: pygame.Surface, + rail_image: pygame.Surface, + widget_name: str, + value_changed_callback: FunctionType | classmethod | staticmethod | None = None, + is_window_relative: int = -1, + area_name: str = "menu_slider"): + super().__init__(area_rect[0], area_rect[1], is_window_relative, widget_name) + self.base_image = base_image + self.hover_image = hover_image + self.rail_image = rail_image + self.area_name = area_name + self.value_changed_callback = value_changed_callback + self.hovered = False + self.follow_mouse = False + self.cursor_size = cursor_size + self.value = 0. + self.width = width + + def set_hover_state(self, state: bool): + """Modifie la valeur du hover.""" + self.hovered = state + + def set_value(self, values: tuple[float, float]): + """Appelée lorsque la valeur du slider est modifiée.""" + new_value = values[0] + + if new_value != self.value: + self.value = new_value + if self.value_changed_callback is not None: + self.value_changed_callback(self.value) + + def get_value(self): + """Retourne la valeur entre 0.0 et 1.0 du slider.""" + return self.value + + 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, + hover_image: pygame.Surface, widget_name: str, centered: bool = False, is_window_relative: int = -1, area_name: str = "menu_button"): - super().__init__(x, y, is_window_relative) + super().__init__(x, y, is_window_relative, widget_name) self.text = text self.size = size self.color = color @@ -68,28 +112,48 @@ class MenuManager: """Ajoute le menu donné au manager de menu avec le nom donné.""" self.menus[name] = menu + def get_widgets_at_name(self, menu_name: str, widget_name: str): + """Donne le widget au nom donné dans le menu au nom donné.""" + menu = self.menus[menu_name] + + found_sliders = [] + for widget in menu.widgets: + if widget.widget_name == widget_name: + found_sliders.append(widget) + + return found_sliders + 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: + for widget in self.active_menu.widgets: + if isinstance(widget, Button): + width = widget.base_image.get_width() / self.engine.renderer.window_size[0] + height = widget.base_image.get_height() / self.engine.renderer.window_size[1] + area_x = widget.x + area_y = widget.y + if widget.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) + self.engine.event_handler.register_button_area((area_x, area_y, width, height), widget.callback, + widget.area_name, + widget.is_window_relative, widget.set_hover_state) + elif isinstance(widget, Slider): + self.engine.event_handler.register_slider_area(widget.cursor_size, + (widget.x, widget.y, widget.width, 0), (True, False), + widget.area_name, + widget.is_window_relative, + hover_callback=widget.set_hover_state, + motion_callback=widget.set_value) def hide(self): - """Affiche le menu actuelement à l'écran.""" + """Cache 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) + for widget in self.active_menu.widgets: + if isinstance(widget, Button): + self.engine.event_handler.remove_button_area(widget.area_name) + if isinstance(widget, Slider): + self.engine.event_handler.remove_slider_area(widget.area_name) self.active_menu = None diff --git a/src/engine/renderer.py b/src/engine/renderer.py index e73bc87..9e50035 100644 --- a/src/engine/renderer.py +++ b/src/engine/renderer.py @@ -1,13 +1,15 @@ import math import random +from time import time_ns +from types import FunctionType from pygame import display, image, surface, transform, draw, font 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 +from src.engine.menu_manager import Label, Button, Slider class Renderer: @@ -15,6 +17,7 @@ class Renderer: def __init__(self, core: 'engine.Engine'): self.engine = core + self.timer = 0 # Timer local self.window_type = RESIZABLE self.window_size = (display.Info().current_w, display.Info().current_h) if self.window_type == FULLSCREEN else ( 600, 600) @@ -37,6 +40,24 @@ class Renderer: # Particules affichées self.particles = [] + # Varialbes du fadeout + self.fadeout_timer = 0 # Timer de fadeout + self.fadeout_is_fading = False + self.fadeout_fade_in_s = 0 + self.fadeout_fade_color = (255, 255, 255) + self.fadeout_fade_opacity = 100 + self.fadeout_pause = False + self.fadeout_fade_callback = None + + # Variables du fadein + self.fadein_timer = 0 # Timer de fadein + self.fadein_is_fading = False + self.fadein_fade_in_s = 0 + self.fadein_fade_color = (255, 255, 255) + self.fadein_fade_opacity = 100 + self.fadein_pause = False + self.fadein_fade_callback = None + def emit_particles(self, x: int, y: int, w: int, h: int, count: int, min_size: int, max_size: int, min_speed: float, max_speed: float, min_life_time: float, max_life_time: float, color: tuple[int, int, int]): @@ -80,18 +101,40 @@ class Renderer: def update(self, delta: float): """Fait le rendu du jeu.""" + self.timer -= delta + self.fadeout_timer -= delta + self.fadein_timer -= delta + + if self.fadeout_timer < 0: + if self.fadeout_is_fading: + self.fadeout_is_fading = False + if self.fadeout_pause: + self.engine.entity_manager.resume() + self.fadeout_pause = False + if self.fadeout_fade_callback is not None: + self.fadeout_fade_callback() + + if self.fadein_timer < 0: + if self.fadein_is_fading: + self.fadein_is_fading = False + if self.fadein_pause: + self.engine.entity_manager.resume() + self.fadein_pause = False + if self.fadein_fade_callback is not None: + self.fadein_fade_callback() + self.window.fill((255, 255, 255)) + # On crée une surface qui sera ajoutée à la fenêtre apres rendered_surface pour pouvoir mettre des GUI + gui_surface = surface.Surface(display.get_window_size(), SRCALPHA) + gui_surface.fill((0, 0, 0, 0)) + if self.engine.game_state == GameState.NORMAL: # On crée une surface temporaire qui nous permettra de faire le rendu à l'échelle 1:1 rendered_surface_size = (display.get_window_size()[0] / self.engine.camera.zoom, display.get_window_size()[1] / self.engine.camera.zoom) rendered_surface = surface.Surface(rendered_surface_size) - # On crée une surface qui sera ajoutée à la fenêtre apres rendered_surface pour pouvoir mettre des GUI - gui_surface = surface.Surface(display.get_window_size(), SRCALPHA) - gui_surface.fill((0, 0, 0, 0)) - self.render_layer(0, rendered_surface) self.render_layer(1, rendered_surface) self.render_entities(rendered_surface, gui_surface, delta) @@ -105,7 +148,6 @@ class Renderer: math.ceil(rendered_surface_size[1] * self.engine.camera.zoom))), (0, 0)) - self.window.blit(gui_surface, (0, 0)) elif self.engine.game_state == GameState.BOSS_FIGHT: self.window.fill((255, 230, 230)) @@ -115,9 +157,22 @@ class Renderer: # Rend les menus self.render_menus() + if self.fadeout_is_fading != self.fadein_is_fading: + if self.fadeout_is_fading: + r, g, b = self.fadeout_fade_color + a = (1 - self.fadeout_timer / self.fadeout_fade_in_s) * self.fadeout_fade_opacity + gui_surface.fill((r, g, b, a)) + + if self.fadein_is_fading: + r, g, b = self.fadein_fade_color + a = self.fadein_timer / self.fadein_fade_in_s * self.fadein_fade_opacity + gui_surface.fill((r, g, b, a)) + + self.window.blit(gui_surface, (0, 0)) + # Conteur de FPS en mode DEBUG if self.engine.DEBUG_MODE: - self.window.blit(font.SysFont("Arial", 20).render(f"FPS: {round(self.engine.clock.get_fps())}", True, (255, 0, 0)), + self.window.blit(font.SysFont("Arial", 20).render(f"FPS: {round(1/delta if delta else 1)}, Game Status: {'Paused' if self.engine.entity_manager.paused else 'Playing'}", True, (255, 0, 0)), (0, 0)) player = self.engine.entity_manager.get_by_name('player') self.window.blit(font.SysFont("Arial", 20).render(f"X: {round(player.x, 2)} Y:{round(player.y, 2)}", @@ -129,9 +184,10 @@ class Renderer: self.window.blit(font.SysFont("Arial", 20).render(f"Track: {self.engine.sound_manager.music_current_song}", True, (255, 0, 0)), (0, 120)) + window_size = display.get_window_size() + # On rend maintenant toutes les zones de détection de la fenêtre for area in self.engine.event_handler.buttons_area: - window_size = display.get_window_size() if area[2] == 0: draw.rect(self.window, (255, 255, 0), (area[0][0] * window_size[0], area[0][1] * window_size[0], @@ -148,6 +204,58 @@ class Renderer: draw.rect(self.window, (255, 255, 0), area[0], width=1) + for area in self.engine.event_handler.sliders_area: + if area[1] == 0: + draw.rect(self.window, (0, 255, 20), + ((area[0][0]-area[0][2]/2) * window_size[0], (area[0][1]-area[0][3]/2) * window_size[0], + area[0][2] * window_size[0], area[0][3] * window_size[0]), width=1) + draw.rect(self.window, (0, 255, 200), + (area[5][0] * window_size[0], area[5][1] * window_size[0], + area[5][2] * window_size[0], area[5][3] * window_size[0]), width=1) + draw.line(self.window, (255, 0, 0), + (area[0][0] * window_size[0] - 2, area[0][1] * window_size[0]), + (area[0][0] * window_size[0] + 2, area[0][1] * window_size[0])) + draw.line(self.window, (255, 0, 0), + (area[0][0] * window_size[0], area[0][1] * window_size[0] - 2), + (area[0][0] * window_size[0], area[0][1] * window_size[0] + 2)) + elif area[1] == 1: + draw.rect(self.window, (0, 255, 20), + ((area[0][0]-area[0][2]/2) * window_size[1], (area[0][1]-area[0][3]/2) * window_size[1], + area[0][2] * window_size[1], area[0][3] * window_size[1]), width=1) + draw.rect(self.window, (0, 255, 200), + (area[5][0] * window_size[1], area[5][1] * window_size[1], + area[5][2] * window_size[1], area[5][3] * window_size[1]), width=1) + draw.line(self.window, (255, 0, 0), + (area[0][0] * window_size[1] - 2, area[0][1] * window_size[1]), + (area[0][0] * window_size[1] + 2, area[0][1] * window_size[1])) + draw.line(self.window, (255, 0, 0), + (area[0][0] * window_size[1], area[0][1] * window_size[1] - 2), + (area[0][0] * window_size[1], area[0][1] * window_size[1] + 2)) + elif area[1] == 2: + draw.rect(self.window, (0, 255, 20), + ((area[0][0]-area[0][2]/2) * window_size[0], (area[0][1]-area[0][3]/2) * window_size[1], + area[0][2] * window_size[0], area[0][3] * window_size[1]), width=1) + draw.rect(self.window, (0, 255, 200), + (area[5][0] * window_size[0], area[5][1] * window_size[1], + area[5][2] * window_size[0], area[5][3] * window_size[1]), width=1) + draw.line(self.window, (255, 0, 0), + (area[0][0]*window_size[0] - 2, area[0][1]*window_size[1]), + (area[0][0]*window_size[0] + 2, area[0][1]*window_size[1])) + draw.line(self.window, (255, 0, 0), + (area[0][0]*window_size[0], area[0][1]*window_size[1] - 2), + (area[0][0]*window_size[0], area[0][1]*window_size[1] + 2)) + else: + draw.rect(self.window, (0, 255, 20), + (area[0][0]-area[0][2]//2, area[0][1]-area[0][3]//2, area[0][2], area[0][3]), width=1) + draw.rect(self.window, (0, 255, 200), + area[5], width=1) + draw.line(self.window, (255, 0, 0), + (area[0][0]-2, area[0][1]), + (area[0][0]+2, area[0][1])) + draw.line(self.window, (255, 0, 0), + (area[0][0], area[0][1]-2), + (area[0][0], area[0][1]+2)) + # Rendu présent dans tous les types de jeu self.render_dialogs_box() @@ -236,6 +344,46 @@ class Renderer: self.window.blit(btn_image, (x, y)) self.window.blit(rendered_text, (x, y)) + elif isinstance(widget, Slider): + if widget.hovered: + slider_image = widget.hover_image + else: + slider_image = widget.base_image + + rail_image = widget.rail_image + + if widget.is_window_relative == 0: + slider_image = transform.scale(slider_image, + (slider_image.get_width()*window_size[0]/self.window_size[0], + slider_image.get_height()*window_size[0]/self.window_size[0])) + rail_image = transform.scale(rail_image, + (rail_image.get_width() * window_size[0] / self.window_size[0], + rail_image.get_height() * window_size[0] / self.window_size[ + 0])) + width = widget.width*window_size[0] + elif widget.is_window_relative == 1: + slider_image = transform.scale(slider_image, + (slider_image.get_width()*window_size[1]/self.window_size[1], + slider_image.get_height()*window_size[1]/self.window_size[1])) + rail_image = transform.scale(rail_image, + (rail_image.get_width() * window_size[1] / self.window_size[1], + rail_image.get_height() * window_size[1] / self.window_size[ + 1])) + width = widget.width * window_size[1] + elif widget.is_window_relative == 2: + slider_image = transform.scale(slider_image, + (slider_image.get_width()*window_size[0]/self.window_size[0], + slider_image.get_height()*window_size[1]/self.window_size[1])) + rail_image = transform.scale(rail_image, + (rail_image.get_width() * window_size[0] / self.window_size[0], + rail_image.get_height() * window_size[1] / self.window_size[ + 1])) + width = widget.width * min(window_size[0], window_size[1]) + + self.window.blit(rail_image, (x+(width-rail_image.get_width()) // 2, + y - rail_image.get_height() // 2)) + self.window.blit(slider_image, (x+widget.value*width-slider_image.get_width()//2, + y-slider_image.get_height()//2)) def render_dialogs_box(self): """Rend la boite de dialogue lorsqu'un dialogue est lancé.""" @@ -332,8 +480,8 @@ class Renderer: draw.rect(rendered_surface, part[7], part_dest + (part[2], part[2])) part[5] += delta - part[0] += part[3] - part[1] += part[4] + part[0] += part[3]*delta + part[1] += part[4]*delta if part[5] > part[6]: self.particles.remove(part) @@ -488,3 +636,27 @@ class Renderer: (math.floor(x * self.tile_size - self.engine.camera.x + x_middle_offset), math.floor(y * self.tile_size - self.engine.camera.y + y_middle_offset), self.tile_size, self.tile_size), width=1) + + def fadeout(self, fade_s: float, fade_color: tuple[int, int, int] = (0, 0, 0), fade_opacity: int = 100, pause_world: bool = True, callback: FunctionType = None): + """Fait un fondu vers la couleur au format : (255, 255, 255) et a l'opacité max spécifié, et dans le temps spécifié, appelle la fonction callback une fois le fadout terminé""" + self.fadein_is_fading = False + self.fadeout_timer = fade_s + self.fadeout_fade_in_s = fade_s + self.fadeout_is_fading = True + self.fadeout_fade_color = fade_color + self.fadeout_fade_opacity = round(fade_opacity * 255 / 100) + self.fadeout_pause = pause_world + self.fadeout_fade_callback = callback + self.engine.entity_manager.pause() + + def fadein(self, fade_s: float, fade_color: tuple[int, int, int] = (0, 0, 0), fade_opacity: int = 100, pause_world: bool = True, callback: FunctionType = None): + """Fait un fondu depuis la couleur au format : (255, 255, 255) et depuis l'opacité spécifié, et dans le temps spécifié, appelle la fonction callback une fois le fadout terminé""" + self.fadeout_is_fading = False + self.fadein_timer = fade_s + self.fadein_fade_in_s = fade_s + self.fadein_is_fading = True + self.fadein_fade_color = fade_color + self.fadein_fade_opacity = round(fade_opacity * 255 / 100) + self.fadein_pause = pause_world + self.fadein_fade_callback = callback + self.engine.entity_manager.pause() \ No newline at end of file diff --git a/src/main.py b/src/main.py index 437a8e2..328e160 100644 --- a/src/main.py +++ b/src/main.py @@ -36,6 +36,10 @@ class Game(Engine): def start_game(self): self.game_state = GameState.NORMAL + self.renderer.fadein(1, (0, 0, 0), 100, True) + + def play_button_callback(self): + self.renderer.fadeout(1, (0, 0, 0), 100, True, self.start_game) self.menu_manager.hide() self.sound_manager.music_remove_from_playlist(".\\assets\\OST\\Main Title (Y'as pas de boss la donc jpp le mettre pour un fight).mp3") @@ -45,12 +49,17 @@ class Game(Engine): 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)) + menu.add_widget(Label(0.5, 0.1, "The Forest's Secret", 0.1, (0, 0, 0), "game_title", 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() + btn_base_image = pygame.image.load("assets/textures/GUI/button_1.png").convert_alpha() + btn_hover_image = pygame.image.load("assets/textures/GUI/button_2.png").convert_alpha() + + slider_base_image = pygame.image.load("assets/textures/GUI/slider_cursor_1.png").convert_alpha() + slider_hover_image = pygame.image.load("assets/textures/GUI/slider_cursor_2.png").convert_alpha() + slider_rail_image = pygame.image.load("assets/textures/GUI/slider_rail_1.png").convert_alpha() + + menu.add_widget(Button(0.5, 0.3, "Play", 0.08, (0, 0, 0), self.play_button_callback, btn_base_image, btn_hover_image, "play_button", True, 0)) - 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") @@ -78,7 +87,7 @@ class Game(Engine): player.collision_rect = [-6, -7, 6, 16] player.set_default_life(15) - player.max_speed = 1.1 + player.max_speed = 64.0 self.entity_manager.set_player_entity("player") @@ -101,7 +110,7 @@ class Game(Engine): mob.collision_rect = [-15, -7, 12, 7] mob.set_default_life(5) - mob.max_speed = 1. + mob.max_speed = 60. mob.x, mob.y = 1600, 16