diff --git a/src/engine.py b/src/engine.py index 6085a33..42d3529 100644 --- a/src/engine.py +++ b/src/engine.py @@ -4,9 +4,11 @@ Un moteur de jeu inspiré de bevy. import glob +import json import math import os from typing import Callable, Optional, Sequence, SupportsFloat, TypeVar, Union +from time import time import pygame @@ -139,8 +141,8 @@ class Vec2: def __add__(self, other: object) -> "Vec2": if isinstance(other, Vec2): return Vec2(self.x + other.x, self.y + other.y) - elif isinstance(other, float): - return Vec2(self.x + other, self.y + other) + elif isinstance(other, SupportsFloat): + return Vec2(self.x + float(other), self.y + float(other)) raise ValueError( f"Unsupported operand type(s) for +: 'Vec2' and '{type(other)}'" ) @@ -148,8 +150,8 @@ class Vec2: def __sub__(self, other: object) -> "Vec2": if isinstance(other, Vec2): return Vec2(self.x - other.x, self.y - other.y) - elif isinstance(other, float): - return Vec2(self.x - other, self.y - other) + elif isinstance(other, SupportsFloat): + return Vec2(self.x - float(other), self.y - float(other)) raise ValueError( f"Unsupported operand type(s) for -: 'Vec2' and '{type(other)}'" ) @@ -157,8 +159,8 @@ class Vec2: def __mul__(self, other: object) -> "Vec2": if isinstance(other, Vec2): return Vec2(self.x * other.x, self.y * other.y) - elif isinstance(other, float): - return Vec2(self.x * other, self.y * other) + elif isinstance(other, SupportsFloat): + return Vec2(self.x * float(other), self.y * float(other)) raise ValueError( f"Unsupported operand type(s) for *: 'Vec2' and '{type(other)}'" ) @@ -166,8 +168,8 @@ class Vec2: def __truediv__(self, other: object) -> "Vec2": if isinstance(other, Vec2): return Vec2(self.x / other.x, self.y / other.y) - elif isinstance(other, float): - return Vec2(self.x / other, self.y / other) + elif isinstance(other, SupportsFloat): + return Vec2(self.x / float(other), self.y / float(other)) raise ValueError( f"Unsupported operand type(s) for /: 'Vec2' and '{type(other)}'" ) @@ -202,34 +204,9 @@ class Vec2: return f"Vec2({self.x}, {self.y})" -class Game: - """ - Un jeu. - """ - - def __init__(self): - self.stop_requested = False - self.next_scene: Optional[str] = None - - def stop(self): - """ - Demande l'arrêt du jeu. - """ - self.stop_requested = True - - def change_scene(self, scene_name: str): - """ - Demande un changement de scène. - - Paramètres: - scene_name: Le nom de la scène dans laquelle aller. - """ - self.next_scene = scene_name - - class Display: """ - La fenêtre d'un jeu. + Une classe utilitaire pour la gestion de la fenêtre du jeu. """ WIDTH = 1440.0 @@ -254,6 +231,120 @@ class Display: return rect +class Game: + """ + Un ressource qui représente le jeu actuel. + """ + + def __init__(self): + self.stop_requested = False + self.next_scene: Optional[str] = None + + def stop(self): + """ + Demande l'arrêt du jeu. + """ + self.stop_requested = True + + def change_scene(self, scene_name: str): + """ + Demande un changement de scène. + + Paramètres: + scene_name: Le nom de la scène dans laquelle aller. + """ + self.next_scene = scene_name + + +class Assets: + """ + Resource qui permet la gestion des assets. + """ + + def __init__(self, surface: pygame.Surface): + # 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(surface) + + # Chargement des textures + self.__textures: dict[str, pygame.Surface] = {} + for file in glob.iglob("assets/textures/**/*.png", recursive=True): + self.__textures[file[16:].replace("\\", "/")] = pygame.image.load( + file + ).convert_alpha(surface) + + # Création du cache pour les polices + self.__fonts: dict[int, pygame.font.Font] = {} + + def get_texture(self, name: str) -> pygame.Surface: + """ + Renvoie la texture qui correspond au nom *name*. + + Paramètres: + name: Le nom de la texture. + + Retourne: + La texture qui correspond au nom *name*. + """ + return self.__textures.get(name, self.__error_texture) + + def get_texture_size(self, name: str) -> Vec2: + """ + Renvoie la taille de la texture qui correspond au nom *name*. + + Paramètres: + name: Le nom de la texture. + + Retourne: + La taille de la texture qui correspond au nom *name*. + """ + return Vec2(*self.get_texture(name).get_size()) + + def get_font(self, size: int) -> pygame.font.Font: + """ + Renvoie la police qui correspond à la taille *size*. + + Paramètres: + size: La taille de la police. + + Retourne: + La police qui correspond à la taille *size*. + """ + font = self.__fonts.get(size) + if font is None: + font = pygame.font.Font("assets/font.ttf", size) + self.__fonts[size] = font + return font + + def get_text_size(self, text: str, size: int) -> Vec2: + """ + Renvoie la taille d'un texte avec une certaine taille de police. + + Paramètres: + text: Le texte. + size: La taille de la police. + + Retourne: + La taille d'un texte avec une certaine taille de police. + """ + return Vec2(*self.get_font(size).size(text)) + + +class Time(float): + """ + Ressource qui represente le temps depuis 1900. + """ + + +class Delta(float): + """ + Ressource qui represente le temps depuis la dernière frame. + """ + + class Keyboard: """ Ressource qui représente les entrées utilisateurs sur le clavier à la frame actuelle. @@ -355,6 +446,12 @@ class Position(Vec2): """ +class Centered: + """ + Composant permettant de dire que l'affichage de l'entité doit être centré. + """ + + class Offset(Vec2): """ Composant qui represente un décalage de la position d'une entité. @@ -373,6 +470,38 @@ class Texture(str): """ +class HoveredTexture(Texture): + """ + Composant qui represente la texture lorsque l'entité est survolée. + """ + + +class Animation: + """ + Composant qui représente une animation. + """ + + def __init__( + self, + name: str, + callback: Callable[[World, Entity], object] = lambda _w, _e: None, + ): + self.name = name + self.callback = callback + + with open( + f"assets/textures/animations/{name}/info.json", + "r", + encoding="utf-8", + ) as f: + info = json.load(f) + self.end_image: str = info.get("end_image", "") + self.offset = Offset(info["offset"]["x"], info["offset"]["y"]) + self.frame_count: int = info["frame_count"] + self.fps: int = info["fps"] + self.time = 0.0 + + class Text(str): """ Composant qui represente un texte. @@ -391,6 +520,33 @@ class Color(pygame.Color): """ +class HoverEnter: + """ + Composant qui marque un entité comme commencée à être survolée. + """ + + +class Hovered: + """ + Composant qui marque un entité comme survolée. + """ + + +class HoverExit: + """ + Composant qui marque un entité comme arreté d'être survolée. + """ + + +class Clickable: + """ + Composant qui marque un entité comme pouvant etre cliquer. + """ + + def __init__(self, callback: Callable[[World, Entity], object]): + self.callback = callback + + class Scene: """ Une scène dans le jeu. @@ -433,22 +589,8 @@ def start_game( keyboard = Keyboard() mouse = Mouse() - # 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)) - error_texture = error_texture.convert(surface) - - # Chargement des textures - textures: dict[str, pygame.Surface] = {} - for file in glob.iglob("textures/**/*.png", recursive=True): - textures[file[9:].replace("\\", "/")] = pygame.image.load(file).convert_alpha( - surface - ) - - # Création du cache pour les polices - fonts: dict[int, pygame.font.Font] = {} + # Chargements des assets + assets = Assets(surface) # On récupère la première scène scene = scenes.get(start_scene) @@ -458,8 +600,10 @@ def start_game( # Initialisation du monde world = World() world[Game] = Game() + world[Assets] = assets world[Keyboard] = keyboard world[Mouse] = mouse + world[Time] = Time(time()) # Initialisation de la scène for system in scene.init_systems: @@ -500,35 +644,111 @@ def start_game( ((event.pos[1] - rect[1]) / rect[3]) * Display.HEIGHT, ) + # On vérifie le survol des textures et textes + for entity in world.query(Position, Texture): + # Récupération de la position et taille de l'entité + position: Vec2 = entity[Position] + if Offset in entity: + position = position + entity[Offset] + size = assets.get_texture_size(entity[Texture]) + if Centered in entity: + position -= size / 2 + + # On détermine si la souris est sur l'entité + if ( + mouse.position.x >= position.x + and mouse.position.x <= position.x + size.x + and mouse.position.y >= position.y + and mouse.position.y <= position.y + size.y + ): + if Hovered not in entity: + entity[HoverEnter] = HoverEnter() + else: + del entity[HoverEnter] + entity[Hovered] = Hovered() + else: + if Hovered in entity: + entity[HoverExit] = HoverExit() + else: + del entity[HoverExit] + del entity[Hovered] + + # On met à jour le temps et delta + now = time() + world[Delta] = Delta(now - world[Time]) + world[Time] = Time(now) + + # On execute les objets clickables + if mouse.is_button_pressed(1): + for entity in world.query(Clickable, Hovered): + entity[Clickable].callback(world, entity) + # On met à jour la scène for system in scene.update_systems: system(world) + # Mise à jour des animations + for entity in world.query(Animation): + animation = entity[Animation] + if animation.time == 0: + if animation.end_image == "" and Texture in entity: + animation.end_image = entity[Texture] + animation.time += world[Delta] + frame_index = int(animation.time * animation.fps) + if frame_index >= animation.frame_count: + entity[Texture] = Texture(animation.end_image) + del entity[Animation] + del entity[Offset] + animation.callback(world, entity) + else: + entity[Offset] = Offset(animation.offset.x, animation.offset.y) + entity[Texture] = Texture( + f"animations/{animation.name}/{frame_index:04}.png" + ) + # Rendu du monde - entities = sorted(world.query(Order, Position), key=lambda e: e[Order]) - for entity in entities: + for entity in sorted(world.query(Order, Position), key=lambda e: e[Order]): # Récupération de la position de l'entité position: Vec2 = entity[Position] if Offset in entity: position = position + entity[Offset] + centered = Centered in entity # Affichage de la texture if Texture in entity: + if HoveredTexture in entity and Hovered in entity: + texture = assets.get_texture(entity[HoveredTexture]) + else: + texture = assets.get_texture(entity[Texture]) surface.blit( - textures.get(entity[Texture], error_texture), - (position.x, position.y), + texture, + ( + position.x - texture.get_width() / 2 + if centered + else position.x, + position.y - texture.get_height() / 2 + if centered + else position.y, + ), ) # Affichage des textes - if Text in entity and TextSize in entity: + if Text in entity: color = entity[Color] if Color in entity else Color(255, 255, 255) - size = entity[TextSize] - font = fonts.get(size) - if font is None: - font = pygame.font.Font("font.ttf", size) - fonts[size] = font + size = entity[TextSize] if TextSize in entity else 50 + font = assets.get_font(size) font_surface = font.render(entity[Text], True, color) - surface.blit(font_surface, (position.x, position.y)) + surface.blit( + font_surface, + ( + position.x - font_surface.get_width() / 2 + if centered + else position.x, + position.y - font_surface.get_height() / 2 + if centered + else position.y, + ), + ) # Mise a jour de la fenêtre rect = Display._calculate_surface_rect() diff --git a/src/main.py b/src/main.py index 3dfccaf..3f11059 100644 --- a/src/main.py +++ b/src/main.py @@ -2,11 +2,39 @@ Example de l'utilisation du moteur de jeu. """ -from engine import Scene, start_game +from engine import ( + Centered, + Clickable, + Display, + Entity, + Game, + HoveredTexture, + Order, + Position, + Scene, + Texture, + World, + start_game, +) + + +def initialize_world(world: World): + """ + Initialise le monde. + """ + Entity( + world, + Position(Display.WIDTH / 2, Display.HEIGHT / 2), + Order(0), + Centered(), + Texture("button_classique.png"), + HoveredTexture("button_classique_hover.png"), + Clickable(lambda w, e: w[Game].change_scene("test")), + ) start_game( - {"menu": Scene([], [], [])}, - "menu", + {"example": Scene([initialize_world], [], []), "test": Scene([], [], [])}, + "example", title="Guess The Number", )