import math from pygame import display, image, surface, transform, draw 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 class Renderer: """Classe contenant le moteur de rendu. On utilise, pour cela la bibliothèque Pygame.""" def __init__(self, core: 'engine.Engine'): self.engine = core self.window_type = FULLSCREEN 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 self.animations: dict[str: Anim] = {} # Variables utilisées pour les combats de boss self.boss_fight_boss_animations: dict[str: Anim] = {} self.boss_fight_player_animations: dict[str: Anim] = {} self.boss_fight_GUI_container = None # Variables utilisées par le menu principal self.main_menu_assets: dict[str: Anim] = {} 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() self.tile_size = tile_size # Scan tout le tile set et le découpe pour créer des tiles de {tile_size} px de hauteur et de largeur for y in range(tile_set.get_height() // tile_size): for x in range(tile_set.get_width() // tile_size): tile = tile_set.subsurface((x * tile_size, y * tile_size, tile_size, tile_size)) self.tiles.append(tile) def update(self, delta: float): """Fait le rendu du jeu.""" self.window.fill((255, 255, 255)) 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) self.render_layer(2, rendered_surface) # Enfin, on redimensionne notre surface et on la colle sur la fenêtre principale self.window.blit( transform.scale(rendered_surface, (math.ceil(rendered_surface_size[0] * self.engine.camera.zoom), 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)) self.render_boss_fight_scene(delta) self.render_boss_fight_gui() # Apres avoir tout rendu, on met à jour l'écran display.update() def register_animation(self, animation: Anim, name: str): """Enregistre une animation.""" self.animations[name] = animation def register_boss_fight_boss_animation(self, animation: Anim, name: str): """Ajoute une animation pour le boss lors d'un combat de boss.""" self.boss_fight_boss_animations[name] = animation def register_boss_fight_player_animation(self, animation: Anim, name: str): """Ajoute une animation pour le joueur lors d'un combat de boss.""" self.boss_fight_player_animations[name] = animation def render_boss_fight_scene(self, delta: float): """Rend les sprites du joueur et du boss lors d'un combat de boss.""" # On récupère l'image de l'animation du boss boss_animation: Anim = self.boss_fight_boss_animations[self.engine.boss_fight_manager.current_boss_animation] frame = boss_animation.get_frame(delta) # On redimensionne l'image 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)) # 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] frame = player_animation.get_frame(delta) # On redimensionne l'image 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)) 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())) def render_entities(self, rendered_surface: surface.Surface, gui_surface: surface.Surface, delta: float): """Rend toutes les entités.""" # 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 for entity in self.engine.entity_manager.get_all_entities(): # On récupère la frame courante de l'animation anim: Anim = self.animations[entity.animation_name] frame = anim.get_frame(delta) # On flip l'image horizontalement si l'entité est retournée if entity.direction == 1: frame = transform.flip(frame, True, False) # Si l'entité n'apparait pas à l'écran, on passe son rendu if (entity.x - self.engine.camera.x + x_middle_offset + frame.get_width() < 0 or entity.x - self.engine.camera.x - x_middle_offset - frame.get_width() > 0 or entity.y - self.engine.camera.y + y_middle_offset + frame.get_height() < 0 or entity.y - self.engine.camera.y - y_middle_offset - frame.get_height() > 0): continue # On calcule les coordonnées de rendu de l'entité entity_dest = (math.floor(entity.x - self.engine.camera.x + x_middle_offset - frame.get_width() / 2), math.floor(entity.y - self.engine.camera.y + y_middle_offset - frame.get_height() / 2)) # On affiche l'image rendered_surface.blit(frame, entity_dest) if entity.max_life_points != -1: # Rendu de la barre de vie des entités life_bar_width = 50 life_bar_height = 8 life_bar_y_offset = 5 life_bar_border = 2 life_bar_value = entity.life_points / entity.max_life_points 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)) # Contour de la barre de vie draw.rect(gui_surface, (20, 0, 0), (life_bar_dest[0] - life_bar_border, life_bar_dest[1] - life_bar_border, life_bar_width + life_bar_border * 2, life_bar_height + life_bar_border * 2)) # Barre de vie draw.rect(gui_surface, (255 - 255 * life_bar_value, 255 * life_bar_value, 0), life_bar_dest + (life_bar_width * life_bar_value, life_bar_height)) draw.rect(gui_surface, (200, 200, 200), life_bar_dest + (life_bar_width * life_bar_value * cooldown_value, life_bar_height)) if self.engine.DEBUG_MODE: top_let_corner_x = entity.x - self.engine.camera.x + x_middle_offset top_let_corner_y = entity.y - self.engine.camera.y + y_middle_offset draw.rect(rendered_surface, (255, 0, 0), (top_let_corner_x + entity.collision_rect[0], top_let_corner_y + entity.collision_rect[1], entity.collision_rect[2] - entity.collision_rect[0], 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 x_map_range = int(display.get_window_size()[0] / self.tile_size / self.engine.camera.zoom) + 2 y_map_range = int(display.get_window_size()[1] / self.tile_size / self.engine.camera.zoom) + 2 # 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 calcule le décalage du début de rendu des tiles x_map_offset = math.floor((self.engine.camera.x - x_middle_offset) / self.tile_size) y_map_offset = math.floor((self.engine.camera.y - y_middle_offset) / self.tile_size) # On itère pour chaque couche, toutes les tiles visibles par la caméra for x in range(x_map_offset, x_map_offset + x_map_range): for y in range(y_map_offset, y_map_offset + y_map_range): # On récupère l'id de la tile à la position donnée tile_id = self.engine.map_manager.get_tile_at(x, y, layer_id) # Si l'id est 0, il s'agit de vide donc on saute le rendu if tile_id == 0: continue # Puis, on cherche à quelle image elle correspond et on la colle sur notre surface rendered_surface.blit(self.tiles[tile_id - 1], (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))) if self.engine.DEBUG_MODE and layer_id == 1: draw.rect(rendered_surface, (100, 100, 255), (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)