""" Un plugin qui gère les assets du jeu. """ import glob import pygame from engine import CurrentScene, GlobalPlugin, KeepAlive, Scene from engine.ecs import World from plugins import render class Assets(KeepAlive): """ Ressource qui gère les assets du jeu. """ def __init__(self): # Création de la texture d'erreur error_texture = pygame.Surface((256, 256)) error_texture.fill((0, 0, 0)) pygame.draw.rect(error_texture, (255, 0, 255), (0, 0, 128, 128)) pygame.draw.rect(error_texture, (255, 0, 255), (128, 128, 128, 128)) self.__error_texture = error_texture.convert() # Chragement du son d'erreur self.__error_sound = pygame.mixer.Sound("assets/error.mp3") # Chargement des textures de chargement self.__unloaded_texture = pygame.image.load("assets/unloaded.png").convert() self.__loaded_texture = pygame.image.load("assets/loaded.png").convert() # Cache des ressources self.__textures: dict[str, pygame.Surface] = {} self.__fonts: dict[int, pygame.font.Font] = {} self.__texts: dict[tuple[int, str], pygame.Surface] = {} self.__sounds: dict[str, pygame.mixer.Sound] = {} @property def error_texture(self) -> pygame.Surface: """ La texture d'erreur. Cette texture est utilisé lorsque la texture demandée n'existe pas. """ return self.__error_texture @property def error_sound(self) -> pygame.mixer.Sound: """ Le son d'erreur. Cette texture est utilisé lorsque le son demandé n'existe pas. """ return self.__error_sound @property def unloaded_texture(self) -> pygame.Surface: """ La texture de chargement qui s'affiche au début du chargement et qui est progressivement remplacé par la texture `loaded_texture`. """ return self.__unloaded_texture @property def loaded_texture(self) -> pygame.Surface: """ La texture de chargement qui s'affiche progressivement lors d'un chargement. """ return self.__loaded_texture def load_texture(self, name: str, path: str) -> pygame.Surface: """ Charge une texture et la renvoi. Si une texture existe déja dans le cache, elle sera remplacée par la nouvelle. """ surface = pygame.image.load(path).convert_alpha() self.__textures[name] = surface return surface def get_texture(self, name: str) -> pygame.Surface: """ Renvoie la texture demandée. Si la texture n'existe pas dans le cache, la texture d'erreur sera renvoyée. """ return self.__textures.get(name, self.__error_texture) def get_font(self, size: int) -> pygame.font.Font: """ Renvoie la police d'ecriture du jeu avec la taille demandée. Cette fonction charge le fichier `assets/font.ttf` pour la taille demandée et la met dans le cache. Si la police d'ecriture existe déjà dans le cache, elle sera renvoyée directement. """ font = self.__fonts.get(size) if font is None: font = self.__fonts[size] = pygame.font.Font("assets/font.ttf", size) return font def get_text(self, size: int, text: str, color: pygame.Color) -> pygame.Surface: """ Renvoie une image correspondant à la chaîne de caractères demandée avec la taille de police demandée et la couleur demandée. Si l'image du texte demandé n'est pas dans le cache, elle sera créer puis mis dans le cache et enfin renvoyée. """ surface = self.__texts.get((size, text)) if surface is None: surface = self.__texts[(size, text)] = self.get_font(size).render( text, True, color ) return surface def load_sound(self, name: str, path: str) -> pygame.mixer.Sound: """ Charge un son et le renvoi. Si un son existe déja dans le cache, il sera remplacé par le nouveau. """ sound = pygame.mixer.Sound(path) self.__sounds[name] = sound return sound def get_sound(self, name: str) -> pygame.mixer.Sound: """ Renvoie le son demandé. Si le son n'existe pas dans le cache, le son d'erreur sera renvoyé. """ return self.__sounds.get(name, self.__error_sound) def clear_cache(self): """ Vide le cache des assets. Les fonts ne sont pas effacés car ils n'y a normalement pas énormément de taille de police différentes utilisées. """ self.__textures.clear() self.__texts.clear() self.__sounds.clear() def __initialize(world: World): """ Ajoute la ressource `Assets` au monde. """ world.set(Assets()) PLUGIN = GlobalPlugin( [__initialize], [], [], [], ) def loading_scene(target: Scene, name: str, clear_cache: bool = True): """ Retourne une scène de chargement des assets qui passe à la scène donné en paramètres lorsque tous les assets de la scène sont chargées. Paramètres: - `target`: la scène qui sera lancé après le chargement des assets. - `name`: le nom de la scène, ce nom est utilisé pour savoir dans quel dossier sont les assets de la scène. Les assets de la scène seront récupéré dans le dossier `assets/`. """ class AssetIterator: """ Une ressource qui contient un itérateur sur les fichiers des assets de la scène à charger. """ def __init__(self): self.files = glob.glob(f"assets/{name}/**/*", recursive=True) self.total = len(self.files) @staticmethod def prepare_world(world: World): """ Retire toutes les ressource précédentes du monde puis ajoute `ResourceIterator` et la barre de progression dans le monde. """ assets = world[Assets] if clear_cache: assets.clear_cache() world.set(AssetIterator()) world.new_entity().set( render.Sprite(assets.unloaded_texture, order=1000000000) ) world.new_entity().set( ProgessBar(), render.Sprite( assets.loaded_texture, order=1000000001, area=(0, 0, 0, render.HEIGHT), ), ) @staticmethod def load_next(world: World): """ Charge le fichier suivant de l'itérateur. """ assets = world[Assets] asset_iterator = world[AssetIterator] if len(asset_iterator.files) == 0: world[CurrentScene] = target else: file = asset_iterator.files.pop().replace("\\", "/") ressource_extension = file.split(".")[-1] ressource_name = "/".join(file.split("/")[2:])[ : -len(ressource_extension) - 1 ] if ressource_extension in ("png", "jpg"): assets.load_texture(ressource_name, file) if ressource_extension in ("mp3", "wav", "ogg"): assets.load_sound(ressource_name, file) class ProgessBar: """ Composant marquant une entité comme étant une barre de progression. """ @staticmethod def render(world: World): """ Affiche une barre de progression du chargement des assets. """ # Calcul du pourcentage de chargement asset_iterator = world[AssetIterator] file_loaded = asset_iterator.total - len(asset_iterator.files) progress = file_loaded / asset_iterator.total pixels = int(render.WIDTH * progress) # Affichage de la barre de progression progress_bar = world.query(ProgessBar).pop() progress_bar[render.Sprite].area = (0, 0, pixels, render.HEIGHT) return Scene( [AssetIterator.prepare_world], [AssetIterator.load_next, ProgessBar.render], [], )