dialogs_event_sheduler #21

Merged
yannis merged 23 commits from dialogs_event_sheduler into main 2024-01-06 18:55:17 +00:00
8 changed files with 291 additions and 28 deletions

5
assets/dialogs.json Normal file
View file

@ -0,0 +1,5 @@
{
"test": ["test1111", "test2", "test très long permettant de tester le retour à la ligne dans le renderer du jeu dans la fonction qui rend les dialogues et il faut éviter d'en faire des si long car ça pourrait dépacer en bas de l'écran ! Bonne journée !"],
"test2": ["salut", "aurevoir"],
"test3": ["test très long permettant de tester le retour à la ligne dans le renderer du jeu dans la fonction qui rend les dialogues et il faut éviter d'en faire des si long car ça pourrait dépacer en bas de l'écran ! Bonne journée !"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -1,13 +1,89 @@
import json import json
from types import FunctionType
from src.engine.event_handler import EventHandler
class DialogsManager: class DialogsManager:
"""Classe qui gère la lecture des dialogues.""" """Classe qui gère la lecture des dialogues."""
def __init__(self): def __init__(self, event_handler: EventHandler):
self.current_dialog = [] self.event_handler = event_handler
self.current_dialogs = []
self.current_dialog_id = -1
self.dialogs = {} self.dialogs = {}
self.reading_dialog = False
self.current_dialogue_letter_id = 0
self.writing_dialog = False
self.LETTER_WRITING_DELAY = 0.02
self.letter_timer = 0
self.dialogue_finished_callback = None
def next_signal(self):
"""Fonction exécutée lorsque l'utilisateur demande de passer au prochain dialogue. Si un dialogue est en
train d'être écrit, il écrit tout d'un coup."""
if self.reading_dialog:
if self.writing_dialog:
self.current_dialogue_letter_id = len(self.current_dialogs[self.current_dialog_id])
else:
self.next_dialog()
print("next")
def next_dialog(self):
"""Passe au dialogue suivant. Appelle le callback si le dialogue est fini."""
self.current_dialog_id += 1
self.writing_dialog = True
if self.current_dialog_id >= len(self.current_dialogs): # Le dialogue est fini.
self.current_dialogs = []
self.current_dialog_id = -1
self.writing_dialog = False
self.reading_dialog = False
self.event_handler.remove_button_area("next_dialog")
if self.dialogue_finished_callback is not None:
self.dialogue_finished_callback()
def start_dialog(self, name: str, dialogue_finished_callback: FunctionType | classmethod | staticmethod = None):
"""Lance le dialogue au nom donné."""
# Si un dialogue n'est pas déja lancé, on lance le dialogue au nom donné
if not self.reading_dialog:
self.event_handler.register_button_area((0, 0, 1, 1), self.next_signal, "next_dialog", 0)
self.current_dialogs = self.dialogs[name]
self.current_dialog_id = 0
self.current_dialogue_letter_id = 0
self.reading_dialog = True
self.dialogue_finished_callback = dialogue_finished_callback
def get_current_dialog_sentence(self, progressive=True) -> str:
"""Renvoie la phrase actuelle du dialogue."""
if progressive:
return self.current_dialogs[self.current_dialog_id][:self.current_dialogue_letter_id]
else:
return self.current_dialogs[self.current_dialog_id]
def load_dialogs(self, file_path: str): def load_dialogs(self, file_path: str):
"""Charge les dialogues du jeu grave au fichier json donné.""" """Charge les dialogues du jeu grave au fichier json donné."""
with open(file_path, "r") as file: with open(file_path, "r", encoding="utf-8") as file:
self.dialogs = json.loads(file.read()) self.dialogs = json.loads(file.read())
def update(self, delta: float):
"""Met à jour e gestionnaire de dialogues."""
if self.reading_dialog:
self.letter_timer -= delta
if self.letter_timer <= 0:
self.letter_timer = self.LETTER_WRITING_DELAY
self.current_dialogue_letter_id += 1
if self.current_dialogue_letter_id > len(self.current_dialogs[self.current_dialog_id]):
self.current_dialogue_letter_id -= 1
self.writing_dialog = False
#print(self.writing_dialog, self.reading_dialog)

View file

@ -1,7 +1,9 @@
from src.engine.boss_fight_manager import BossFightManager from src.engine.boss_fight_manager import BossFightManager
from src.engine.camera import Camera from src.engine.camera import Camera
from src.engine.dialogs_manager import DialogsManager
from src.engine.entity_manager import EntityManager from src.engine.entity_manager import EntityManager
from src.engine.event_handler import EventHandler from src.engine.event_handler import EventHandler
from src.engine.event_sheduler import EventSheduler
from src.engine.map_manager import MapManager from src.engine.map_manager import MapManager
from src.engine.renderer import Renderer from src.engine.renderer import Renderer
from src.engine.enums import GameState from src.engine.enums import GameState
@ -31,6 +33,8 @@ class Engine:
self.camera = Camera() self.camera = Camera()
self.entity_manager = EntityManager(self.map_manager) self.entity_manager = EntityManager(self.map_manager)
self.boss_fight_manager = BossFightManager(self) self.boss_fight_manager = BossFightManager(self)
self.event_sheduler = EventSheduler(self)
self.dialogs_manager = DialogsManager(self.event_handler)
def loop(self): def loop(self):
"""Fonction à lancer au début du programme et qui va lancer les updates dans une boucle. """Fonction à lancer au début du programme et qui va lancer les updates dans une boucle.
@ -47,6 +51,8 @@ class Engine:
self.entity_manager.update(0.016666666) self.entity_manager.update(0.016666666)
self.renderer.update(0.016666666) self.renderer.update(0.016666666)
self.event_handler.update() self.event_handler.update()
self.event_sheduler.update()
self.dialogs_manager.update(0.016666666)
def stop(self): def stop(self):
"""Arrête le programme.""" """Arrête le programme."""

View file

@ -1,6 +1,7 @@
import math import math
from types import FunctionType
from pygame import event from pygame import event, display
from pygame.locals import * from pygame.locals import *
import src.engine.engine as engine import src.engine.engine as engine
@ -8,9 +9,46 @@ import src.engine.engine as engine
class EventHandler: class EventHandler:
"""Classe utilisée pour traiter les pygame.event.get() et gérer les interactions avec le reste du programme.""" """Classe utilisée pour traiter les pygame.event.get() et gérer les interactions avec le reste du programme."""
def __init__(self, core: 'engine.Engine'): def __init__(self, core: 'engine.Engine'):
self.engine = core self.engine = core
self.key_pressed = [] self.key_pressed = []
self.buttons_area = []
@staticmethod
def get_click_collision(rect: tuple[float | int, float | int, float | int, float | int], point: tuple[int, int],
is_window_relative: int):
"""Vérifie si le point et le rectangle donné sont en collision."""
window_size = display.get_window_size()
if is_window_relative == 0:
return (rect[0]*window_size[0] < point[0] < rect[0]*window_size[0] + rect[2]*window_size[0]
and rect[1]*window_size[0] < point[1] < rect[1]*window_size[0] + rect[3]*window_size[0])
elif is_window_relative == 1:
return (rect[0]*window_size[1] < point[0] < rect[0]*window_size[1] + rect[2]*window_size[1] and
rect[1]*window_size[1] < point[1] < rect[1]*window_size[1] + rect[3]*window_size[1])
return rect[0] < point[0] < rect[0] + rect[2] and rect[1] < point[1] < rect[1] + rect[3]
def register_button_area(self, rect: tuple[float | int, float | int, float | int, float | int],
callback: FunctionType | classmethod | staticmethod, name: str,
is_window_relative: int = -1):
"""Enregistre une zone comme bouton. La fonction donnée sera donc executé lorsque la zone sur la fenêtre
sera cliqué. is_window_relative doit être 0 pour que le rect soit multipliée par la largeur de la fenêtre et 1
pour qu'elle soit multipliée par la hauteur"""
self.buttons_area.append((rect, callback, is_window_relative, name))
def remove_button_area(self, name: str):
"""Supprime les boutons aux noms donnés."""
# On itère dans toute la liste et on ne garde que les éléments ne portant pas le nom cherché
cleared_list = []
for area in self.buttons_area:
if area[3] != name:
cleared_list.append(area)
self.buttons_area = cleared_list
def update(self): def update(self):
"""Vérifie s'il y a de nouvelles interactions et les traites.""" """Vérifie s'il y a de nouvelles interactions et les traites."""
@ -22,7 +60,14 @@ class EventHandler:
elif e.type == KEYDOWN: elif e.type == KEYDOWN:
self.key_pressed.append(e.key) self.key_pressed.append(e.key)
elif e.type == KEYUP: elif e.type == KEYUP:
if e.key in self.key_pressed:
self.key_pressed.remove(e.key) self.key_pressed.remove(e.key)
elif e.type == MOUSEBUTTONDOWN:
# Vérifie si une des zones enregistrées comme bouton n'a pas été cliqué
if e.button == 1:
for area in self.buttons_area:
if self.get_click_collision(area[0], e.pos, area[2]):
area[1]()
if self.engine.entity_manager.player_entity_name: if self.engine.entity_manager.player_entity_name:
if K_RIGHT in self.key_pressed: if K_RIGHT in self.key_pressed:
@ -34,6 +79,10 @@ class EventHandler:
if K_DOWN in self.key_pressed: if K_DOWN in self.key_pressed:
self.engine.entity_manager.move_player_controls(0, 1) self.engine.entity_manager.move_player_controls(0, 1)
if K_SPACE in self.key_pressed:
self.engine.dialogs_manager.next_signal()
self.key_pressed.remove(K_SPACE)
if self.engine.DEBUG_MODE: if self.engine.DEBUG_MODE:
if K_l in self.key_pressed: if K_l in self.key_pressed:
self.engine.entity_manager.get_by_name("player").take_damages(1) self.engine.entity_manager.get_by_name("player").take_damages(1)
@ -49,4 +98,3 @@ class EventHandler:
self.engine.camera.target_zoom *= 1.01 self.engine.camera.target_zoom *= 1.01
if K_c in self.key_pressed: if K_c in self.key_pressed:
self.engine.camera.target_zoom *= 0.99 self.engine.camera.target_zoom *= 0.99

View file

@ -1,14 +1,41 @@
from types import FunctionType from types import FunctionType
import src.engine.engine
from src.engine.entity import Entity
class EventSheduler: class EventSheduler:
"""Gère le lancement d'évenements avec des conditions.""" """Gère le lancement d'évenements avec des conditions."""
def __init__(self): def __init__(self, engine: 'src.engine.engine.Engine'):
self.area_callbacks = [] self.area_callbacks = []
self.engine = engine
def register_area(self, area_rect: tuple[int, int, int, int], callback: FunctionType | classmethod | staticmethod): def register_area(self, area_rect: tuple[int, int, int, int], callback: FunctionType | classmethod | staticmethod,
self.area_callbacks.append((area_rect, callback)) linked_entities_name: list[Entity], single_use: bool = True, no_spam: bool = False):
self.area_callbacks.append((area_rect, callback, linked_entities_name, single_use, no_spam, []))
# La liste vide en dernier argument correspond aux entités actuellement dans la zone
@staticmethod
def get_collisions_with_entity(rect: tuple[int, int, int, int], entity: 'Entity'):
"""Retourne True si l'entité donnée touche le rectangle donné."""
return (rect[0] <= entity.x+entity.collision_rect[2] and
rect[0] + rect[2] >= entity.x+entity.collision_rect[0] and
rect[1] + rect[3] >= entity.y+entity.collision_rect[1] and
rect[1] <= entity.y+entity.collision_rect[3])
def update(self): def update(self):
for area in self.area_callbacks: """Met à jour l'event sheduler et execute les actions si les conditions à son execution sont respéctées."""
area_rect = area[0]
# On itère dans la liste des zones de détection
for area in self.area_callbacks.copy():
# On itère dans toutes les entités enregistrées
for entity in area[2]:
entity_in_area = self.get_collisions_with_entity(area[0], self.engine.entity_manager.get_by_name(entity))
if entity_in_area and not (area[4] and entity in area[5]):
area[1](entity)
if area[3]:
self.area_callbacks.remove(area)
if area[4]:
area[5].append(entity)
elif (not entity_in_area) and (area[4] and entity in area[5]):
area[5].remove(entity)

View file

@ -15,7 +15,8 @@ class Renderer:
def __init__(self, core: 'engine.Engine'): def __init__(self, core: 'engine.Engine'):
self.engine = core self.engine = core
self.window_type = RESIZABLE self.window_type = RESIZABLE
self.window_size = (display.Info().current_w, display.Info().current_h) if self.window_type == FULLSCREEN else (600, 600) 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.window = display.set_mode(self.window_size, self.window_type)
self.tiles = [] self.tiles = []
self.tile_size = 0 self.tile_size = 0
@ -26,6 +27,9 @@ class Renderer:
self.boss_fight_player_animations: dict[str: Anim] = {} self.boss_fight_player_animations: dict[str: Anim] = {}
self.boss_fight_GUI_container = None self.boss_fight_GUI_container = None
# Boite de dialogue
self.dialogs_box = None
# Variables utilisées par le menu principal # Variables utilisées par le menu principal
self.main_menu_assets: dict[str: Anim] = {} self.main_menu_assets: dict[str: Anim] = {}
@ -54,8 +58,8 @@ class Renderer:
part_speed_y = - part_speed_y part_speed_y = - part_speed_y
# On choisit sa position dans le rectangle # On choisit sa position dans le rectangle
part_x = random.randint(x-w, x+w-part_size) part_x = random.randint(x - w, x + w - part_size)
part_y = random.randint(y-h, y+h-part_size) part_y = random.randint(y - h, y + h - part_size)
# On choisit la durée de vie # On choisit la durée de vie
part_life_time = random.uniform(min_life_time, max_life_time) part_life_time = random.uniform(min_life_time, max_life_time)
@ -98,6 +102,7 @@ class Renderer:
self.render_entities(rendered_surface, gui_surface, delta) self.render_entities(rendered_surface, gui_surface, delta)
self.render_particles(rendered_surface, delta) self.render_particles(rendered_surface, delta)
self.render_layer(2, rendered_surface) self.render_layer(2, rendered_surface)
self.render_debug_area(rendered_surface)
# Enfin, on redimensionne notre surface et on la colle sur la fenêtre principale # Enfin, on redimensionne notre surface et on la colle sur la fenêtre principale
self.window.blit( self.window.blit(
@ -122,9 +127,94 @@ class Renderer:
self.window.blit(font.SysFont("Arial", 20).render(f"Zoom: {self.engine.camera.zoom}", self.window.blit(font.SysFont("Arial", 20).render(f"Zoom: {self.engine.camera.zoom}",
True, (255, 0, 0)), (0, 60)) True, (255, 0, 0)), (0, 60))
# On rend maintenant toutes les zones de détection de la fenêtre
for area in self.engine.event_handler.buttons_area:
window_size = display.get_window_size()
if area[2] == 0:
draw.rect(self.window, (255, 255, 0),
(area[0][0] * window_size[0], area[0][1] * window_size[0],
area[0][2] * window_size[0], area[0][3] * window_size[0]), width=1)
elif area[2] == 1:
draw.rect(self.window, (255, 255, 0),
(area[0][0] * window_size[1], area[0][1] * window_size[1],
area[0][2] * window_size[1], area[0][3] * window_size[1]), width=1)
else:
draw.rect(self.window, (255, 255, 0),
area[0], width=1)
# Rendu présent dans tous les types de jeu
self.render_dialogs_box()
# Apres avoir tout rendu, on met à jour l'écran # Apres avoir tout rendu, on met à jour l'écran
display.update() display.update()
def render_dialogs_box(self):
"""Rend la boite de dialogue lorsqu'un dialogue est lancé."""
# Rend le conteneur des dialogues
if self.engine.dialogs_manager.reading_dialog:
resized_box = transform.scale(self.dialogs_box,
(display.get_window_size()[0],
self.dialogs_box.get_height() / self.dialogs_box.get_width() *
display.get_window_size()[0]))
self.window.blit(resized_box, (0, display.get_window_size()[1] - resized_box.get_height()))
# Rend le texte
# On récupère le texte
sentence = self.engine.dialogs_manager.get_current_dialog_sentence()
# On crée la font qui permettra de faire le rendu du texte après
text_font = font.SysFont("Arial", display.get_window_size()[0]//30)
# On calcule la taille du décalage puis on calcule la largeur maximale que peut faire une ligne
x_border = display.get_window_size()[0]/30
max_width = display.get_window_size()[0]-2*x_border
# On passe le texte dans un algorithme qui coupe le texte entre les espaces pour empecher de dépacer la
# taille maximale de la ligne
lines = []
current_line = ""
for i in sentence:
current_line += i
# Si on déplace de la ligne, on ajoute la ligne jusqu'au dernier mot
if text_font.size(current_line)[0] > max_width:
lines.append(current_line[:current_line.rfind(" ")])
current_line = current_line[current_line.rfind(" "):]
# Si la ligne est incomplète, on ajoute la ligne
lines.append(current_line)
# On itère dans les lignes avec un enumerate pour avoir sont index
for i in enumerate(lines):
# On récupère le texte et s'il commence par un espace, on le retire
text = i[1]
if len(text) > 0 and text[0] == " ":
text = text[1:]
# On rend la ligne au bon endroit sur l'écran
rendered_text = text_font.render(text, True, (0, 0, 0))
self.window.blit(rendered_text,
(x_border,
display.get_window_size()[1] - resized_box.get_height() +
display.get_window_size()[0]/30 +
(text_font.get_height()+display.get_window_size()[0]/200)*i[0]))
def render_debug_area(self, rendered_surface: surface.Surface):
"""Rend les zones de collisions et de détections quand le mode DEBUG est activé."""
# 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 itère et on rend toutes les zones de détection
for area in self.engine.event_sheduler.area_callbacks:
area_rect = area[0]
draw.rect(rendered_surface, (200, 100, 0),
(math.floor(x_middle_offset + area_rect[0] - self.engine.camera.x),
math.floor(y_middle_offset + area_rect[1] - self.engine.camera.y),
math.floor(area_rect[2]), math.floor(area_rect[3])), width=1)
def register_shadow(self, file_path: str, name: str): def register_shadow(self, file_path: str, name: str):
"""Enregistre une image d'ombre utilisée pour le rendu des entités.""" """Enregistre une image d'ombre utilisée pour le rendu des entités."""
shadow = image.load(file_path).convert_alpha() shadow = image.load(file_path).convert_alpha()
@ -169,8 +259,8 @@ class Renderer:
frame = transform.scale(frame, (display.get_window_size()[0] / 5, display.get_window_size()[0] / 5)) 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 # 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, 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)) display.get_window_size()[1] / 4 - frame.get_height() / 2))
# On récupère l'image de l'animation du joueur # 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] player_animation = self.boss_fight_player_animations[self.engine.boss_fight_manager.current_player_animation]
@ -180,14 +270,17 @@ class Renderer:
frame = transform.scale(frame, (display.get_window_size()[0] / 5, display.get_window_size()[0] / 5)) 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 # 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)) 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): def render_boss_fight_gui(self):
"""Rend la barre d'action en bas de l'écran pendant le combat de boss.""" """Rend la barre d'action en bas de l'écran pendant le combat de boss."""
resized_container = transform.scale(self.boss_fight_GUI_container, 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])) (display.get_window_size()[0],
self.window.blit(resized_container, (0, display.get_window_size()[1]-resized_container.get_height())) 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): def render_entities(self, rendered_surface: surface.Surface, gui_surface: surface.Surface, delta: float):
"""Rend toutes les entités.""" """Rend toutes les entités."""
@ -235,7 +328,8 @@ class Renderer:
cooldown_value = entity.damage_cooldown / entity.default_damage_cooldown cooldown_value = entity.damage_cooldown / entity.default_damage_cooldown
# On calcule où placer la barre de vei sur la surface des GUI # 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_dest = (
math.floor((entity.x - self.engine.camera.x + x_middle_offset) * self.engine.camera.zoom -
life_bar_width / 2), life_bar_width / 2),
math.floor((entity.y - self.engine.camera.y + y_middle_offset - frame.get_height() / 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)) self.engine.camera.zoom - life_bar_height - life_bar_y_offset))

