gtn/src/plugins/assets.py

262 lines
8.7 KiB
Python

"""
Un plugin qui gère les assets du jeu.
"""
import glob
import random
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/<name>`.
"""
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.files.extend(glob.glob("assets/global/**/*", recursive=True))
random.shuffle(self.files)
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()
asset_iterator = AssetIterator()
world.set(AssetIterator())
if asset_iterator.total <= 30:
for _ in range(asset_iterator.total):
asset_iterator.load_next(world)
else:
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]
prefix = (
"assets/global/"
if file.startswith("assets/global/")
else f"assets/{name}/"
)
ressource_name = file[len(prefix) : -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]
if asset_iterator.total == 0:
progress = 0.0
else:
file_loaded = asset_iterator.total - len(asset_iterator.files)
progress = file_loaded / asset_iterator.total
# Affichage de la barre de progression
for progress_bar in world.query(ProgessBar):
progress_bar[render.Sprite].area = (0, 0, progress, 1.0)
return Scene(
[AssetIterator.prepare_world],
[AssetIterator.load_next, ProgessBar.render],
[],
)