ecs #58
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -9,13 +9,10 @@
|
|||
"python.analysis.inlayHints.functionReturnTypes": true,
|
||||
"python.analysis.inlayHints.pytestParameters": true,
|
||||
"python.analysis.typeCheckingMode": "strict",
|
||||
"python.analysis.diagnosticSeverityOverrides": {
|
||||
"reportPrivateUsage": "none"
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"pylint.args": [
|
||||
"--disable=broad-exception-caught,protected-access,undefined-variable,import-error,unused-import,no-member",
|
||||
"--disable=protected-access,undefined-variable,import-error,unused-import,no-member",
|
||||
],
|
||||
}
|
857
src/engine.py
857
src/engine.py
|
@ -1,614 +1,16 @@
|
|||
"""
|
||||
Un moteur de jeu inspiré de bevy.
|
||||
Permet de lancer un jeu et de gérer la boucle principale de celui-ci.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from typing import Callable, Optional, Sequence, SupportsFloat, TypeVar, Union
|
||||
from time import time
|
||||
import pygame
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class World:
|
||||
"""
|
||||
Un monde contenant toutes les données du jeu.
|
||||
"""
|
||||
|
||||
__T = TypeVar("__T")
|
||||
"""
|
||||
Permet d'indiquer le type d'un composant ou d'une ressource.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Constructeur d'un nouveau monde vide.
|
||||
"""
|
||||
self._entities: dict[type[object], set[Entity]] = {}
|
||||
self.__resources: dict[type[object], object] = {}
|
||||
|
||||
def create_entity(self, *components: object) -> "Entity":
|
||||
"""
|
||||
Crée une nouvelle entité avec les composants donnés et l'ajoute au monde.
|
||||
|
||||
Paramètres:
|
||||
*components: les composants de l'entité.
|
||||
|
||||
Retourne:
|
||||
L'entité.
|
||||
"""
|
||||
return Entity(self, *components)
|
||||
|
||||
def __setitem__(self, resource_type: type[__T], resource: __T):
|
||||
if resource_type != type(resource):
|
||||
raise TypeError()
|
||||
self.__resources[resource_type] = resource
|
||||
|
||||
def __getitem__(self, resource_type: type[__T]) -> __T:
|
||||
return self.__resources[resource_type] # type: ignore
|
||||
|
||||
def __contains__(self, resource_type: type[object]) -> bool:
|
||||
return resource_type in self.__resources
|
||||
|
||||
def __delitem__(self, resource_type: type[object]):
|
||||
del self.__resources[resource_type]
|
||||
|
||||
def query(
|
||||
self, *component_types: type[object], without: Sequence[type[object]] = ()
|
||||
) -> set["Entity"]:
|
||||
"""
|
||||
Renvoie les entités qui ont tous les composants de type *component_types
|
||||
et qui n'ont aucun des composants de type *without.
|
||||
|
||||
Paramètres:
|
||||
*component_types: les types de composants des entités a renvoyer.
|
||||
without: les types de composants des entités a exclure.
|
||||
|
||||
Retourne:
|
||||
Les entités qui correspondent aux critères.
|
||||
"""
|
||||
if len(component_types) == 0:
|
||||
return set()
|
||||
entities = set(self._entities.get(component_types[0], set()))
|
||||
for component_type in component_types[1:]:
|
||||
entities.intersection_update(self._entities.get(component_type, set()))
|
||||
for component_type in without:
|
||||
entities.difference_update(self._entities.get(component_type, set()))
|
||||
return entities
|
||||
|
||||
|
||||
class Entity:
|
||||
"""
|
||||
Une entité du monde. Elle contient des composants.
|
||||
"""
|
||||
|
||||
__T = TypeVar("__T")
|
||||
"""
|
||||
Permet d'indiquer le type d'un composant.
|
||||
"""
|
||||
|
||||
def __init__(self, world: World, *components: object):
|
||||
"""
|
||||
Crée une nouvelle entité avec les composants donnés et l'ajoute au monde.
|
||||
|
||||
Paramètres:
|
||||
world: le monde dans lequel ajouter l'entité
|
||||
*components: les composants à ajouter à l'entité.
|
||||
"""
|
||||
self.__world = world
|
||||
self.__components: dict[type[object], object] = {}
|
||||
for component in components:
|
||||
self[type(component)] = component
|
||||
|
||||
def __setitem__(self, component_type: type[__T], component: __T):
|
||||
if component_type != type(component):
|
||||
raise TypeError()
|
||||
self.__components[component_type] = component
|
||||
self.__world._entities.setdefault(component_type, set()).add(self)
|
||||
|
||||
def __getitem__(self, component_type: type[__T]) -> __T:
|
||||
return self.__components[component_type] # type: ignore
|
||||
|
||||
def __contains__(self, component_type: type[object]) -> bool:
|
||||
return component_type in self.__components
|
||||
|
||||
def __delitem__(self, component_type: type[object]):
|
||||
if self.__components.pop(component_type, None) is not None:
|
||||
self.__world._entities[component_type].remove(self)
|
||||
if len(self.__world._entities[component_type]) == 0:
|
||||
del self.__world._entities[component_type]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Entity({id(self)})"
|
||||
|
||||
|
||||
class Vec2:
|
||||
"""
|
||||
Un vecteur 2D
|
||||
"""
|
||||
|
||||
def __init__(self, *args: Union[SupportsFloat, "Vec2"]):
|
||||
if (
|
||||
len(args) == 2
|
||||
and isinstance(args[0], SupportsFloat)
|
||||
and isinstance(args[1], SupportsFloat)
|
||||
):
|
||||
self.x = float(args[0])
|
||||
self.y = float(args[1])
|
||||
elif len(args) == 1:
|
||||
if isinstance(args[0], Vec2):
|
||||
self.x = args[0].x
|
||||
self.y = args[0].y
|
||||
else:
|
||||
self.x = float(args[0])
|
||||
self.y = float(args[0])
|
||||
elif len(args) == 0:
|
||||
self.x = 0.0
|
||||
self.y = 0.0
|
||||
else:
|
||||
raise ValueError("Invalid number of arguments")
|
||||
|
||||
def __add__(self, other: object) -> "Vec2":
|
||||
if isinstance(other, Vec2):
|
||||
return Vec2(self.x + other.x, self.y + other.y)
|
||||
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)}'"
|
||||
)
|
||||
|
||||
def __sub__(self, other: object) -> "Vec2":
|
||||
if isinstance(other, Vec2):
|
||||
return Vec2(self.x - other.x, self.y - other.y)
|
||||
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)}'"
|
||||
)
|
||||
|
||||
def __mul__(self, other: object) -> "Vec2":
|
||||
if isinstance(other, Vec2):
|
||||
return Vec2(self.x * other.x, self.y * other.y)
|
||||
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)}'"
|
||||
)
|
||||
|
||||
def __truediv__(self, other: object) -> "Vec2":
|
||||
if isinstance(other, Vec2):
|
||||
return Vec2(self.x / other.x, self.y / other.y)
|
||||
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)}'"
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, Vec2):
|
||||
return self.x == other.x and self.y == other.y
|
||||
return False
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.x, self.y))
|
||||
|
||||
def __neg__(self) -> "Vec2":
|
||||
return Vec2(-self.x, -self.y)
|
||||
|
||||
@property
|
||||
def length(self) -> float:
|
||||
"""
|
||||
Retourne la longueur du vecteur.
|
||||
"""
|
||||
return math.sqrt(self.x**2 + self.y**2)
|
||||
|
||||
@property
|
||||
def normalized(self) -> "Vec2":
|
||||
"""
|
||||
Retourne une version normalisé du vecteur.
|
||||
"""
|
||||
length = self.length
|
||||
return Vec2(self.x / length, self.y / length)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Vec2({self.x}, {self.y})"
|
||||
|
||||
|
||||
class Display:
|
||||
"""
|
||||
Une classe utilitaire pour la gestion de la fenêtre du jeu.
|
||||
"""
|
||||
|
||||
WIDTH = 1440.0
|
||||
HEIGHT = 1080.0
|
||||
RATIO = WIDTH / HEIGHT
|
||||
INVERT_RATIO = HEIGHT / WIDTH
|
||||
|
||||
@staticmethod
|
||||
def _calculate_surface_rect() -> tuple[float, float, float, float]:
|
||||
"""
|
||||
Calcule et renvoit le rectangle de la surface dans la fenêtre du jeu.
|
||||
"""
|
||||
width, height = pygame.display.get_surface().get_size()
|
||||
if width / height < Display.RATIO:
|
||||
target_height = width * (Display.INVERT_RATIO)
|
||||
offset = (height - target_height) / 2
|
||||
rect = (0.0, offset, float(width), target_height)
|
||||
else:
|
||||
target_width = height * (Display.RATIO)
|
||||
offset = (width - target_width) / 2
|
||||
rect = (offset, 0.0, target_width, float(height))
|
||||
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)
|
||||
|
||||
# Cache des ressources
|
||||
self.__textures: dict[str, pygame.Surface] = {}
|
||||
self.__fonts: dict[int, pygame.font.Font] = {}
|
||||
self.__sounds: dict[str, pygame.mixer.Sound] = {}
|
||||
|
||||
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*.
|
||||
"""
|
||||
texture = self.__textures.get(name)
|
||||
if texture is None:
|
||||
if os.path.exists(f"assets/textures/{name}"):
|
||||
texture = pygame.image.load(f"assets/textures/{name}")
|
||||
if not name.startswith("animations/"):
|
||||
texture = texture.convert_alpha()
|
||||
self.__textures[name] = texture
|
||||
return texture
|
||||
return self.__error_texture
|
||||
return 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))
|
||||
|
||||
def get_sound(self, name: str) -> pygame.mixer.Sound:
|
||||
"""
|
||||
Renvoie le son qui correspond au nom *name*.
|
||||
|
||||
Paramètres:
|
||||
name: Le nom du son.
|
||||
|
||||
Retourne:
|
||||
Le son qui correspond au nom *name*.
|
||||
"""
|
||||
sound = self.__sounds.get(name)
|
||||
if sound is None:
|
||||
sound = pygame.mixer.Sound(f"assets/sounds/{name}")
|
||||
self.__sounds[name] = sound
|
||||
return sound
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.keys: set[str] = set()
|
||||
self.pressed: set[str] = set()
|
||||
self.released: set[str] = set()
|
||||
|
||||
def is_key_pressed(self, key_name: str) -> bool:
|
||||
"""
|
||||
Renvoie True si la touche *key_name* a commencé a être appuyée pendant la frame actuelle.
|
||||
|
||||
Paramètres:
|
||||
key_name: Le nom de la touche à tester.
|
||||
|
||||
Retourne:
|
||||
True si la touche *key_name* a commencé a être appuyée pendant la frame actuelle.
|
||||
"""
|
||||
return key_name in self.pressed
|
||||
|
||||
def is_key(self, key_name: str) -> bool:
|
||||
"""
|
||||
Renvoie True si la touche *key_name* est actuellement appuyée.
|
||||
|
||||
Paramètres:
|
||||
key_name: Le nom de la touche à tester.
|
||||
|
||||
Retourne:
|
||||
True si la touche *key_name* est actuellement appuyée.
|
||||
"""
|
||||
return key_name in self.keys
|
||||
|
||||
def is_key_released(self, key_name: str) -> bool:
|
||||
"""
|
||||
Renvoie True si la touche *key_name* a été relachée pendant la frame actuelle.
|
||||
|
||||
Paramètres:
|
||||
key_name: Le nom de la touche à tester.
|
||||
|
||||
Retourne:
|
||||
True si la touche *key_name* a été relachée pendant la frame actuelle.
|
||||
"""
|
||||
return key_name in self.released
|
||||
|
||||
|
||||
class Mouse:
|
||||
"""
|
||||
Ressource qui représente l'état de la souris à la frame actuelle.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.buttons: set[int] = set()
|
||||
self.pressed: set[int] = set()
|
||||
self.released: set[int] = set()
|
||||
self.position: Vec2 = Vec2(0.0, 0.0)
|
||||
self.delta: Vec2 = Vec2(0.0, 0.0)
|
||||
|
||||
def is_button_pressed(self, button: int) -> bool:
|
||||
"""
|
||||
Renvoie True si le bouton *button* a commencé a être appuyée pendant la frame actuelle.
|
||||
|
||||
Paramètres:
|
||||
button: Le numéro du bouton à tester.
|
||||
|
||||
Retourne:
|
||||
True si le bouton *button* a commencé a être appuyée pendant la frame actuelle.
|
||||
"""
|
||||
return button in self.pressed
|
||||
|
||||
def is_button(self, button: int) -> bool:
|
||||
"""
|
||||
Renvoie True si le bouton *button* est actuellement appuyé.
|
||||
|
||||
Paramètres:
|
||||
button: Le numéro du bouton à tester.
|
||||
|
||||
Retourne:
|
||||
True si le bouton *button* est actuellement appuyé.
|
||||
"""
|
||||
return button in self.buttons
|
||||
|
||||
def is_button_released(self, button: int) -> bool:
|
||||
"""
|
||||
Renvoie True si le bouton *button* a été relaché pendant la frame actuelle.
|
||||
|
||||
Paramètres:
|
||||
button: Le numéro du bouton à tester.
|
||||
|
||||
Retourne:
|
||||
True si le bouton *button* aLongrightarrow relaché pendant la frame actuelle.
|
||||
"""
|
||||
return button in self.released
|
||||
|
||||
|
||||
class Position(Vec2):
|
||||
"""
|
||||
Composant qui représente la position d'une entité.
|
||||
"""
|
||||
|
||||
|
||||
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é.
|
||||
"""
|
||||
|
||||
|
||||
class Order(int):
|
||||
"""
|
||||
Composant qui represente l'ordre d'affichage d'une entité.
|
||||
"""
|
||||
|
||||
|
||||
class Texture(str):
|
||||
"""
|
||||
Composant qui rerpésente la texture d'une entité.
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class TextSize(int):
|
||||
"""
|
||||
Composant qui represente la taille d'un texte.
|
||||
"""
|
||||
|
||||
|
||||
class Color(pygame.Color):
|
||||
"""
|
||||
Composant qui represente la couleur d'une entité.
|
||||
"""
|
||||
|
||||
|
||||
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 Sound:
|
||||
"""
|
||||
Composant qui une entité emettrant un son.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
volume: float = 0.5,
|
||||
loop: bool = False,
|
||||
callback: Callable[[World, Entity], object] = lambda _w, _e: None,
|
||||
stop_on_remove: bool = False,
|
||||
) -> None:
|
||||
self.name = name
|
||||
|
||||
if os.path.isdir(f"assets/sounds/{name}"):
|
||||
list_files = os.listdir(f"assets/sounds/{name}")
|
||||
random_file = random.choice(list_files)
|
||||
self.name = f"{name}/{random_file}"
|
||||
|
||||
self.volume = volume
|
||||
self.loop = loop
|
||||
self.callback = callback
|
||||
self.stop_on_remove = stop_on_remove
|
||||
from ecs import World
|
||||
|
||||
|
||||
class Scene:
|
||||
"""
|
||||
Une scène dans le jeu.
|
||||
Une scène du jeu.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -629,241 +31,52 @@ class Scene:
|
|||
)
|
||||
|
||||
|
||||
def start_game(
|
||||
scenes: dict[str, Scene],
|
||||
start_scene: str,
|
||||
*,
|
||||
title: str = "Game",
|
||||
) -> None:
|
||||
class CurrentScene(str):
|
||||
"""
|
||||
Lance le moteur de jeu.
|
||||
Resource qui permet de savoir et de changer la scène actuelle.
|
||||
"""
|
||||
# Initialisation de pygame
|
||||
pygame.init()
|
||||
if os.path.exists("icon.png"):
|
||||
pygame.display.set_icon(pygame.image.load("icon.png"))
|
||||
pygame.display.set_caption(title)
|
||||
pygame.display.set_mode((800, 600), pygame.RESIZABLE)
|
||||
surface = pygame.Surface((Display.WIDTH, Display.HEIGHT))
|
||||
keyboard = Keyboard()
|
||||
mouse = Mouse()
|
||||
|
||||
# Chargements des assets
|
||||
assets = Assets(surface)
|
||||
|
||||
# creation des channels pour les sons
|
||||
channels: dict[Entity, tuple[bool, pygame.mixer.Channel]] = {}
|
||||
class KeepAlive:
|
||||
"""
|
||||
Composant qui marque une entité comme n'étant pas détruit lors
|
||||
d'un changement de scène.
|
||||
"""
|
||||
|
||||
# On récupère la première scène
|
||||
scene = scenes.get(start_scene)
|
||||
|
||||
# Tant qu'il y a des scènes a executer on les executent
|
||||
while scene is not None:
|
||||
# Initialisation du monde
|
||||
def start_game(scenes: dict[str, Scene], scene_name: str):
|
||||
"""
|
||||
Lance un jeu.
|
||||
|
||||
Paramètres:
|
||||
- `scenes`: un dictionnaire contenant les scènes du jeu.
|
||||
- `scene_name`: le nom de la scène à charger en premier lors du lancement du jeu.
|
||||
"""
|
||||
world = World()
|
||||
world[Game] = Game()
|
||||
world[Assets] = assets
|
||||
world[Keyboard] = keyboard
|
||||
world[Mouse] = mouse
|
||||
world[Time] = Time(time())
|
||||
world[CurrentScene] = CurrentScene(scene_name)
|
||||
scene = scenes.get(scene_name)
|
||||
while scene is not None:
|
||||
# On retire les ressources de l'ancienne scène
|
||||
for resource in world:
|
||||
if not isinstance(resource, KeepAlive):
|
||||
world.remove(type(resource))
|
||||
|
||||
# Initialisation de la scène
|
||||
# On retire les entité de l'ancienne scène
|
||||
for entity in world.query(without=(KeepAlive,)):
|
||||
entity.destroy()
|
||||
|
||||
# On initialise la nouvelle scene
|
||||
for system in scene.init_systems:
|
||||
system(world)
|
||||
|
||||
# Tant que la scène n'est pas terminé
|
||||
while world[Game].next_scene is None and not world[Game].stop_requested:
|
||||
# On gère les évenements pygame
|
||||
keyboard.pressed.clear()
|
||||
keyboard.released.clear()
|
||||
mouse.pressed.clear()
|
||||
mouse.released.clear()
|
||||
last_position = Vec2(mouse.position)
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
world[Game].stop()
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
key_name = pygame.key.name(event.key)
|
||||
if key_name == "f11":
|
||||
pygame.display.toggle_fullscreen()
|
||||
keyboard.keys.add(key_name)
|
||||
keyboard.pressed.add(key_name)
|
||||
elif event.type == pygame.KEYUP:
|
||||
key_name = pygame.key.name(event.key)
|
||||
if key_name == "f11":
|
||||
continue
|
||||
keyboard.keys.remove(key_name)
|
||||
keyboard.released.add(key_name)
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN:
|
||||
mouse.buttons.add(event.button)
|
||||
mouse.pressed.add(event.button)
|
||||
elif event.type == pygame.MOUSEBUTTONUP:
|
||||
mouse.buttons.remove(event.button)
|
||||
mouse.released.add(event.button)
|
||||
elif event.type == pygame.MOUSEMOTION:
|
||||
rect = Display._calculate_surface_rect()
|
||||
mouse.position = Vec2(
|
||||
((event.pos[0] - rect[0]) / rect[2]) * Display.WIDTH,
|
||||
((event.pos[1] - rect[1]) / rect[3]) * Display.HEIGHT,
|
||||
)
|
||||
mouse.delta = mouse.position - last_position
|
||||
|
||||
# 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
|
||||
# Tant que la scène n'est pas terminé on la met à jour
|
||||
while world.get(CurrentScene, "") == scene_name:
|
||||
for system in scene.update_systems:
|
||||
system(world)
|
||||
|
||||
# Gestion des sons en cours
|
||||
sound_entities = world.query(Sound)
|
||||
entities_to_delete: list[Entity] = []
|
||||
for entity, (stop_on_remove, channel) in channels.items():
|
||||
if Sound in entity:
|
||||
entity_sound = entity[Sound]
|
||||
channel.set_volume(entity_sound.volume)
|
||||
if not channel.get_busy():
|
||||
entities_to_delete.append(entity)
|
||||
del entity[Sound]
|
||||
entity_sound.callback(world, entity)
|
||||
continue
|
||||
if stop_on_remove and entity not in sound_entities:
|
||||
entities_to_delete.append(entity)
|
||||
channel.stop()
|
||||
for entity in entities_to_delete:
|
||||
del channels[entity]
|
||||
|
||||
# Ajout des sons non gérés
|
||||
for entity in world.query(Sound):
|
||||
if entity not in channels:
|
||||
entity_sound = entity[Sound]
|
||||
sound = assets.get_sound(entity_sound.name)
|
||||
channel = sound.play(loops=-1 if entity_sound.loop else 0)
|
||||
if channel is None: # type: ignore
|
||||
continue
|
||||
channel.set_volume(entity_sound.volume)
|
||||
channels[entity] = entity_sound.stop_on_remove, channel
|
||||
|
||||
# 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
|
||||
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(
|
||||
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:
|
||||
color = entity[Color] if Color in entity else Color(255, 255, 255)
|
||||
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 - 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()
|
||||
pygame.transform.set_smoothscale_backend("MMX")
|
||||
pygame.transform.smoothscale(
|
||||
surface,
|
||||
(rect[2], rect[3]),
|
||||
pygame.display.get_surface().subsurface(rect),
|
||||
)
|
||||
pygame.display.flip()
|
||||
surface.fill((0, 0, 0))
|
||||
|
||||
# Arrêt de la scène
|
||||
# On arrete la scene
|
||||
for system in scene.stop_systems:
|
||||
system(world)
|
||||
|
||||
# Récupération de la scène suivante
|
||||
next_scene = world[Game].next_scene
|
||||
if next_scene is not None:
|
||||
scene = scenes.get(next_scene)
|
||||
else:
|
||||
scene = None
|
||||
|
||||
# Arrêt du jeu
|
||||
pygame.quit()
|
||||
scene = scenes.get(world.get(CurrentScene, ""))
|
||||
|
|
18
src/main.py
18
src/main.py
|
@ -1,19 +1,3 @@
|
|||
"""
|
||||
Example de l'utilisation du moteur de jeu.
|
||||
Module d'exemple de l'utilisation du moteur de jeu.
|
||||
"""
|
||||
|
||||
|
||||
from engine import start_game
|
||||
from scenes import classique, menteur, menu, directory_search
|
||||
|
||||
|
||||
start_game(
|
||||
{
|
||||
"menu": menu.SCENE,
|
||||
"classique": classique.SCENE,
|
||||
"menteur": menteur.SCENE,
|
||||
"histoire": directory_search.SCENE,
|
||||
},
|
||||
"menu",
|
||||
title="Guess The Number",
|
||||
)
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
"""
|
||||
Contient des plugins pour certaines features du jeu.
|
||||
Module contenant tous les plugins du jeu.
|
||||
|
||||
Un plugin est une scène pouvant être ajouté a d'autres scènes
|
||||
afin d'ajouter des fonctionnalités au jeu.
|
||||
|
||||
Le but est de faire en sorte que les plugins soient génériques
|
||||
afin de pouvoir les utilisers dans plusieurs scènes et donc
|
||||
éviter de répéter plusieurs fois le même code.
|
||||
"""
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
"""
|
||||
Un plugin permettant de faire des déplacements fluides des entités.
|
||||
"""
|
||||
|
||||
from engine import Delta, Position, Scene, Vec2, World
|
||||
|
||||
|
||||
class Target(Vec2):
|
||||
"""
|
||||
Composant donnant la position voulue de l'entité.
|
||||
"""
|
||||
|
||||
|
||||
class Speed(float):
|
||||
"""
|
||||
Composant donnant la vittesse de déplacement de l'entité.
|
||||
"""
|
||||
|
||||
|
||||
def __update_positions(world: World):
|
||||
"""
|
||||
Met à jour la position des entités pour se rapprocher de la position voulue.
|
||||
"""
|
||||
for entity in world.query(Position, Target):
|
||||
position = entity[Position]
|
||||
target = entity[Target]
|
||||
speed = entity[Speed] if Speed in entity else Speed(10)
|
||||
entity[Position] = Position(
|
||||
position + (target - position) * world[Delta] * speed
|
||||
)
|
||||
|
||||
|
||||
PLUGIN = Scene(
|
||||
[],
|
||||
[__update_positions],
|
||||
[],
|
||||
)
|
|
@ -1,37 +0,0 @@
|
|||
"""
|
||||
Definit un plugin qui crée un texte avec les touches frappées
|
||||
"""
|
||||
|
||||
from engine import Keyboard, Scene, Sound, Text, World
|
||||
|
||||
|
||||
class Typing(str):
|
||||
"""
|
||||
Marque une entité comme un texte qui s'ecrit en fonction du clavier
|
||||
"""
|
||||
|
||||
|
||||
def __update(world: World):
|
||||
"""
|
||||
Met a jour les entitées contenant le composant Typing
|
||||
"""
|
||||
keyboard = world[Keyboard]
|
||||
for entity in world.query(Typing, Text):
|
||||
text = entity[Text]
|
||||
for key in keyboard.pressed:
|
||||
if key == "backspace":
|
||||
world.create_entity(Sound("click"))
|
||||
text = text[:-1]
|
||||
if key.startswith("["): # pavé numerique
|
||||
key = key[1]
|
||||
if key in entity[Typing]:
|
||||
world.create_entity(Sound("click"))
|
||||
text += key
|
||||
entity[Text] = Text(text)
|
||||
|
||||
|
||||
PLUGIN = Scene(
|
||||
[],
|
||||
[__update],
|
||||
[],
|
||||
)
|
|
@ -1,3 +0,0 @@
|
|||
"""
|
||||
Contient toutes les scènes du jeu.
|
||||
"""
|
|
@ -1,227 +0,0 @@
|
|||
"""
|
||||
Définis la scène du jeu classique, sans variante.
|
||||
"""
|
||||
|
||||
import random
|
||||
from plugins import typing
|
||||
from engine import (
|
||||
Centered,
|
||||
Clickable,
|
||||
Color,
|
||||
Display,
|
||||
Entity,
|
||||
Game,
|
||||
HoveredTexture,
|
||||
Keyboard,
|
||||
Order,
|
||||
Position,
|
||||
Scene,
|
||||
Sound,
|
||||
Text,
|
||||
TextSize,
|
||||
Texture,
|
||||
World,
|
||||
)
|
||||
|
||||
COLOR_TEXT = Color(66, 39, 148)
|
||||
|
||||
|
||||
class RandomNumber(int):
|
||||
"""
|
||||
La ressource qui est le nombre a deviner.
|
||||
"""
|
||||
|
||||
|
||||
class TextDialogue:
|
||||
"""
|
||||
Le component qui declare l'entitee Text qui affiche le plus petit ou le plus grand
|
||||
"""
|
||||
|
||||
|
||||
class NombreEssai(int):
|
||||
"""
|
||||
Le component qui declare le nombre d'essai
|
||||
"""
|
||||
|
||||
|
||||
class NombreEssaiText:
|
||||
"""
|
||||
Le component qui affiche le nombre d'essai
|
||||
"""
|
||||
|
||||
|
||||
class IsRunning:
|
||||
"""
|
||||
Le component qui indique si le jeu est en cours
|
||||
"""
|
||||
|
||||
|
||||
def __initialize_world(world: World):
|
||||
"""
|
||||
Initialise le monde du menu.
|
||||
"""
|
||||
|
||||
# Fond d'ecran
|
||||
world.create_entity(
|
||||
Position(),
|
||||
Order(0),
|
||||
Texture("classique/background.png"),
|
||||
)
|
||||
|
||||
# Bouton valider/rejouer
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH / 2, 875),
|
||||
Order(1),
|
||||
Centered(),
|
||||
Texture("classique/valider.png"),
|
||||
HoveredTexture("classique/valider_hover.png"),
|
||||
Clickable(lambda world, _: _update(world)),
|
||||
)
|
||||
|
||||
# Zone de saisie
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH / 2, 750),
|
||||
Order(2),
|
||||
Centered(),
|
||||
typing.Typing("1234567890"),
|
||||
Text(""),
|
||||
COLOR_TEXT,
|
||||
TextSize(150),
|
||||
)
|
||||
|
||||
# Text qui dit si ton nombre et trop grand ou trop petit
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH / 2, 500),
|
||||
Order(3),
|
||||
Centered(),
|
||||
TextDialogue(),
|
||||
TextSize(150),
|
||||
COLOR_TEXT,
|
||||
Text("Devine le nombre..."),
|
||||
)
|
||||
|
||||
# Text qui affiche le nombre d'essai
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH / 2 - 100, 150),
|
||||
Order(4),
|
||||
TextSize(100),
|
||||
NombreEssaiText(),
|
||||
COLOR_TEXT,
|
||||
Text("il reste : 7 essais"),
|
||||
)
|
||||
|
||||
# Bouton pour revenir au menu
|
||||
world.create_entity(
|
||||
Order(11),
|
||||
Position(150, 150),
|
||||
Texture("classique/arrow.png"),
|
||||
Clickable(on_menu_button),
|
||||
HoveredTexture("classique/arrow_hover.png"),
|
||||
)
|
||||
|
||||
# Les ressources.
|
||||
world[NombreEssai] = NombreEssai(7)
|
||||
world[RandomNumber] = RandomNumber(random.randint(0, 99))
|
||||
world[IsRunning] = IsRunning()
|
||||
|
||||
|
||||
def on_menu_button(world: World, entity: Entity):
|
||||
"""
|
||||
Fonction qui s'execute quand on clique sur un bouton.
|
||||
"""
|
||||
world[Game].change_scene("menu")
|
||||
entity[Sound] = Sound("click")
|
||||
|
||||
|
||||
def _update(world: World):
|
||||
"""
|
||||
Verifie si le nombre donné est le meme que celui que l'on a choisi.
|
||||
Boucle du jeu.
|
||||
"""
|
||||
|
||||
world.create_entity(Sound("menu_click.wav"))
|
||||
|
||||
# si le jeu s'est arrete.
|
||||
if IsRunning not in world:
|
||||
# on relance le jeu.
|
||||
world[Game].change_scene("classique")
|
||||
|
||||
for entity in world.query(typing.Typing, Text):
|
||||
# One efface le nombre.
|
||||
number: str = entity[Text]
|
||||
entity[Text] = Text("")
|
||||
|
||||
# On gere le l'input de l'utilisateur.
|
||||
for entity_text in world.query(TextDialogue):
|
||||
if number == "": # si il a rien evoyé.
|
||||
entity_text[Text] = Text("tu doit entrer un nombre !")
|
||||
return
|
||||
if world[RandomNumber] == int(number): # si il a trouve le nombre.
|
||||
end_game(world, "Gagné")
|
||||
return
|
||||
elif world[NombreEssai] <= 1: # si il n'a plus d'essai.
|
||||
end_game(world, "Perdu")
|
||||
return
|
||||
elif world[RandomNumber] > int(number): # si le nombre est trop petit.
|
||||
entity_text[Text] = Text("Plus grand...")
|
||||
else: # si le nombre est trop grand.
|
||||
entity_text[Text] = Text("Plus petit...")
|
||||
|
||||
# on update l'affichage du nombre d'essai.
|
||||
world[NombreEssai] = NombreEssai(world[NombreEssai] - 1)
|
||||
for entity in world.query(NombreEssaiText):
|
||||
entity[Text] = Text(
|
||||
f"il reste : {world[NombreEssai]} essai{'s' if world[NombreEssai] != 1 else ''}"
|
||||
)
|
||||
|
||||
|
||||
def end_game(world: World, state: str):
|
||||
"""
|
||||
fonction applé quand le jeu est fini.
|
||||
"""
|
||||
del world[IsRunning] # le jeu est fini.
|
||||
|
||||
# On joue le son
|
||||
if state == "Gagné":
|
||||
world.create_entity(Sound("win_sound.wav"))
|
||||
else:
|
||||
world.create_entity(Sound("lose_sound.wav"))
|
||||
|
||||
# On affiche le message de fin.
|
||||
for entity_text in world.query(TextDialogue):
|
||||
entity_text[Text] = Text(f"{state} !")
|
||||
|
||||
# On empeche de pourvoir continuer le jeu.
|
||||
for entity in world.query(typing.Typing, Text):
|
||||
del entity[typing.Typing]
|
||||
|
||||
if state == "Gagné":
|
||||
for entity in world.query(NombreEssaiText):
|
||||
entity[Text] = Text("")
|
||||
else:
|
||||
for entity in world.query(NombreEssaiText):
|
||||
entity[Text] = Text(" plus d'essais")
|
||||
|
||||
# on change la texture du button submit.
|
||||
for entity in world.query(Clickable, Centered):
|
||||
entity[Texture] = Texture("classique/play_again.png")
|
||||
entity[HoveredTexture] = HoveredTexture("classique/play_again_hover.png")
|
||||
|
||||
|
||||
def _check_return(world: World):
|
||||
"""
|
||||
Verifie si la touche entrée est appuyée.
|
||||
"""
|
||||
keyboard = world[Keyboard]
|
||||
if keyboard.is_key_pressed("return") or keyboard.is_key_pressed("enter"):
|
||||
_update(world)
|
||||
|
||||
|
||||
SCENE = (
|
||||
Scene(
|
||||
[__initialize_world],
|
||||
[_check_return],
|
||||
[],
|
||||
)
|
||||
+ typing.PLUGIN
|
||||
)
|
|
@ -1,378 +0,0 @@
|
|||
"""
|
||||
Scène du jeu dans lequel on se cache de Edmond dans les dossiers.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
import random
|
||||
from engine import (
|
||||
Animation,
|
||||
Centered,
|
||||
Delta,
|
||||
Display,
|
||||
Entity,
|
||||
Hovered,
|
||||
Mouse,
|
||||
Order,
|
||||
Position,
|
||||
Scene,
|
||||
Sound,
|
||||
Text,
|
||||
TextSize,
|
||||
Texture,
|
||||
Vec2,
|
||||
World,
|
||||
)
|
||||
from plugins import smooth
|
||||
|
||||
|
||||
LINES = 3
|
||||
COLUMNS = 5
|
||||
SPACING = 200
|
||||
|
||||
|
||||
class State(Enum):
|
||||
"""
|
||||
Etat de la scène.
|
||||
"""
|
||||
|
||||
MOVING = 0
|
||||
SEARCHING = 1
|
||||
GAME_OVER = 2
|
||||
|
||||
|
||||
class SelectedDirectory:
|
||||
"""
|
||||
Une ressource qui stoque le dossier selectionné pour le déplacement.
|
||||
"""
|
||||
|
||||
def __init__(self, entity: Entity, start_position: Vec2):
|
||||
self.entity = entity
|
||||
self.position = start_position
|
||||
|
||||
|
||||
class AttackTimer(float):
|
||||
"""
|
||||
Ressource qui stoque un timer pour l'attaque.
|
||||
"""
|
||||
|
||||
|
||||
class AttackSpeed(float):
|
||||
"""
|
||||
Ressource qui dit le temps de l'attaque.
|
||||
"""
|
||||
|
||||
|
||||
class DirectoryPosition:
|
||||
"""
|
||||
La position d'un dossier dans la grille.
|
||||
"""
|
||||
|
||||
def __init__(self, x: int, y: int):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
if isinstance(value, DirectoryPosition):
|
||||
return self.x == value.x and self.y == value.y
|
||||
return False
|
||||
|
||||
def screen_position(self) -> Vec2:
|
||||
"""
|
||||
Calcule la position de l'entité sur l'ecran.
|
||||
"""
|
||||
size = Vec2(SPACING)
|
||||
offset = -(size * Vec2(COLUMNS - 1, LINES - 1) / 2)
|
||||
first_position = Vec2(Display.WIDTH / 2, Display.HEIGHT / 2) + offset
|
||||
return first_position + Vec2(self.x, self.y) * size
|
||||
|
||||
|
||||
class AttackPoint(DirectoryPosition):
|
||||
"""
|
||||
Composant qui marque un point d'attaque.
|
||||
"""
|
||||
|
||||
|
||||
class DirectoryName:
|
||||
"""
|
||||
Composant qui marque une entité comme étant le nom d'un dossier.
|
||||
"""
|
||||
|
||||
def __init__(self, entity: Entity):
|
||||
self.entity = entity
|
||||
|
||||
|
||||
class UserDirectory:
|
||||
"""
|
||||
Composant qui marque le dossier que l'utilisateur doit protéger.
|
||||
"""
|
||||
|
||||
|
||||
class GameStarted:
|
||||
"""
|
||||
Une ressource qui permet de savoir que le jeu commence.
|
||||
"""
|
||||
|
||||
|
||||
def __change_folders_speeds(world: World, _e: Entity):
|
||||
"""
|
||||
Change les vitesses des dossiers.
|
||||
"""
|
||||
for entity in world.query(DirectoryPosition, smooth.Speed):
|
||||
entity[smooth.Speed] = smooth.Speed(random.uniform(2.0, 2.5))
|
||||
for entity in world.query(TextSize):
|
||||
entity[TextSize] = TextSize(40)
|
||||
|
||||
|
||||
def __remove_folders_speeds(world: World):
|
||||
"""
|
||||
Supprime les vitesses des dossiers.
|
||||
"""
|
||||
if GameStarted not in world and world[AttackTimer] >= 3.0:
|
||||
for entity in world.query(DirectoryPosition, smooth.Speed):
|
||||
del entity[smooth.Speed]
|
||||
world[GameStarted] = GameStarted()
|
||||
|
||||
|
||||
def __initialize_world(world: World):
|
||||
"""
|
||||
Initialise le monde de la scène.
|
||||
"""
|
||||
world[State] = State.MOVING
|
||||
world[AttackTimer] = AttackTimer(0.0)
|
||||
world[AttackSpeed] = AttackSpeed(5.0)
|
||||
|
||||
world.create_entity(
|
||||
Position(),
|
||||
Order(0),
|
||||
Animation("fade_desktop", __change_folders_speeds),
|
||||
)
|
||||
|
||||
names = [
|
||||
"Classique",
|
||||
"Menteur",
|
||||
"Tricheur",
|
||||
"Histoire",
|
||||
"Je t'aime",
|
||||
"Hello",
|
||||
"Cheval",
|
||||
"Defender",
|
||||
"Dansons",
|
||||
"Secrets",
|
||||
"Edmond",
|
||||
"Mon Amour",
|
||||
"Melatonin",
|
||||
"Films",
|
||||
"Cinéma",
|
||||
]
|
||||
|
||||
positions = [
|
||||
Position(40 + (7 * 180) + 48, 35 + (5 * 166) + 38),
|
||||
Position(40 + (5 * 180) + 48, 35 + (5 * 166) + 38),
|
||||
Position(40 + (4 * 180) + 48, 35 + (4 * 166) + 38),
|
||||
Position(40 + (3 * 180) + 48, 35 + (5 * 166) + 38),
|
||||
Position(40 + (1 * 180) + 48, 35 + (5 * 166) + 38),
|
||||
Position(40 + (6 * 180) + 48, 35 + (2 * 166) + 38),
|
||||
Position(40 + (5 * 180) + 48, 35 + (3 * 166) + 38),
|
||||
Position(40 + (4 * 180) + 48, 35 + (2 * 166) + 38),
|
||||
Position(40 + (2 * 180) + 48, 35 + (4 * 166) + 38),
|
||||
Position(40 + (1 * 180) + 48, 35 + (2 * 166) + 38),
|
||||
Position(40 + (7 * 180) + 48, 35 + (1 * 166) + 38),
|
||||
Position(40 + (5 * 180) + 48, 35 + (1 * 166) + 38),
|
||||
Position(40 + (3 * 180) + 48, 35 + (0 * 166) + 38),
|
||||
Position(40 + (2 * 180) + 48, 35 + (1 * 166) + 38),
|
||||
Position(40 + (0 * 180) + 48, 35 + (1 * 166) + 38),
|
||||
]
|
||||
|
||||
for y in range(LINES):
|
||||
for x in range(COLUMNS):
|
||||
position = DirectoryPosition(x, y)
|
||||
entity = world.create_entity(
|
||||
positions.pop(),
|
||||
smooth.Speed(0),
|
||||
Order(1),
|
||||
Centered(),
|
||||
Texture("directory.png"),
|
||||
position,
|
||||
)
|
||||
|
||||
if x == 2 and y == 1:
|
||||
entity[UserDirectory] = UserDirectory()
|
||||
entity[Texture] = Texture("user_directory.png")
|
||||
|
||||
world.create_entity(
|
||||
Position(0, 0),
|
||||
Order(1),
|
||||
Centered(),
|
||||
Text(names.pop()),
|
||||
TextSize(0),
|
||||
DirectoryName(entity),
|
||||
)
|
||||
|
||||
|
||||
def __attacks(world: World):
|
||||
"""
|
||||
Déclenche les attaques de Edmond.
|
||||
"""
|
||||
if world[State] == State.GAME_OVER:
|
||||
return
|
||||
world[AttackTimer] = AttackTimer(world[AttackTimer] + world[Delta])
|
||||
timer = world[AttackTimer]
|
||||
if timer >= world[AttackSpeed] and world[State] == State.MOVING:
|
||||
world[State] = State.SEARCHING
|
||||
for entity in world.query(AttackPoint):
|
||||
position = entity[AttackPoint]
|
||||
for directory_entity in world.query(DirectoryPosition):
|
||||
if directory_entity[DirectoryPosition] == position:
|
||||
if UserDirectory in directory_entity:
|
||||
directory_entity[Animation] = Animation(
|
||||
"search_directory_failed"
|
||||
)
|
||||
world[State] = State.GAME_OVER
|
||||
else:
|
||||
directory_entity[Animation] = Animation("search_directory")
|
||||
del entity[AttackPoint]
|
||||
del entity[Position]
|
||||
del entity[Order]
|
||||
del entity[Centered]
|
||||
del entity[Texture]
|
||||
elif timer >= world[AttackSpeed] + 4.5 and world[State] == State.SEARCHING:
|
||||
world[State] = State.MOVING
|
||||
for _ in range(10):
|
||||
position = AttackPoint(
|
||||
random.randint(0, COLUMNS - 1),
|
||||
random.randint(0, LINES - 1),
|
||||
)
|
||||
world.create_entity(
|
||||
position,
|
||||
Position(position.screen_position()),
|
||||
Order(50),
|
||||
Centered(),
|
||||
Texture("attack_point.png"),
|
||||
)
|
||||
world[AttackTimer] = AttackTimer(0.0)
|
||||
world[AttackSpeed] = AttackSpeed(world[AttackSpeed] * 0.9)
|
||||
|
||||
|
||||
def __move_directories(world: World):
|
||||
"""
|
||||
Permet de déplacer les dossiers avec la souris.
|
||||
"""
|
||||
# Si on n'est pas dans le bon state on annule
|
||||
if GameStarted not in world or world[State] == State.GAME_OVER:
|
||||
return
|
||||
|
||||
# On met à jour la séléction
|
||||
mouse = world[Mouse]
|
||||
for entity in world.query(Hovered, DirectoryPosition):
|
||||
if Animation in entity:
|
||||
continue
|
||||
if mouse.is_button_pressed(1):
|
||||
world[SelectedDirectory] = SelectedDirectory(entity, Vec2(mouse.position))
|
||||
break
|
||||
|
||||
# Si un dossier est séléctionné
|
||||
if SelectedDirectory in world:
|
||||
selected_directory = world[SelectedDirectory]
|
||||
selected_entity = selected_directory.entity
|
||||
directory_position = selected_entity[DirectoryPosition]
|
||||
|
||||
# Vérification du relachement de la souris
|
||||
if not mouse.is_button(1):
|
||||
del world[SelectedDirectory]
|
||||
return
|
||||
|
||||
# On calcule le déplacement de la souris
|
||||
mouse_delta = mouse.position - selected_directory.position
|
||||
|
||||
# On annule si il y a pas eu de déplacement significatif de la souris
|
||||
if mouse_delta.length < 40:
|
||||
return
|
||||
|
||||
# Récupération du mouvement voulu
|
||||
if abs(mouse_delta.x) >= abs(mouse_delta.y):
|
||||
movement = (int(mouse_delta.x / abs(mouse_delta.x)), 0)
|
||||
else:
|
||||
movement = (0, int(mouse_delta.y / abs(mouse_delta.y)))
|
||||
|
||||
# Récupération des mouvements possible du dossier
|
||||
movements: list[tuple[int, int]] = []
|
||||
if directory_position.x != 0:
|
||||
movements.append((-1, 0))
|
||||
if directory_position.x != COLUMNS - 1:
|
||||
movements.append((1, 0))
|
||||
if directory_position.y != 0:
|
||||
movements.append((0, -1))
|
||||
if directory_position.y != LINES - 1:
|
||||
movements.append((0, 1))
|
||||
if len(movements) == 0:
|
||||
return
|
||||
|
||||
# Si le mouvement n'est pas possible, on annule
|
||||
if movement not in movements:
|
||||
return
|
||||
|
||||
# Si l'entité est animé on annule
|
||||
if Animation in selected_entity:
|
||||
return
|
||||
|
||||
# On trouve l'autre dossier
|
||||
for entity in world.query(DirectoryPosition, without=(Animation,)):
|
||||
if entity != selected_entity and entity[
|
||||
DirectoryPosition
|
||||
] == DirectoryPosition(
|
||||
directory_position.x + movement[0],
|
||||
directory_position.y + movement[1],
|
||||
):
|
||||
other_directory = entity
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
# On actualise la position de l'autre dossier
|
||||
other_directory[DirectoryPosition].x -= movement[0]
|
||||
other_directory[DirectoryPosition].y -= movement[1]
|
||||
|
||||
# On actualise la position du dossier
|
||||
selected_entity[DirectoryPosition].x += movement[0]
|
||||
selected_entity[DirectoryPosition].y += movement[1]
|
||||
|
||||
# On joue un son
|
||||
world.create_entity(Sound("slide.wav"))
|
||||
|
||||
# On retire le dossier selectionné
|
||||
del world[SelectedDirectory]
|
||||
|
||||
|
||||
def __update_positions(world: World):
|
||||
"""
|
||||
Met à jour la position cible des dossiers.
|
||||
"""
|
||||
for entity in world.query(DirectoryPosition):
|
||||
position = entity[DirectoryPosition]
|
||||
entity[smooth.Target] = smooth.Target(position.screen_position())
|
||||
entity[Order] = Order(position.y + 1)
|
||||
|
||||
|
||||
def __update_directory_names(world: World):
|
||||
"""
|
||||
Met à jour la position des noms des dossiers.
|
||||
"""
|
||||
for entity in world.query(DirectoryName):
|
||||
directory_entity = entity[DirectoryName].entity
|
||||
entity[Position] = Position(directory_entity[Position] + Vec2(0, 75))
|
||||
entity[Order] = directory_entity[Order]
|
||||
|
||||
|
||||
SCENE = (
|
||||
Scene(
|
||||
[__initialize_world],
|
||||
[__attacks, __move_directories, __update_positions, __remove_folders_speeds],
|
||||
[],
|
||||
)
|
||||
+ smooth.PLUGIN
|
||||
+ Scene(
|
||||
[],
|
||||
[__update_directory_names],
|
||||
[],
|
||||
)
|
||||
)
|
|
@ -1,236 +0,0 @@
|
|||
"""
|
||||
Définis la scène du jeu menteur, sans variante.
|
||||
"""
|
||||
|
||||
from random import randint
|
||||
from plugins import typing
|
||||
from engine import (
|
||||
Centered,
|
||||
Clickable,
|
||||
Color,
|
||||
Display,
|
||||
Entity,
|
||||
Game,
|
||||
HoveredTexture,
|
||||
Keyboard,
|
||||
Order,
|
||||
Position,
|
||||
Scene,
|
||||
Sound,
|
||||
Text,
|
||||
TextSize,
|
||||
Texture,
|
||||
World,
|
||||
)
|
||||
|
||||
|
||||
class RandomNumber(int):
|
||||
"""
|
||||
La ressource qui est le nombre a deviner.
|
||||
"""
|
||||
|
||||
|
||||
class TextDialogue:
|
||||
"""
|
||||
Le component qui declare l'entitee Text qui affiche le plus petit ou le plus grand
|
||||
"""
|
||||
|
||||
|
||||
class NombreEssai(int):
|
||||
"""
|
||||
Le component qui declare le nombre d'essai
|
||||
"""
|
||||
|
||||
|
||||
class NombreEssaiText:
|
||||
"""
|
||||
Le component qui affiche le nombre d'essai
|
||||
"""
|
||||
|
||||
|
||||
class IsRunning:
|
||||
"""
|
||||
Le component qui indique si le jeu est en cours
|
||||
"""
|
||||
|
||||
|
||||
COLOR_TEXT = Color(59, 162, 0)
|
||||
|
||||
|
||||
def __initialize_world(world: World):
|
||||
"""
|
||||
Initialise le monde du menu.
|
||||
"""
|
||||
|
||||
# Fond d'ecran
|
||||
world.create_entity(
|
||||
Position(),
|
||||
Order(0),
|
||||
Texture("menteur/background.png"),
|
||||
)
|
||||
|
||||
# Bouton valider/rejouer
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH / 2, 880),
|
||||
Order(1),
|
||||
Centered(),
|
||||
Texture("menteur/valider.png"),
|
||||
HoveredTexture("menteur/valider_hover.png"),
|
||||
Clickable(lambda world, _: _update(world)),
|
||||
)
|
||||
|
||||
# Zone de saisie
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH / 2, 750),
|
||||
Order(2),
|
||||
Centered(),
|
||||
typing.Typing("1234567890"),
|
||||
Text(""),
|
||||
COLOR_TEXT,
|
||||
TextSize(150),
|
||||
)
|
||||
|
||||
# Text qui dit si ton nombre et trop grand ou trop petit
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH / 2, 500),
|
||||
Order(3),
|
||||
Centered(),
|
||||
TextDialogue(),
|
||||
TextSize(150),
|
||||
COLOR_TEXT,
|
||||
Text("Devine le nombre..."),
|
||||
)
|
||||
|
||||
# Text qui affiche le nombre d'essai
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH - 750, 120),
|
||||
Order(4),
|
||||
TextSize(100),
|
||||
NombreEssaiText(),
|
||||
COLOR_TEXT,
|
||||
Text("il reste : 15 essais"),
|
||||
)
|
||||
|
||||
# Bouton pour revenir au menu
|
||||
world.create_entity(
|
||||
Order(11),
|
||||
Position(100, 100),
|
||||
Texture("menteur/arrow.png"),
|
||||
Clickable(on_menu_button),
|
||||
HoveredTexture("menteur/arrow_hover.png"),
|
||||
)
|
||||
|
||||
# Les ressources.
|
||||
world[NombreEssai] = NombreEssai(15)
|
||||
world[RandomNumber] = RandomNumber(randint(0, 99))
|
||||
world[IsRunning] = IsRunning()
|
||||
|
||||
|
||||
def on_menu_button(world: World, entity: Entity):
|
||||
"""
|
||||
Fonction qui s'execute quand on clique sur un bouton.
|
||||
"""
|
||||
world[Game].change_scene("menu")
|
||||
entity[Sound] = Sound("click")
|
||||
|
||||
|
||||
def _update(world: World):
|
||||
"""
|
||||
Verifie si le nombre donné est le meme que celui que l'on a choisi.
|
||||
Boucle du jeu.
|
||||
"""
|
||||
|
||||
world.create_entity(Sound("menu_click.wav"))
|
||||
|
||||
# si le jeu s'est arrete.
|
||||
if IsRunning not in world:
|
||||
# on relance le jeu.
|
||||
world[Game].change_scene("menteur")
|
||||
|
||||
for entity in world.query(typing.Typing, Text):
|
||||
# One efface le nombre.
|
||||
number: str = entity[Text]
|
||||
entity[Text] = Text("")
|
||||
|
||||
# On gere le l'input de l'utilisateur.
|
||||
for entity_text in world.query(TextDialogue):
|
||||
if number == "": # si il a rien evoyé.
|
||||
entity_text[Text] = Text("tu doit entrer un nombre !")
|
||||
return
|
||||
if world[RandomNumber] == int(number): # si il a trouve le nombre.
|
||||
end_game(world, "Gagné")
|
||||
return
|
||||
elif world[NombreEssai] <= 1: # si il n'a plus d'essai.
|
||||
end_game(world, "Perdu")
|
||||
return
|
||||
elif world[RandomNumber] > int(number): # si le nombre est trop petit.
|
||||
lie = randint(1, 4)
|
||||
if lie == 4:
|
||||
entity_text[Text] = Text("Plus petit...")
|
||||
else:
|
||||
entity_text[Text] = Text("Plus grand...")
|
||||
else: # si le nombre est trop grand.
|
||||
lie = randint(1, 4)
|
||||
if lie == 4:
|
||||
entity_text[Text] = Text("Plus grand...")
|
||||
else:
|
||||
entity_text[Text] = Text("Plus petit...")
|
||||
|
||||
# on update l'affichage du nombre d'essai.
|
||||
world[NombreEssai] = NombreEssai(world[NombreEssai] - 1)
|
||||
for entity in world.query(NombreEssaiText):
|
||||
entity[Text] = Text(
|
||||
f"il reste : {world[NombreEssai]} essai{'s' if world[NombreEssai] != 1 else ''}"
|
||||
)
|
||||
|
||||
|
||||
def end_game(world: World, state: str):
|
||||
"""
|
||||
fonction applé quand le jeu est fini.
|
||||
"""
|
||||
del world[IsRunning] # le jeu est fini.
|
||||
|
||||
# On joue le son
|
||||
if state == "Gagné":
|
||||
world.create_entity(Sound("win_sound.wav"))
|
||||
else:
|
||||
world.create_entity(Sound("lose_sound.wav"))
|
||||
|
||||
# On affiche le message de fin.
|
||||
for entity_text in world.query(TextDialogue):
|
||||
entity_text[Text] = Text(f"{state} !")
|
||||
|
||||
# On empeche de pourvoir continuer le jeu.
|
||||
for entity in world.query(typing.Typing, Text):
|
||||
del entity[typing.Typing]
|
||||
|
||||
if state == "Gagné":
|
||||
for entity in world.query(NombreEssaiText):
|
||||
entity[Text] = Text("")
|
||||
else:
|
||||
for entity in world.query(NombreEssaiText):
|
||||
entity[Text] = Text(" plus d'essais")
|
||||
|
||||
# on change la texture du button submit.
|
||||
for entity in world.query(Clickable, Centered):
|
||||
entity[Texture] = Texture("menteur/play_again.png")
|
||||
entity[HoveredTexture] = HoveredTexture("menteur/play_again_hover.png")
|
||||
|
||||
|
||||
def _check_return(world: World):
|
||||
"""
|
||||
Verifie si la touche entrée est appuyée.
|
||||
"""
|
||||
keyboard = world[Keyboard]
|
||||
if keyboard.is_key_pressed("return") or keyboard.is_key_pressed("enter"):
|
||||
_update(world)
|
||||
|
||||
|
||||
SCENE = (
|
||||
Scene(
|
||||
[__initialize_world],
|
||||
[_check_return],
|
||||
[],
|
||||
)
|
||||
+ typing.PLUGIN
|
||||
)
|
|
@ -1,57 +0,0 @@
|
|||
"""
|
||||
Définis la scène du menu du jeu.
|
||||
"""
|
||||
|
||||
from engine import (
|
||||
Centered,
|
||||
Clickable,
|
||||
Display,
|
||||
Entity,
|
||||
Game,
|
||||
HoveredTexture,
|
||||
Order,
|
||||
Position,
|
||||
Scene,
|
||||
Sound,
|
||||
Texture,
|
||||
World,
|
||||
)
|
||||
|
||||
|
||||
def __create_button(world: World, i: int, name: str):
|
||||
"""
|
||||
Ajoute un bouton au monde.
|
||||
"""
|
||||
world.create_entity(
|
||||
Position(Display.WIDTH / 2, 450 + 150 * i),
|
||||
Order(1),
|
||||
Centered(),
|
||||
Texture(f"menu/button_{name}.png"),
|
||||
HoveredTexture(f"menu/button_{name}_hover.png"),
|
||||
Clickable(lambda world, entity: on_click_butons(world, entity, name)),
|
||||
)
|
||||
|
||||
|
||||
def on_click_butons(world: World, entity: Entity, name: str):
|
||||
"""
|
||||
Fonction qui s'execute quand on clique sur un bouton.
|
||||
"""
|
||||
entity[Sound] = Sound("click")
|
||||
world[Game].change_scene(name)
|
||||
|
||||
|
||||
def __initialize_world(world: World):
|
||||
"""
|
||||
Initialise le monde du menu.
|
||||
"""
|
||||
world.create_entity(Position(), Order(0), Texture("menu/background.png"))
|
||||
scenes_name = ["classique", "menteur", "tricheur", "histoire"]
|
||||
for i, name in enumerate(scenes_name):
|
||||
__create_button(world, i, name)
|
||||
|
||||
|
||||
SCENE = Scene(
|
||||
[__initialize_world],
|
||||
[],
|
||||
[],
|
||||
)
|
Loading…
Reference in a new issue