diff --git a/assets/dialogs.json b/assets/dialogs.json new file mode 100644 index 0000000..2185f81 --- /dev/null +++ b/assets/dialogs.json @@ -0,0 +1,5 @@ +{ + "test": ["test1111", "test2", "test très long permettant de tester le retour à la ligne dans le renderer du jeu dans la fonction qui rend les dialogues et il faut éviter d'en faire des si long car ça pourrait dépacer en bas de l'écran ! Bonne journée !"], + "test2": ["salut", "aurevoir"], + "test3": ["test très long permettant de tester le retour à la ligne dans le renderer du jeu dans la fonction qui rend les dialogues et il faut éviter d'en faire des si long car ça pourrait dépacer en bas de l'écran ! Bonne journée !"] +} \ No newline at end of file diff --git a/assets/textures/GUI/dialogs_box.png b/assets/textures/GUI/dialogs_box.png new file mode 100644 index 0000000..2e7b56a Binary files /dev/null and b/assets/textures/GUI/dialogs_box.png differ diff --git a/src/engine/dialogs_manager.py b/src/engine/dialogs_manager.py index bf15837..fd2c538 100644 --- a/src/engine/dialogs_manager.py +++ b/src/engine/dialogs_manager.py @@ -1,13 +1,89 @@ import json +from types import FunctionType + +from src.engine.event_handler import EventHandler class DialogsManager: """Classe qui gère la lecture des dialogues.""" - def __init__(self): - self.current_dialog = [] + def __init__(self, event_handler: EventHandler): + self.event_handler = event_handler + + self.current_dialogs = [] + self.current_dialog_id = -1 self.dialogs = {} + self.reading_dialog = False + self.current_dialogue_letter_id = 0 + self.writing_dialog = False + + self.LETTER_WRITING_DELAY = 0.02 + self.letter_timer = 0 + + self.dialogue_finished_callback = None + + def next_signal(self): + """Fonction exécutée lorsque l'utilisateur demande de passer au prochain dialogue. Si un dialogue est en + train d'être écrit, il écrit tout d'un coup.""" + + if self.reading_dialog: + if self.writing_dialog: + self.current_dialogue_letter_id = len(self.current_dialogs[self.current_dialog_id]) + 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 + self.writing_dialog = True + if self.current_dialog_id >= len(self.current_dialogs): # Le dialogue est fini. + self.current_dialogs = [] + self.current_dialog_id = -1 + self.writing_dialog = False + self.reading_dialog = False + self.event_handler.remove_button_area("next_dialog") + if self.dialogue_finished_callback is not None: + self.dialogue_finished_callback() + + def start_dialog(self, name: str, dialogue_finished_callback: FunctionType | classmethod | staticmethod = None): + """Lance le dialogue au nom donné.""" + + # 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", 0) + + self.current_dialogs = self.dialogs[name] + self.current_dialog_id = 0 + self.current_dialogue_letter_id = 0 + + self.reading_dialog = True + + self.dialogue_finished_callback = dialogue_finished_callback + + def get_current_dialog_sentence(self, progressive=True) -> str: + """Renvoie la phrase actuelle du dialogue.""" + if progressive: + return self.current_dialogs[self.current_dialog_id][:self.current_dialogue_letter_id] + else: + return self.current_dialogs[self.current_dialog_id] def load_dialogs(self, file_path: str): """Charge les dialogues du jeu grave au fichier json donné.""" - with open(file_path, "r") as file: + with open(file_path, "r", encoding="utf-8") as file: self.dialogs = json.loads(file.read()) + + def update(self, delta: float): + """Met à jour e gestionnaire de dialogues.""" + if self.reading_dialog: + self.letter_timer -= delta + + if self.letter_timer <= 0: + self.letter_timer = self.LETTER_WRITING_DELAY + self.current_dialogue_letter_id += 1 + if self.current_dialogue_letter_id > len(self.current_dialogs[self.current_dialog_id]): + self.current_dialogue_letter_id -= 1 + self.writing_dialog = False + + #print(self.writing_dialog, self.reading_dialog) + diff --git a/src/engine/engine.py b/src/engine/engine.py index 2d1dc8d..0d67740 100644 --- a/src/engine/engine.py +++ b/src/engine/engine.py @@ -1,7 +1,9 @@ from src.engine.boss_fight_manager import BossFightManager from src.engine.camera import Camera +from src.engine.dialogs_manager import DialogsManager 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.renderer import Renderer from src.engine.enums import GameState @@ -31,6 +33,8 @@ class Engine: self.camera = Camera() 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) def loop(self): """Fonction à lancer au début du programme et qui va lancer les updates dans une boucle. @@ -47,6 +51,8 @@ class Engine: self.entity_manager.update(0.016666666) self.renderer.update(0.016666666) self.event_handler.update() + self.event_sheduler.update() + self.dialogs_manager.update(0.016666666) def stop(self): """Arrête le programme.""" diff --git a/src/engine/event_handler.py b/src/engine/event_handler.py index 4d2ce83..e14cf13 100644 --- a/src/engine/event_handler.py +++ b/src/engine/event_handler.py @@ -1,6 +1,7 @@ import math +from types import FunctionType -from pygame import event +from pygame import event, display from pygame.locals import * import src.engine.engine as engine @@ -8,9 +9,46 @@ import src.engine.engine as engine class EventHandler: """Classe utilisée pour traiter les pygame.event.get() et gérer les interactions avec le reste du programme.""" + def __init__(self, core: 'engine.Engine'): self.engine = core self.key_pressed = [] + self.buttons_area = [] + + @staticmethod + def get_click_collision(rect: tuple[float | int, float | int, float | int, float | int], point: tuple[int, int], + is_window_relative: int): + """Vérifie si le point et le rectangle donné sont en collision.""" + window_size = display.get_window_size() + + if is_window_relative == 0: + return (rect[0]*window_size[0] < point[0] < rect[0]*window_size[0] + rect[2]*window_size[0] + and rect[1]*window_size[0] < point[1] < rect[1]*window_size[0] + rect[3]*window_size[0]) + + elif is_window_relative == 1: + return (rect[0]*window_size[1] < point[0] < rect[0]*window_size[1] + rect[2]*window_size[1] and + rect[1]*window_size[1] < point[1] < rect[1]*window_size[1] + rect[3]*window_size[1]) + + return rect[0] < point[0] < rect[0] + rect[2] and rect[1] < point[1] < rect[1] + rect[3] + + 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): + """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)) + + def remove_button_area(self, name: str): + """Supprime les boutons 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.buttons_area: + if area[3] != name: + cleared_list.append(area) + + self.buttons_area = cleared_list def update(self): """Vérifie s'il y a de nouvelles interactions et les traites.""" @@ -22,7 +60,14 @@ class EventHandler: elif e.type == KEYDOWN: self.key_pressed.append(e.key) elif e.type == KEYUP: - self.key_pressed.remove(e.key) + if e.key in self.key_pressed: + self.key_pressed.remove(e.key) + elif e.type == MOUSEBUTTONDOWN: + # Vérifie si une des zones enregistrées comme bouton n'a pas été cliqué + if e.button == 1: + for area in self.buttons_area: + if self.get_click_collision(area[0], e.pos, area[2]): + area[1]() if self.engine.entity_manager.player_entity_name: if K_RIGHT in self.key_pressed: @@ -34,13 +79,17 @@ class EventHandler: if K_DOWN in self.key_pressed: self.engine.entity_manager.move_player_controls(0, 1) + if K_SPACE in self.key_pressed: + self.engine.dialogs_manager.next_signal() + self.key_pressed.remove(K_SPACE) + if self.engine.DEBUG_MODE: if K_l in self.key_pressed: self.engine.entity_manager.get_by_name("player").take_damages(1) 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)) + math.floor(self.engine.entity_manager.get_by_name("player").y), + 16, 16, 16, 1, 8, 0, 1, 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}") @@ -49,4 +98,3 @@ class EventHandler: self.engine.camera.target_zoom *= 1.01 if K_c in self.key_pressed: self.engine.camera.target_zoom *= 0.99 - diff --git a/src/engine/event_sheduler.py b/src/engine/event_sheduler.py index c72456a..393e8f8 100644 --- a/src/engine/event_sheduler.py +++ b/src/engine/event_sheduler.py @@ -1,14 +1,41 @@ from types import FunctionType +import src.engine.engine +from src.engine.entity import Entity + class EventSheduler: """Gère le lancement d'évenements avec des conditions.""" - def __init__(self): + def __init__(self, engine: 'src.engine.engine.Engine'): self.area_callbacks = [] + self.engine = engine - def register_area(self, area_rect: tuple[int, int, int, int], callback: FunctionType | classmethod | staticmethod): - self.area_callbacks.append((area_rect, callback)) + def register_area(self, area_rect: tuple[int, int, int, int], callback: FunctionType | classmethod | staticmethod, + linked_entities_name: list[Entity], single_use: bool = True, no_spam: bool = False): + self.area_callbacks.append((area_rect, callback, linked_entities_name, single_use, no_spam, [])) + # La liste vide en dernier argument correspond aux entités actuellement dans la zone + + @staticmethod + def get_collisions_with_entity(rect: tuple[int, int, int, int], entity: 'Entity'): + """Retourne True si l'entité donnée touche le rectangle donné.""" + return (rect[0] <= entity.x+entity.collision_rect[2] and + rect[0] + rect[2] >= entity.x+entity.collision_rect[0] and + rect[1] + rect[3] >= entity.y+entity.collision_rect[1] and + rect[1] <= entity.y+entity.collision_rect[3]) def update(self): - for area in self.area_callbacks: - area_rect = area[0] \ No newline at end of file + """Met à jour l'event sheduler et execute les actions si les conditions à son execution sont respéctées.""" + + # On itère dans la liste des zones de détection + for area in self.area_callbacks.copy(): + # On itère dans toutes les entités enregistrées + for entity in area[2]: + entity_in_area = self.get_collisions_with_entity(area[0], self.engine.entity_manager.get_by_name(entity)) + if entity_in_area and not (area[4] and entity in area[5]): + area[1](entity) + if area[3]: + self.area_callbacks.remove(area) + if area[4]: + area[5].append(entity) + elif (not entity_in_area) and (area[4] and entity in area[5]): + area[5].remove(entity) diff --git a/src/engine/renderer.py b/src/engine/renderer.py index 39f1393..c39b1b5 100644 --- a/src/engine/renderer.py +++ b/src/engine/renderer.py @@ -15,7 +15,8 @@ class Renderer: def __init__(self, core: 'engine.Engine'): self.engine = core self.window_type = RESIZABLE - self.window_size = (display.Info().current_w, display.Info().current_h) if self.window_type == FULLSCREEN else (600, 600) + self.window_size = (display.Info().current_w, display.Info().current_h) if self.window_type == FULLSCREEN else ( + 600, 600) self.window = display.set_mode(self.window_size, self.window_type) self.tiles = [] self.tile_size = 0 @@ -26,6 +27,9 @@ class Renderer: self.boss_fight_player_animations: dict[str: Anim] = {} self.boss_fight_GUI_container = None + # Boite de dialogue + self.dialogs_box = None + # Variables utilisées par le menu principal self.main_menu_assets: dict[str: Anim] = {} @@ -54,8 +58,8 @@ class Renderer: part_speed_y = - part_speed_y # On choisit sa position dans le rectangle - part_x = random.randint(x-w, x+w-part_size) - part_y = random.randint(y-h, y+h-part_size) + part_x = random.randint(x - w, x + w - part_size) + part_y = random.randint(y - h, y + h - part_size) # On choisit la durée de vie part_life_time = random.uniform(min_life_time, max_life_time) @@ -98,6 +102,7 @@ class Renderer: self.render_entities(rendered_surface, gui_surface, delta) self.render_particles(rendered_surface, delta) self.render_layer(2, rendered_surface) + self.render_debug_area(rendered_surface) # Enfin, on redimensionne notre surface et on la colle sur la fenêtre principale self.window.blit( @@ -122,9 +127,94 @@ class Renderer: self.window.blit(font.SysFont("Arial", 20).render(f"Zoom: {self.engine.camera.zoom}", True, (255, 0, 0)), (0, 60)) + # 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], + area[0][2] * window_size[0], area[0][3] * window_size[0]), width=1) + elif area[2] == 1: + draw.rect(self.window, (255, 255, 0), + (area[0][0] * window_size[1], area[0][1] * window_size[1], + area[0][2] * window_size[1], area[0][3] * window_size[1]), width=1) + else: + draw.rect(self.window, (255, 255, 0), + area[0], width=1) + + # Rendu présent dans tous les types de jeu + self.render_dialogs_box() + # Apres avoir tout rendu, on met à jour l'écran display.update() + def render_dialogs_box(self): + """Rend la boite de dialogue lorsqu'un dialogue est lancé.""" + + # Rend le conteneur des dialogues + if self.engine.dialogs_manager.reading_dialog: + resized_box = transform.scale(self.dialogs_box, + (display.get_window_size()[0], + self.dialogs_box.get_height() / self.dialogs_box.get_width() * + display.get_window_size()[0])) + self.window.blit(resized_box, (0, display.get_window_size()[1] - resized_box.get_height())) + + # Rend le texte + + # On récupère le texte + sentence = self.engine.dialogs_manager.get_current_dialog_sentence() + + # On crée la font qui permettra de faire le rendu du texte après + text_font = font.SysFont("Arial", display.get_window_size()[0]//30) + + # On calcule la taille du décalage puis on calcule la largeur maximale que peut faire une ligne + x_border = display.get_window_size()[0]/30 + max_width = display.get_window_size()[0]-2*x_border + + # On passe le texte dans un algorithme qui coupe le texte entre les espaces pour empecher de dépacer la + # taille maximale de la ligne + lines = [] + current_line = "" + for i in sentence: + current_line += i + # Si on déplace de la ligne, on ajoute la ligne jusqu'au dernier mot + if text_font.size(current_line)[0] > max_width: + lines.append(current_line[:current_line.rfind(" ")]) + current_line = current_line[current_line.rfind(" "):] + + # Si la ligne est incomplète, on ajoute la ligne + lines.append(current_line) + + # On itère dans les lignes avec un enumerate pour avoir sont index + for i in enumerate(lines): + # On récupère le texte et s'il commence par un espace, on le retire + text = i[1] + if len(text) > 0 and text[0] == " ": + text = text[1:] + + # On rend la ligne au bon endroit sur l'écran + rendered_text = text_font.render(text, True, (0, 0, 0)) + self.window.blit(rendered_text, + (x_border, + display.get_window_size()[1] - resized_box.get_height() + + display.get_window_size()[0]/30 + + (text_font.get_height()+display.get_window_size()[0]/200)*i[0])) + + def render_debug_area(self, rendered_surface: surface.Surface): + """Rend les zones de collisions et de détections quand le mode DEBUG est activé.""" + + # On calcule le décalage pour centrer la caméra + x_middle_offset = display.get_window_size()[0] / 2 / self.engine.camera.zoom + y_middle_offset = display.get_window_size()[1] / 2 / self.engine.camera.zoom + + # On itère et on rend toutes les zones de détection + for area in self.engine.event_sheduler.area_callbacks: + area_rect = area[0] + draw.rect(rendered_surface, (200, 100, 0), + (math.floor(x_middle_offset + area_rect[0] - self.engine.camera.x), + math.floor(y_middle_offset + area_rect[1] - self.engine.camera.y), + math.floor(area_rect[2]), math.floor(area_rect[3])), width=1) + def register_shadow(self, file_path: str, name: str): """Enregistre une image d'ombre utilisée pour le rendu des entités.""" shadow = image.load(file_path).convert_alpha() @@ -169,8 +259,8 @@ class Renderer: frame = transform.scale(frame, (display.get_window_size()[0] / 5, display.get_window_size()[0] / 5)) # On colle le boss à droite de la fenêtre - self.window.blit(frame, (display.get_window_size()[0]-frame.get_width()-display.get_window_size()[0]/20, - display.get_window_size()[1]/4-frame.get_height()/2)) + self.window.blit(frame, (display.get_window_size()[0] - frame.get_width() - display.get_window_size()[0] / 20, + display.get_window_size()[1] / 4 - frame.get_height() / 2)) # On récupère l'image de l'animation du joueur player_animation = self.boss_fight_player_animations[self.engine.boss_fight_manager.current_player_animation] @@ -180,14 +270,17 @@ class Renderer: frame = transform.scale(frame, (display.get_window_size()[0] / 5, display.get_window_size()[0] / 5)) # On colle le joueur à gauche de la fenêtre - self.window.blit(frame, (display.get_window_size()[0]/20, display.get_window_size()[1]/4-frame.get_height()/2)) + self.window.blit(frame, + (display.get_window_size()[0] / 20, display.get_window_size()[1] / 4 - frame.get_height() / 2)) def render_boss_fight_gui(self): """Rend la barre d'action en bas de l'écran pendant le combat de boss.""" resized_container = transform.scale(self.boss_fight_GUI_container, - (display.get_window_size()[0], self.boss_fight_GUI_container.get_height()/self.boss_fight_GUI_container.get_width()*display.get_window_size()[0])) - self.window.blit(resized_container, (0, display.get_window_size()[1]-resized_container.get_height())) + (display.get_window_size()[0], + self.boss_fight_GUI_container.get_height() / self.boss_fight_GUI_container.get_width() * + display.get_window_size()[0])) + self.window.blit(resized_container, (0, display.get_window_size()[1] - resized_container.get_height())) def render_entities(self, rendered_surface: surface.Surface, gui_surface: surface.Surface, delta: float): """Rend toutes les entités.""" @@ -235,10 +328,11 @@ class Renderer: cooldown_value = entity.damage_cooldown / entity.default_damage_cooldown # On calcule où placer la barre de vei sur la surface des GUI - life_bar_dest = (math.floor((entity.x - self.engine.camera.x + x_middle_offset) * self.engine.camera.zoom - - life_bar_width / 2), - math.floor((entity.y - self.engine.camera.y + y_middle_offset - frame.get_height() / 2) * - self.engine.camera.zoom - life_bar_height - life_bar_y_offset)) + life_bar_dest = ( + math.floor((entity.x - self.engine.camera.x + x_middle_offset) * self.engine.camera.zoom - + life_bar_width / 2), + math.floor((entity.y - self.engine.camera.y + y_middle_offset - frame.get_height() / 2) * + self.engine.camera.zoom - life_bar_height - life_bar_y_offset)) # Contour de la barre de vie draw.rect(gui_surface, (20, 0, 0), (life_bar_dest[0] - life_bar_border, diff --git a/src/main.py b/src/main.py index 5287bd9..414b3a2 100644 --- a/src/main.py +++ b/src/main.py @@ -12,15 +12,22 @@ class Game(Engine): self.map_manager.load_new("maps/map5.tmj") self.renderer.load_tile_set("assets/textures/tileset.png", 16) + self.dialogs_manager.load_dialogs("assets/dialogs.json") self.create_player_entity() self.load_boss_fight_assets() self.spawn_mobs() - self.DEBUG_MODE = False + self.DEBUG_MODE = True self.game_state = GameState.NORMAL + self.event_sheduler.register_area((64, 64, 32, 32), lambda _: self.dialogs_manager.start_dialog("test"), ["player"], False, True) + + self.renderer.dialogs_box = pygame.image.load("assets/textures/GUI/dialogs_box.png").convert_alpha() + + self.event_handler.register_button_area((0, 0, 0.1, 0.1), lambda : print("salut"), 0) + def create_player_entity(self): """Crée une entité joueur.""" anim = Anim(0.5) @@ -61,7 +68,7 @@ class Game(Engine): mob.set_default_life(5) mob.max_speed = 1. - mob.x, mob.y = 160, 16 + mob.x, mob.y = 1600, 16 def load_boss_fight_assets(self): """Charge les animations de combat des combats de boss.""" @@ -72,7 +79,7 @@ class Game(Engine): boss_none.load_animation_from_directory("assets/textures/boss_fight/boss_sprite/test/none") self.renderer.register_boss_fight_boss_animation(boss_none, "none") - self.renderer.boss_fight_GUI_container = pygame.image.load("assets/textures/boss_fight/fight_actions_GUI.png") + self.renderer.boss_fight_GUI_container = pygame.image.load("assets/textures/boss_fight/fight_actions_GUI.png").convert_alpha() game = Game()