View file

@ -12,15 +12,22 @@ class Game(Engine):
self.map_manager.load_new("maps/map5.tmj") self.map_manager.load_new("maps/map5.tmj")
self.renderer.load_tile_set("assets/textures/tileset.png", 16) self.renderer.load_tile_set("assets/textures/tileset.png", 16)
self.dialogs_manager.load_dialogs("assets/dialogs.json")
self.create_player_entity() self.create_player_entity()
self.load_boss_fight_assets() self.load_boss_fight_assets()
self.spawn_mobs() self.spawn_mobs()
self.DEBUG_MODE = False self.DEBUG_MODE = True
self.game_state = GameState.NORMAL self.game_state = GameState.NORMAL
self.event_sheduler.register_area((64, 64, 32, 32), lambda _: self.dialogs_manager.start_dialog("test"), ["player"], False, True)
self.renderer.dialogs_box = pygame.image.load("assets/textures/GUI/dialogs_box.png").convert_alpha()
self.event_handler.register_button_area((0, 0, 0.1, 0.1), lambda : print("salut"), 0)
def create_player_entity(self): def create_player_entity(self):
"""Crée une entité joueur.""" """Crée une entité joueur."""
anim = Anim(0.5) anim = Anim(0.5)
@ -61,7 +68,7 @@ class Game(Engine):
mob.set_default_life(5) mob.set_default_life(5)
mob.max_speed = 1. mob.max_speed = 1.
mob.x, mob.y = 160, 16 mob.x, mob.y = 1600, 16
def load_boss_fight_assets(self): def load_boss_fight_assets(self):
"""Charge les animations de combat des combats de boss.""" """Charge les animations de combat des combats de boss."""
@ -72,7 +79,7 @@ class Game(Engine):
boss_none.load_animation_from_directory("assets/textures/boss_fight/boss_sprite/test/none") boss_none.load_animation_from_directory("assets/textures/boss_fight/boss_sprite/test/none")
self.renderer.register_boss_fight_boss_animation(boss_none, "none") self.renderer.register_boss_fight_boss_animation(boss_none, "none")
self.renderer.boss_fight_GUI_container = pygame.image.load("assets/textures/boss_fight/fight_actions_GUI.png") self.renderer.boss_fight_GUI_container = pygame.image.load("assets/textures/boss_fight/fight_actions_GUI.png").convert_alpha()
game = Game() game = Game()