diff --git a/.gitignore b/.gitignore index bae480b..813169c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,9 @@ checkpoint.db checkpoint.db-journal game.db -map/infinite/* \ No newline at end of file +map/infinite/* + +temp_audio.mp3 +output.prof + +**/*.pyc \ No newline at end of file diff --git a/CHATGPT.MD b/CHATGPT.MD new file mode 100644 index 0000000..c69feaf --- /dev/null +++ b/CHATGPT.MD @@ -0,0 +1,2 @@ +# Utilisation de LLM +Dans ce projet les LLM (GPT 4o et Claude 3.7 Thinking) ont été utilisés pour du debug (impossible de trouver tous les endroits précisément) ainsi que pour écrire les commits messages. \ No newline at end of file diff --git a/README.md b/README.md index 070000c..2c8f850 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,6 @@ Power-ups Disponibles : Affichés de manière visible pour que le joueur sache q ## Structure du Projet La structure du projet est disponible dans le fichier [PROJECT_STRUCTURE](PROJECT_STRUCTURE.MD). Ce fichier contient une description détaillée de l'organisation des fichiers et des dossiers du projet. + +## Utilisation des LLM +L'utilisation est détaillée dans le fichier [CHATGPT](CHATGPT.MD). \ No newline at end of file diff --git a/assets/map/collectibles/jump.png b/assets/map/collectibles/jump.png new file mode 100644 index 0000000..688d176 Binary files /dev/null and b/assets/map/collectibles/jump.png differ diff --git a/assets/map/collectibles/speed.png b/assets/map/collectibles/speed.png new file mode 100644 index 0000000..9446c08 Binary files /dev/null and b/assets/map/collectibles/speed.png differ diff --git a/assets/map/platform/stone_texture.png b/assets/map/platform/stone_texture.png index 579595a..f35817d 100644 Binary files a/assets/map/platform/stone_texture.png and b/assets/map/platform/stone_texture.png differ diff --git a/assets/player/Sanic.gif b/assets/player/Sanic.gif new file mode 100644 index 0000000..c6994e8 Binary files /dev/null and b/assets/player/Sanic.gif differ diff --git a/assets/sound/Boule de feu.mp3 b/assets/sound/Boule de feu.mp3 new file mode 100644 index 0000000..7cb4a95 Binary files /dev/null and b/assets/sound/Boule de feu.mp3 differ diff --git a/assets/sound/Coin.mp3 b/assets/sound/Coin.mp3 new file mode 100644 index 0000000..5f93aaf Binary files /dev/null and b/assets/sound/Coin.mp3 differ diff --git a/assets/sound/Jump.mp3 b/assets/sound/Jump.mp3 new file mode 100644 index 0000000..454cd1d Binary files /dev/null and b/assets/sound/Jump.mp3 differ diff --git a/assets/sound/main_music.mp3 b/assets/sound/main_music.mp3 new file mode 100644 index 0000000..231d0b9 Binary files /dev/null and b/assets/sound/main_music.mp3 differ diff --git a/map/levels/1.json b/map/levels/1.json index 5e6a6eb..e0f1904 100644 --- a/map/levels/1.json +++ b/map/levels/1.json @@ -8,22 +8,14 @@ { "id": "main_ground", "x": -1000, - "y": 780, + "y": 800, "width": 1800, "height": 200, "texture": "assets/map/platform/grass_texture.png" }, { - "id": "main_ground_2", - "x": 1000, - "y": 900, - "width": 1800, - "height": 200, - "texture": "assets/map/platform/stone_texture.png" - }, - { - "id": "platform1", - "x": 300, + "id": "platform1_01", + "x": 200, "y": 600, "width": 200, "height": 20, @@ -31,80 +23,140 @@ "is_moving": false }, { - "id": "platform2", - "x": 700, + "id": "platform1_02", + "x": 500, "y": 500, - "width": 150, + "width": 200, + "height": 20, + "texture": "assets/map/platform/grass_texture.png", + "is_moving": false + }, + { + "id": "platform1_03", + "x": 900, + "y": 650, + "width": 100, "height": 20, "texture": "assets/map/platform/wood_texture.png", "is_moving": true, "movement": { "type": "linear", "points": [ - {"x": 700, "y": 300}, - {"x": 700, "y": 600} + {"x": 800, "y": 650}, + {"x": 1600, "y": 650} ], - "speed": 2.0, + "speed": 3.0, "wait_time": 1.0 } }, { - "id": "platform21", - "x": 1200, - "y": 750, - "width": 150, + "id": "main_ground_2", + "x": 1700, + "y": 800, + "width": 700, + "height": 200, + "texture": "assets/map/platform/stone_texture.png" + }, + { + "id": "platform2_01", + "x": 2500, + "y": 700, + "width": 200, "height": 20, - "texture": "assets/map/platform/grass_texture.png", + "texture": "assets/map/platform/wood_texture.png", "is_moving": true, "movement": { "type": "linear", "points": [ - {"x": 1200, "y": 550}, - {"x": 1500, "y": 550} + {"x": 2500, "y": 700}, + {"x": 2500, "y": 1200} ], "speed": 2.0, "wait_time": 1.0 } }, { - "id": "platform3", - "x": 1200, - "y": 400, + "id": "platform2_02", + "x": 3300, + "y": 1200, + "width": 200, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": true, + "movement": { + "type": "linear", + "points": [ + {"x": 2800, "y": 1200}, + {"x": 3300, "y": 1200} + ], + "speed": 2.0, + "wait_time": 1.0 + } + }, + { + "id": "main_ground_3", + "x": 3600, + "y": 1400, + "width": 700, + "height": 200, + "texture": "assets/map/platform/stone_texture.png" + }, + { + "id": "platform3_01", + "x": 4600, + "y": 1100, "width": 100, "height": 20, "texture": "assets/map/platform/wood_texture.png", "is_moving": true, "movement": { "type": "circular", - "center": {"x": 1200, "y": 400}, + "center": {"x": 4600, "y": 1100}, "radius": 3, "speed": 0.02, "clockwise": true } + }, + { + "id": "platform3_02", + "x": 3900, + "y": 1200, + "width": 200, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": false + }, + { + "id": "main_ground_4", + "x": 4900, + "y": 1300, + "width": 600, + "height": 200, + "texture": "assets/map/platform/stone_texture.png" } ], "enemies": [ { - "id": "enemy1", + "id": "enemy1_01", "type": "walker", "x": 500, - "y": 660, + "y": 700, "health": 1, "damage": 1, "behavior": "patrol", "patrol_points": [ - {"x": 400, "y": 660}, - {"x": 600, "y": 660} + {"x": 400, "y": 700}, + {"x": 600, "y": 700} ], "speed": 1.5, "sprite_sheet": "assets/map/enemy/walker_enemy.png", "size": [50,50] }, { - "id": "enemy2", + "id": "enemy1_02", "type": "flyer", - "x": 1000, + "x": 700, "y": 400, "health": 1, "damage": 1, @@ -115,9 +167,9 @@ "size": [50,50] }, { - "id": "enemy3", + "id": "enemy2_01", "type": "turret", - "x": 1500, + "x": 2000, "y": 700, "health": 1, "damage": 1, @@ -126,6 +178,35 @@ "attack_range": 300, "sprite_sheet": "assets/map/enemy/turret.gif", "size": [50,100] + }, + { + "id": "enemy3_01", + "type": "walker", + "x": 3600, + "y": 1300, + "health": 1, + "damage": 1, + "behavior": "patrol", + "patrol_points": [ + {"x": 3600, "y": 1300}, + {"x": 3900, "y": 1300} + ], + "speed": 1.5, + "sprite_sheet": "assets/map/enemy/walker_enemy.png", + "size": [50,50] + }, + { + "id": "enemy3_02", + "type": "turret", + "x": 4000, + "y": 1150, + "health": 1, + "damage": 1, + "behavior": "stationary", + "attack_interval": 2.0, + "attack_range": 300, + "sprite_sheet": "assets/map/enemy/turret.gif", + "size": [50,100] } ], @@ -133,15 +214,8 @@ { "id": "coin1", "type": "coin", - "x": 1220, - "y": 320, - "sprite": "assets/map/collectibles/Sanic_Coin.png" - }, - { - "id": "coin2", - "type": "coin", - "x": 400, - "y": 540, + "x": 600, + "y": 300, "sprite": "assets/map/collectibles/Sanic_Coin.png" } ], @@ -149,10 +223,10 @@ "checkpoints": [ { "id": "checkpoint1", - "x": 1200, - "y": 760, + "x": 2200, + "y": 600, "width": 50, - "height": 50, + "height": 125, "sprite": "assets/map/checkpoints/checkpoint.png" } ], @@ -164,8 +238,8 @@ "exits": [ { - "x": 2300, - "y": 700, + "x": 5100, + "y": 1200, "width": 50, "height": 80, "next_level": "Level 2", diff --git a/map/levels/2.json b/map/levels/2.json index 2ac8c3e..91c06a9 100644 --- a/map/levels/2.json +++ b/map/levels/2.json @@ -1,174 +1,456 @@ { "name": "Level 2", - "width": 2400, - "height": 800, + "width": 10500, + "height": 1500, "background": "assets/map/background/forest_bg.jpg", "gravity": 1.0, "platforms": [ { - "id": "platform1", + "id": "main_ground", "x": -1000, - "y": 520, - "width": 1800, + "y": 800, + "width": 1700, "height": 200, - "texture": "assets/map/platform/grass_texture.png", - "is_moving": false + "texture": "assets/map/platform/grass_texture.png" }, { - "id": "platform2", + "id": "platform1_01", "x": 1000, - "y": 600, - "width": 1800, - "height": 200, - "texture": "assets/map/platform/grass_texture.png", - "is_moving": false - }, - { - "id": "platform3", - "x": 300, - "y": 570, - "width": 200, - "height": 20, - "texture": "assets/map/platform/grass_texture.png", - "is_moving": false - }, - { - "id": "platform4", - "x": 700, - "y": 470, - "width": 150, - "height": 20, - "texture": "assets/map/platform/grass_texture.png", - "is_moving": false - }, - { - "id": "platform5", - "x": 900, - "y": 470, - "width": 150, - "height": 20, - "texture": "assets/map/platform/grass_texture.png", - "is_moving": false - }, - { - "id": "platform6", - "x": 1200, - "y": 370, + "y": 700, "width": 100, "height": 20, - "texture": "assets/map/platform/grass_texture.png", - "is_moving": false + "texture": "assets/map/platform/wood_texture.png", + "is_moving": true, + "movement": { + "type": "circular", + "center": {"x": 1000, "y": 700}, + "radius": 2, + "speed": 0.02, + "clockwise": true + } }, { - "id": "platform7", - "x": 300, - "y": 240, - "width": 260, - "height": 40, - "texture": "assets/map/platform/grass_texture.png", - "is_moving": false + "id": "platform1_02", + "x": 1300, + "y": 700, + "width": 200, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": true, + "movement": { + "type": "linear", + "points": [ + {"x": 1300, "y": 700}, + {"x": 1800, "y": 700} + ], + "speed": 3.0, + "wait_time": 1.0 + } }, { - "id": "platform8", - "x": 720, - "y": 220, - "width": 240, - "height": 60, - "texture": "assets/map/platform/grass_texture.png", - "is_moving": false + "id": "platform1_03", + "x": 2100, + "y": 700, + "width": 100, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": true, + "movement": { + "type": "linear", + "points": [ + {"x": 2100, "y": 100}, + {"x": 2100, "y": 700} + ], + "speed": 2.0, + "wait_time": 1.0 + } }, { - "id": "platform9", - "x": 520, + "id": "main_ground_2", + "x": 2300, + "y": 200, + "width": 1000, + "height": 200, + "texture": "assets/map/platform/stone_texture.png" + }, + { + "id": "platform2_01", + "x": 2500, "y": 0, - "width": 240, - "height": 60, - "texture": "assets/map/platform/grass_texture.png", + "width": 100, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", "is_moving": false + }, + { + "id": "platform2_02", + "x": 3400, + "y": 100, + "width": 200, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": false + }, + { + "id": "platform2_03", + "x": 3600, + "y": 300, + "width": 200, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": false + }, + { + "id": "main_ground_3", + "x": 3700, + "y": 600, + "width": 1000, + "height": 200, + "texture": "assets/map/platform/stone_texture.png" + }, + { + "id": "platform3_01", + "x": 4800, + "y": 600, + "width": 200, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": true, + "movement": { + "type": "linear", + "points": [ + {"x": 4800, "y": 600}, + {"x": 4800, "y": 1100} + ], + "speed": 2.0, + "wait_time": 1.0 + } + }, + { + "id": "platform3_02", + "x": 5100, + "y": 1200, + "width": 500, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": false + }, + { + "id": "main_ground_4", + "x": 5700, + "y": 1200, + "width": 900, + "height": 200, + "texture": "assets/map/platform/stone_texture.png" + }, + { + "id": "platform4_01", + "x": 5900, + "y": 1000, + "width": 100, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": false + }, + { + "id": "platform4_02", + "x": 6000, + "y": 900, + "width": 100, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": false + }, + { + "id": "platform4_03", + "x": 6700, + "y": 1300, + "width": 200, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": true, + "movement": { + "type": "linear", + "points": [ + {"x": 6700, "y": 1300}, + {"x": 7300, "y": 1300} + ], + "speed": 3.0, + "wait_time": 1.0 + } + }, + { + "id": "platform4_04", + "x": 7600, + "y": 1150, + "width": 200, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": false + }, + { + "id": "main_ground_5", + "x": 7900, + "y": 1200, + "width": 700, + "height": 200, + "texture": "assets/map/platform/stone_texture.png" + }, + { + "id": "platform5_01", + "x": 8900, + "y": 1200, + "width": 300, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": false + }, + { + "id": "platform5_02", + "x": 9500, + "y": 1200, + "width": 100, + "height": 20, + "texture": "assets/map/platform/wood_texture.png", + "is_moving": true, + "movement": { + "type": "circular", + "center": {"x": 9500, "y": 1200}, + "radius": 2, + "speed": 0.02, + "clockwise": true + } + }, + { + "id": "main_ground_6", + "x": 9800, + "y": 1300, + "width": 700, + "height": 200, + "texture": "assets/map/platform/stone_texture.png" } ], + "enemies": [ { - "id": "enemy1", - "type": "turret", - "x": 260, - "y": 100, - "health": 1, - "damage": 1, - "sprite_sheet": "assets/map/enemy/turret.gif", - "size": [ - 50, - 100 - ], - "behavior": "stationary", - "attack_interval": 2.0, - "attack_range": 300 - }, - { - "id": "enemy2", + "id": "enemy1_01", "type": "walker", - "x": 770, - "y": 140, + "x": 300, + "y": 700, "health": 1, "damage": 1, - "sprite_sheet": "assets/map/enemy/walker_enemy.png", - "size": [ - 50, - 100 - ], "behavior": "patrol", "patrol_points": [ - { - "x": 670, - "y": 140 - }, - { - "x": 870, - "y": 140 - } + {"x": 300, "y": 700}, + {"x": 600, "y": 700} ], - "speed": 1.5 + "speed": 1.5, + "sprite_sheet": "assets/map/enemy/walker_enemy.png", + "size": [50,50] }, { - "id": "enemy3", + "id": "enemy2_01", "type": "flyer", - "x": 420, - "y": 520, + "x": 1600, + "y": 600, "health": 1, "damage": 1, - "sprite_sheet": "assets/map/enemy/flying_enemy.png", - "size": [ - 50, - 100 - ], "behavior": "chase", "detection_radius": 200, - "speed": 2.0 + "speed": 2.0, + "sprite_sheet": "assets/map/enemy/flying_enemy.png", + "size": [50,50] + }, + { + "id": "enemy1_02", + "type": "turret", + "x": 2900, + "y": 75, + "health": 1, + "damage": 1, + "behavior": "stationary", + "attack_interval": 2.0, + "attack_range": 300, + "sprite_sheet": "assets/map/enemy/turret.gif", + "size": [50,100] + }, + { + "id": "enemy2_02", + "type": "flyer", + "x": 3700, + "y": 200, + "health": 1, + "damage": 1, + "behavior": "chase", + "detection_radius": 200, + "speed": 2.0, + "sprite_sheet": "assets/map/enemy/flying_enemy.png", + "size": [50,50] + }, + { + "id": "enemy1_03", + "type": "walker", + "x": 3800, + "y": 450, + "health": 1, + "damage": 1, + "behavior": "patrol", + "patrol_points": [ + {"x": 3800, "y": 450}, + {"x": 4100, "y": 450} + ], + "speed": 1.5, + "sprite_sheet": "assets/map/enemy/walker_enemy.png", + "size": [50,50] + }, + { + "id": "enemy2_03", + "type": "walker", + "x": 4200, + "y": 450, + "health": 1, + "damage": 1, + "behavior": "patrol", + "patrol_points": [ + {"x": 4200, "y": 450}, + {"x": 4600, "y": 450} + ], + "speed": 1.5, + "sprite_sheet": "assets/map/enemy/walker_enemy.png", + "size": [50,50] + }, + { + "id": "enemy1_04", + "type": "flyer", + "x": 6100, + "y": 800, + "health": 1, + "damage": 1, + "behavior": "chase", + "detection_radius": 200, + "speed": 2.0, + "sprite_sheet": "assets/map/enemy/flying_enemy.png", + "size": [50,50] + }, + { + "id": "enemy2_04", + "type": "turret", + "x": 6300, + "y": 1050, + "health": 1, + "damage": 1, + "behavior": "stationary", + "attack_interval": 2.0, + "attack_range": 300, + "sprite_sheet": "assets/map/enemy/turret.gif", + "size": [50,100] + }, + { + "id": "enemy1_05", + "type": "turret", + "x": 8300, + "y": 1050, + "health": 1, + "damage": 1, + "behavior": "stationary", + "attack_interval": 2.0, + "attack_range": 300, + "sprite_sheet": "assets/map/enemy/turret.gif", + "size": [50,100] + }, + { + "id": "enemy1_06", + "type": "flyer", + "x": 9800, + "y": 1100, + "health": 1, + "damage": 1, + "behavior": "chase", + "detection_radius": 200, + "speed": 2.0, + "sprite_sheet": "assets/map/enemy/flying_enemy.png", + "size": [50,50] + }, + { + "id": "enemy2_06", + "type": "walker", + "x": 9900, + "y": 1200, + "health": 1, + "damage": 1, + "behavior": "patrol", + "patrol_points": [ + {"x": 9900, "y": 1200}, + {"x": 10200, "y": 1200} + ], + "speed": 1.5, + "sprite_sheet": "assets/map/enemy/walker_enemy.png", + "size": [50,50] } ], + + "collectibles": [ + { + "id": "coin1", + "type": "coin", + "x": 2500, + "y": -100, + "sprite": "assets/map/collectibles/Sanic_Coin.png" + }, + { + "id": "coin2", + "type": "coin", + "x": 4600, + "y": 800, + "sprite": "assets/map/collectibles/Sanic_Coin.png" + }, + { + "id": "coin3", + "type": "coin", + "x": 6000, + "y": 800, + "sprite": "assets/map/collectibles/Sanic_Coin.png" + }, + { + "id": "jump1", + "type": "jump", + "x": 6500, + "y": 1000, + "sprite": "assets/map/collectibles/jump.png" + }, + { + "id": "speed1", + "type": "speed", + "x": 8000, + "y": 1000, + "sprite": "assets/map/collectibles/speed.png" + } + ], + "checkpoints": [ { "id": "checkpoint1", - "x": 1080, - "y": 450, + "x": 5300, + "y": 1075, "width": 50, - "height": 50, - "sprite": "assets/map/checkpoints/checkpoint_uncheck.png" + "height": 125, + "sprite": "assets/map/checkpoints/checkpoint.png" } ], + + "spawn_point": { + "x": 50, + "y": 650 + }, + "exits": [ { - "x": 2225, - "y": 500, + "x": 10450, + "y": 1000, "width": 50, "height": 80, - "next_level": "map/levels/1.json", + "next_level": "Level 2", "sprite": "assets/map/exit/Zeldo.png" } - ], - "collectibles": [], - "spawn_point": { - "x": 50.0, - "y": 350.0 - } + ] } \ No newline at end of file diff --git a/profiler.py b/profiler.py new file mode 100644 index 0000000..1d04425 --- /dev/null +++ b/profiler.py @@ -0,0 +1,9 @@ +import cProfile +from src.handler import handler + + +def main(): + handler() + + +cProfile.run("main()", "output.prof") diff --git a/src/Database/CheckpointDB.py b/src/Database/CheckpointDB.py index 0c8a317..dce97bc 100644 --- a/src/Database/CheckpointDB.py +++ b/src/Database/CheckpointDB.py @@ -81,3 +81,18 @@ class CheckpointDB: self.conn.commit() except Exception as e: print(f"Error clearing checkpoint database: {e}") + + def reset_level(self, map_name): + """ + Reset the checkpoint for a specific map + + Args: + map_name: Map name to reset + """ + try: + self.cursor.execute( + "DELETE FROM checkpoints WHERE map_name = ?", (map_name,) + ) + self.conn.commit() + except Exception as e: + print(f"Error resetting checkpoint for {map_name}: {e}") diff --git a/src/Database/InfiniteModeDB.py b/src/Database/InfiniteModeDB.py new file mode 100644 index 0000000..627840d --- /dev/null +++ b/src/Database/InfiniteModeDB.py @@ -0,0 +1,56 @@ +import sqlite3 +import os + + +class InfiniteModeDB: + def __init__(self, db_file="game.db"): + """ + Initialize database connection for infinite game mode points management. + + Args: + db_file: SQLite database file path. + """ + os.makedirs( + os.path.dirname(db_file) if os.path.dirname(db_file) else ".", exist_ok=True + ) + self.conn = sqlite3.connect(db_file) + self.cursor = self.conn.cursor() + self._create_tables() + + def _create_tables(self): + """Create required tables if they don't exist.""" + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS InfiniteMode ( + player_name TEXT, + score INTEGER + ) + """ + ) + self.conn.commit() + + def get_all(self): + """Get all scores from the table.""" + self.cursor.execute("SELECT * FROM InfiniteMode") + return self.cursor.fetchall() + + def add_score(self, player_name, score): + """Add a new score to the InfiniteMode.""" + self.cursor.execute( + "INSERT INTO InfiniteMode (player_name, score) VALUES (?, ?)", + (player_name, score), + ) + self.conn.commit() + + def clear_InfiniteModeDB(self): + """Clear all scores from the InfiniteMode table.""" + try: + self.cursor.execute("DELETE FROM InfiniteMode") + self.conn.commit() + except sqlite3.Error as e: + print(f"Error clearing InfiniteMode table: {e}") + + def close(self): + """Close database connection.""" + if self.conn: + self.conn.close() \ No newline at end of file diff --git a/src/Database/LeaderboardDB.py b/src/Database/LeaderboardDB.py new file mode 100644 index 0000000..896bc4a --- /dev/null +++ b/src/Database/LeaderboardDB.py @@ -0,0 +1,56 @@ +import sqlite3 +import os + + +class LeaderboardDB: + def __init__(self, db_file="game.db"): + """ + Initialize database connection for leaderboard management. + + Args: + db_file: SQLite database file path. + """ + os.makedirs( + os.path.dirname(db_file) if os.path.dirname(db_file) else ".", exist_ok=True + ) + self.conn = sqlite3.connect(db_file) + self.cursor = self.conn.cursor() + self._create_tables() + + def _create_tables(self): + """Ensure the Leaderboard table has the correct schema.""" + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS Leaderboard ( + player_name TEXT, + score INTEGER, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + self.conn.commit() + + def get_top_10_scores(self): + """Get top 10 scores from the leaderboard.""" + self.cursor.execute( + "SELECT score, date FROM Leaderboard ORDER BY score DESC LIMIT 10" + ) + return self.cursor.fetchall() + + def add_score(self, player_name, score): + """Add a new score to the leaderboard.""" + self.cursor.execute( + "INSERT INTO Leaderboard (player_name, score) VALUES (?, ?)", + (player_name, score), + ) + self.conn.commit() + + def clear_leaderboard(self): + """Clear all scores from the leaderboard.""" + self.cursor.execute("DELETE FROM Leaderboard") + self.conn.commit() + + def close(self): + """Close database connection.""" + if self.conn: + self.conn.close() \ No newline at end of file diff --git a/src/Database/__pycache__/CheckpointDB.cpython-313.pyc b/src/Database/__pycache__/CheckpointDB.cpython-313.pyc new file mode 100644 index 0000000..ff7a75f Binary files /dev/null and b/src/Database/__pycache__/CheckpointDB.cpython-313.pyc differ diff --git a/src/Database/__pycache__/CoinDB.cpython-313.pyc b/src/Database/__pycache__/CoinDB.cpython-313.pyc new file mode 100644 index 0000000..1d69fc7 Binary files /dev/null and b/src/Database/__pycache__/CoinDB.cpython-313.pyc differ diff --git a/src/Database/__pycache__/InfiniteModeDB.cpython-313.pyc b/src/Database/__pycache__/InfiniteModeDB.cpython-313.pyc new file mode 100644 index 0000000..63495be Binary files /dev/null and b/src/Database/__pycache__/InfiniteModeDB.cpython-313.pyc differ diff --git a/src/Database/__pycache__/LeaderboardDB.cpython-313.pyc b/src/Database/__pycache__/LeaderboardDB.cpython-313.pyc new file mode 100644 index 0000000..5966f7b Binary files /dev/null and b/src/Database/__pycache__/LeaderboardDB.cpython-313.pyc differ diff --git a/src/Database/__pycache__/LevelDB.cpython-313.pyc b/src/Database/__pycache__/LevelDB.cpython-313.pyc new file mode 100644 index 0000000..8c6990a Binary files /dev/null and b/src/Database/__pycache__/LevelDB.cpython-313.pyc differ diff --git a/src/Entity/Exit.py b/src/Entity/Exit.py index cdbcd48..2463ceb 100644 --- a/src/Entity/Exit.py +++ b/src/Entity/Exit.py @@ -1,4 +1,5 @@ import pygame +import os from src.Entity.Entity import Entity from moviepy import VideoFileClip import moviepy as mp @@ -75,9 +76,16 @@ class Exit(Entity): # Extract audio from the video audio = mp.AudioFileClip(video_path) audio.write_audiofile("temp_audio.mp3") - pygame.mixer.init() - pygame.mixer.music.load("temp_audio.mp3") - pygame.mixer.music.play() + + # Pause the main music without stopping it + main_music_pos = ( + pygame.mixer.music.get_pos() / 1000 if pygame.mixer.get_init() else 0 + ) + pygame.mixer.music.pause() + + # Load and play the audio on a separate channel + temp_sound = pygame.mixer.Sound("temp_audio.mp3") + sound_channel = temp_sound.play() for frame in clip.iter_frames(fps=24, dtype="uint8"): frame_surface = pygame.surfarray.make_surface(frame.swapaxes(0, 1)) @@ -85,11 +93,22 @@ class Exit(Entity): pygame.display.flip() clock.tick(24) - clip.close() - pygame.mixer.music.stop() - pygame.mixer.quit() + # Check if the sound channel is still playing + if sound_channel and not sound_channel.get_busy(): + break - # Create and post a return to menu event + clip.close() + + # Play the main music again from the last position + pygame.mixer.music.unpause() + + # Remove the temporary audio file + try: + os.remove("temp_audio.mp3") + except Exception as e: + print(f"Error removing temporary audio file: {e}") + + # Return to the menu return_event = pygame.event.Event( pygame.USEREVENT, {"action": "return_to_menu"} ) diff --git a/src/Entity/JumpBoost.py b/src/Entity/JumpBoost.py new file mode 100644 index 0000000..ac90f7d --- /dev/null +++ b/src/Entity/JumpBoost.py @@ -0,0 +1,112 @@ +import os +import pygame +import time +from src.Entity.Entity import Entity + + +class JumpBoost(Entity): + """ + A collectible that temporarily increases the player's jump power + for 3 seconds when collected. + """ + + def __init__(self, pos, size=(30, 30), color=(0, 255, 0), texturePath=""): + super().__init__(pos=pos, size=size, color=color, texturePath=texturePath) + self.collected = False + + # Jump boost properties + self.boost_factor = 1.5 + self.boost_duration = 10 + + # Create initial surface + self.surf = pygame.Surface(size, pygame.SRCALPHA) + + # Load and scale texture + if texturePath: + try: + if os.path.exists(texturePath): + texture = pygame.image.load(texturePath).convert_alpha() + textureSize = (size[0] * 1.5, size[1] * 1.5) + self.surf = pygame.transform.scale(texture, textureSize) + else: + self.draw_fallback(color, size) + except Exception as e: + self.draw_fallback(color, size) + else: + self.draw_fallback(color, size) + + # Set rect + self.rect = self.surf.get_rect() + self.rect.topleft = pos + + # Animation properties + self.animation_frame = 0 + self.last_update = 0 + + def draw_fallback(self, color, size): + """Draw a green arrow pointing up as fallback""" + self.surf.fill((0, 0, 0, 0)) + + # Draw an arrow pointing up + half_width = size[0] // 2 + height = size[1] + + # Arrow head + head_points = [ + (half_width, 0), + (half_width - 8, 10), + (half_width + 8, 10), + ] + + # Arrow body + body_rect = pygame.Rect(half_width - 4, 10, 8, height - 10) + + pygame.draw.polygon(self.surf, color, head_points) + pygame.draw.rect(self.surf, color, body_rect) + + def update(self): + """Update the jump boost animation""" + now = pygame.time.get_ticks() + if now - self.last_update > 200: + self.last_update = now + self.animation_frame = (self.animation_frame + 1) % 4 + # Simple floating animation + self.rect.y += [-1, 0, 1, 0][self.animation_frame] + + def on_collision(self, player): + """ + Handle jump boost collision with player + + Args: + player: The player object to apply the boost to + """ + if not self.collected: + self.collected = True + + # Store original jump power + original_jump_power = player.jump_power + + # Apply boost effect + player.jump_power *= self.boost_factor + + # Set visual feedback + player.jump_boost_active = True + + # Schedule effect removal + pygame.time.set_timer( + pygame.USEREVENT + 2, # Custom event ID for jump boost expiration + self.boost_duration * 1000, # Convert to milliseconds + 1, # Only trigger once + ) + + # Store reference to restore original jump power + player.active_jump_boost = { + "original_power": original_jump_power, + "boost_object": self, + } + + # Remove the collectible from display + self.kill() + + return True + return False diff --git a/src/Entity/Player.py b/src/Entity/Player.py index ac1b41b..49e5a68 100644 --- a/src/Entity/Player.py +++ b/src/Entity/Player.py @@ -2,6 +2,7 @@ from src.Entity.Entity import Entity from pygame import * import pygame import os +from PIL import Image, ImageSequence from pygame.math import Vector2 as vec from src.Entity.Projectile import Projectile @@ -45,10 +46,17 @@ class Player(Entity): self.dash_start_time = 0 self.dash_duration = 500 # 1/2 second activation time self.dash_cooldown = 3000 # 3 seconds cooldown + self.speed_boost_active = False + self.active_speed_boost = None + + # Jump mechanics + self.jump_power = 30 + self.jump_boost_active = False + self.active_jump_boost = None # Life system - self.max_lives = 2 - self.lives = 2 + self.max_lives = 5 + self.lives = 3 self.invulnerable = False self.invulnerable_timer = 0 self.invulnerable_duration = 1.5 @@ -75,107 +83,174 @@ class Player(Entity): self.attack_start_time = 0 self.attack_cooldown = 2000 + self.facing_right = True + + # Initilize mixer + pygame.mixer.init() + def load_images(self): + """Load images for the player""" try: - # Load static image - if os.path.isfile("assets/player/Sanic Base.png"): - self.static_image = pygame.image.load( - "assets/player/Sanic Base.png" - ).convert_alpha() - self.static_image = pygame.transform.scale( - self.static_image, (100, 100) - ) - - # Load regular animation sprite sheet - if os.path.isfile("assets/player/Sanic Annimate.png"): - sprite_sheet = pygame.image.load( - "assets/player/Sanic Annimate.png" - ).convert_alpha() - - # Extract the 4 frames - frame_height = sprite_sheet.get_height() - frame_width = sprite_sheet.get_width() // 4 - - for i in range(4): - # Cut out a region of the sprite sheet - frame = sprite_sheet.subsurface( - (i * 2290, 0, frame_width, frame_height) - ) - # Resize the frame - frame = pygame.transform.scale(frame, (100, 100)) - self.animation_frames.append(frame) - - # Load jump animation sprite sheet - if os.path.isfile("assets/player/Sanic Boule.png"): - self.jump_frames.append( - pygame.transform.scale( - pygame.image.load( - "assets/player/Sanic Boule.png" - ).convert_alpha(), - (80, 80), - ) - ) - - # Load dash animation sprite sheet - if os.path.isfile("assets/player/Sanic Boule Annimate.png"): - dash_sheet = pygame.image.load( - "assets/player/Sanic Boule Annimate.png" - ).convert_alpha() - - dash_frame_height = dash_sheet.get_height() - - for i in range(4): - frame = dash_sheet.subsurface( - (i * 2000, 0, dash_frame_height, dash_frame_height) - ) - frame = pygame.transform.scale(frame, (80, 80)) - self.dash_frames.append(frame) - - # Load life icon - if os.path.isfile("assets/player/Sanic Head.png"): - self.life_icon = pygame.image.load( - "assets/player/Sanic Head.png" - ).convert_alpha() - self.life_icon = pygame.transform.scale( - self.life_icon, - ( - self.game_resources.life_icon_width, - self.game_resources.life_icon_width, - ), - ) + # Load the 7 frames of the GIF + if os.path.isfile("assets/player/Sanic.gif"): + self.load_gif_frames("assets/player/Sanic.gif") + self.animation_speed = 0.05 else: - # Backup: use a red square - self.life_icon = pygame.Surface( - ( - self.game_resources.life_icon_width, - self.game_resources.life_icon_width, - ) - ) - self.life_icon.fill((255, 0, 0)) + # Fallback to static image if GIF is not found + self.load_static_and_sprite_sheets() + + # Load special animations (jump, dash, ...) + self.load_special_animations() except Exception as e: print(f"Error loading player images: {e}") + def load_gif_frames(self, gif_path): + """Load frames from a GIF file""" + try: + gif = Image.open(gif_path) + + self.animation_frames = [] + + for frame in ImageSequence.Iterator(gif): + # Convert the frame to a format compatible with Pygame + frame_rgb = frame.convert("RGBA") + raw_str = frame_rgb.tobytes("raw", "RGBA") + + pygame_surface = pygame.image.fromstring( + raw_str, frame_rgb.size, "RGBA" + ) + + pygame_surface = pygame.transform.scale(pygame_surface, (125, 125)) + + self.animation_frames.append(pygame_surface) + + # Use the first frame as the static image + if self.animation_frames: + self.static_image = self.animation_frames[0] + + except Exception as e: + print(f"Error while loading the GIF: {e}") + + def load_static_and_sprite_sheets(self): + """Previous method to load static image and sprite sheets""" + # Load static image + if os.path.isfile("assets/player/Sanic Base.png"): + self.static_image = pygame.image.load( + "assets/player/Sanic Base.png" + ).convert_alpha() + self.static_image = pygame.transform.scale(self.static_image, (100, 100)) + + # Load regular animation sprite sheet + if os.path.isfile("assets/player/Sanic Annimate.png"): + sprite_sheet = pygame.image.load( + "assets/player/Sanic Annimate.png" + ).convert_alpha() + + # Extract the 4 frames + frame_height = sprite_sheet.get_height() + frame_width = sprite_sheet.get_width() // 4 + + for i in range(4): + # Cut out a region of the sprite sheet + frame = sprite_sheet.subsurface( + (i * 2290, 0, frame_width, frame_height) + ) + # Resize the frame + frame = pygame.transform.scale(frame, (100, 100)) + self.animation_frames.append(frame) + + def load_special_animations(self): + """Load special animations for jump and dash""" + # Load jump animation sprite sheet + if os.path.isfile("assets/player/Sanic Boule.png"): + self.jump_frames.append( + pygame.transform.scale( + pygame.image.load("assets/player/Sanic Boule.png").convert_alpha(), + (80, 80), + ) + ) + + # Load dash animation sprite sheet + if os.path.isfile("assets/player/Sanic Boule Annimate.png"): + dash_sheet = pygame.image.load( + "assets/player/Sanic Boule Annimate.png" + ).convert_alpha() + + dash_frame_height = dash_sheet.get_height() + + for i in range(4): + frame = dash_sheet.subsurface( + (i * 2000, 0, dash_frame_height, dash_frame_height) + ) + frame = pygame.transform.scale(frame, (80, 80)) + self.dash_frames.append(frame) + + # Load life icon + if os.path.isfile("assets/player/Sanic Head.png"): + self.life_icon = pygame.image.load( + "assets/player/Sanic Head.png" + ).convert_alpha() + self.life_icon = pygame.transform.scale( + self.life_icon, + ( + self.game_resources.life_icon_width, + self.game_resources.life_icon_width, + ), + ) + else: + # Backup: use a red square + self.life_icon = pygame.Surface( + ( + self.game_resources.life_icon_width, + self.game_resources.life_icon_width, + ) + ) + self.life_icon.fill((255, 0, 0)) + def update_animation(self): current_time = pygame.time.get_ticks() + current_image = None + # Priority: Dashing > Jumping > Moving > Static - if self.dashing and self.dash_frames: + if self.dashing and self.dash_frames and len(self.dash_frames) > 0: if current_time - self.last_update > self.animation_speed * 1000: self.current_frame = (self.current_frame + 1) % len(self.dash_frames) - self.surf = self.dash_frames[self.current_frame] self.last_update = current_time - elif self.jumping and self.jump_frames: - self.surf = self.jump_frames[0] # Use jump frame - elif self.moving and self.animation_frames: + + if 0 <= self.current_frame < len(self.dash_frames): + current_image = self.dash_frames[self.current_frame] + + elif self.jumping and self.jump_frames and len(self.jump_frames) > 0: + if 0 < len(self.jump_frames): + current_image = self.jump_frames[0] + + elif self.moving and self.animation_frames and len(self.animation_frames) > 0: if current_time - self.last_update > self.animation_speed * 1000: self.current_frame = (self.current_frame + 1) % len( self.animation_frames ) - self.surf = self.animation_frames[self.current_frame] self.last_update = current_time + + if 0 <= self.current_frame < len(self.animation_frames): + current_image = self.animation_frames[self.current_frame] + elif self.static_image: - self.surf = self.static_image + current_image = self.static_image + + # If no animation is found, use the static imagef available + if not current_image: + if self.static_image: + current_image = self.static_image + else: + return + + # Appliquer le retournement selon la direction + if not self.facing_right: + self.surf = pygame.transform.flip(current_image, True, False) + else: + self.surf = current_image def dash(self, acc): current_time = pygame.time.get_ticks() @@ -209,7 +284,7 @@ class Player(Entity): if self.has_joystick and self.joystick: try: - # Joystick gauche pour mouvement + # Left joystick for movement if self.joystick.get_numaxes() > 0: joystick_x = self.joystick.get_axis(0) if abs(joystick_x) > 0.2: @@ -218,7 +293,7 @@ class Player(Entity): elif joystick_x > 0: move_right = True - # Boutons pour sauter/dasher + # Button for jumping and dashing if self.joystick.get_numbuttons() > self.jump_button: if self.joystick.get_button(self.jump_button): jump = True @@ -227,7 +302,7 @@ class Player(Entity): if self.joystick.get_button(self.dash_button): dash_key = True except pygame.error: - pass # Ignorer les erreurs de manette + pass if move_left: # Check if X is > 0 to prevent player from going off screen @@ -248,7 +323,12 @@ class Player(Entity): # Jumping logic if jump and not self.jumping: - self.vel.y = -30 + try: + jump_sound = pygame.mixer.Sound("assets/sound/Jump.mp3") + jump_sound.play() + except Exception as e: + print(f"Error playing jump sound: {e}") + self.vel.y = -self.jump_power self.jumping = True # Apply friction @@ -350,6 +430,11 @@ class Player(Entity): if fall_distance > 500: self.death() + if self.vel.x > 0: + self.facing_right = True + elif self.vel.x < 0: + self.facing_right = False + def take_damage(self, amount=1): """Reduce life number if not invulnerable""" if not self.invulnerable: @@ -358,7 +443,7 @@ class Player(Entity): if self.lives <= 0: self.death() else: - # Période d'invulnérabilité temporaire + # Temporarily make the player invulnerable self.invulnerable = True self.invulnerable_timer = 0 @@ -377,7 +462,7 @@ class Player(Entity): for i in range(self.max_lives): if i < self.lives: - # Vie active: afficher l'icône normale + # Active life: display the icon surface.blit( self.life_icon, ( @@ -386,9 +471,9 @@ class Player(Entity): ), ) else: - # Vie perdue: afficher l'icône grisée + # Life lost: display a grayscale version of the icon grayscale_icon = self.life_icon.copy() - # Appliquer un filtre gris + # Apply grayscale effect for x in range(grayscale_icon.get_width()): for y in range(grayscale_icon.get_height()): color = grayscale_icon.get_at((x, y)) @@ -433,9 +518,16 @@ class Player(Entity): surface.blit(coin_text, (text_x, text_y)) - def collect_coin(self, surface): + def collect_coin(self, surface, speedrun_timer=None): """Increment coin counter when collecting a coin""" + coin_sound = pygame.mixer.Sound("assets/sound/Coin.mp3") + coin_sound.play() self.coins += 1 + if self.lives < self.max_lives: + self.lives += 1 + self.draw_lives(surface) + if speedrun_timer: + speedrun_timer.collected_items += 1 def attack(self): """Do an attack action on the player""" @@ -491,6 +583,9 @@ class Player(Entity): if pressed_keys[K_q] and pressed_keys[K_v]: if current_time - self.last_attack_time >= self.attack_cooldown: + attack_sound = pygame.mixer.Sound("assets/sound/Boule de feu.mp3") + attack_sound.set_volume(0.4) + attack_sound.play() self.is_attacking = True self.attack_start_time = current_time self.last_attack_time = current_time @@ -539,6 +634,9 @@ class Player(Entity): if pressed_keys[K_d] and pressed_keys[K_v]: if current_time - self.last_attack_time >= self.attack_cooldown: + attack_sound = pygame.mixer.Sound("assets/sound/Boule de feu.mp3") + attack_sound.set_volume(0.4) + attack_sound.play() self.is_attacking = True self.attack_start_time = current_time self.last_attack_time = current_time diff --git a/src/Entity/SpeedBoost.py b/src/Entity/SpeedBoost.py new file mode 100644 index 0000000..c171426 --- /dev/null +++ b/src/Entity/SpeedBoost.py @@ -0,0 +1,110 @@ +import os +import pygame +import time +from src.Entity.Entity import Entity + + +class SpeedBoost(Entity): + """ + A collectible that temporarily increases the player's movement speed + for 3 seconds when collected. + """ + + def __init__(self, pos, size=(30, 30), color=(0, 0, 255), texturePath=""): + super().__init__(pos=pos, size=size, color=color, texturePath=texturePath) + self.collected = False + + # Speed boost properties + self.boost_factor = 2 + self.boost_duration = 10 + + # Create initial surface + self.surf = pygame.Surface(size, pygame.SRCALPHA) + + # Load and scale texture + if texturePath: + try: + if os.path.exists(texturePath): + texture = pygame.image.load(texturePath).convert_alpha() + textureSize = (size[0] * 3, size[1] * 3) + self.surf = pygame.transform.scale(texture, textureSize) + else: + self.draw_fallback(color, size) + except Exception as e: + self.draw_fallback(color, size) + else: + self.draw_fallback(color, size) + + # Set rect + self.rect = self.surf.get_rect() + self.rect.topleft = pos + + # Animation properties + self.animation_frame = 0 + self.last_update = 0 + + def draw_fallback(self, color, size): + """Draw a blue lightning bolt as fallback""" + self.surf.fill((0, 0, 0, 0)) + + # Draw a lightning bolt symbol + width, height = size + points = [ + (width // 2, 0), + (width // 4, height // 2), + (width // 2 - 2, height // 2), + (width // 3, height), + (width // 2 + 5, height // 2 + 5), + (width // 2 + 2, height // 2), + (3 * width // 4, height // 2), + ] + + pygame.draw.polygon(self.surf, color, points) + + def update(self): + """Update the speed boost animation""" + now = pygame.time.get_ticks() + if now - self.last_update > 200: + self.last_update = now + self.animation_frame = (self.animation_frame + 1) % 4 + # Simple floating animation + self.rect.y += [-1, 0, 1, 0][self.animation_frame] + + def on_collision(self, player, game_resources): + """ + Handle speed boost collision with player + + Args: + player: The player object to apply the boost to + game_resources: Game resources object containing player speed + """ + if not self.collected: + self.collected = True + + # Store original movement speed + original_ACC = game_resources.ACC + + # Apply boost effect + game_resources.ACC *= self.boost_factor + + # Set visual feedback + player.speed_boost_active = True + + # Schedule effect removal + pygame.time.set_timer( + pygame.USEREVENT + 3, # Custom event ID for speed boost expiration + self.boost_duration * 1000, # Convert to milliseconds + 1, # Only trigger once + ) + + # Store reference to restore original speed + player.active_speed_boost = { + "original_ACC": original_ACC, + "boost_object": self, + } + + # Remove the collectible from display + self.kill() + + return True + return False diff --git a/src/Entity/__pycache__/Checkpoint.cpython-313.pyc b/src/Entity/__pycache__/Checkpoint.cpython-313.pyc new file mode 100644 index 0000000..c2343ed Binary files /dev/null and b/src/Entity/__pycache__/Checkpoint.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/Coin.cpython-313.pyc b/src/Entity/__pycache__/Coin.cpython-313.pyc new file mode 100644 index 0000000..dc95a46 Binary files /dev/null and b/src/Entity/__pycache__/Coin.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/Enemy.cpython-313.pyc b/src/Entity/__pycache__/Enemy.cpython-313.pyc new file mode 100644 index 0000000..d8deb08 Binary files /dev/null and b/src/Entity/__pycache__/Enemy.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/Entity.cpython-313.pyc b/src/Entity/__pycache__/Entity.cpython-313.pyc new file mode 100644 index 0000000..87c1d7f Binary files /dev/null and b/src/Entity/__pycache__/Entity.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/Exit.cpython-313.pyc b/src/Entity/__pycache__/Exit.cpython-313.pyc new file mode 100644 index 0000000..cc437f0 Binary files /dev/null and b/src/Entity/__pycache__/Exit.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/JumpBoost.cpython-313.pyc b/src/Entity/__pycache__/JumpBoost.cpython-313.pyc new file mode 100644 index 0000000..5ddc982 Binary files /dev/null and b/src/Entity/__pycache__/JumpBoost.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/Platform.cpython-313.pyc b/src/Entity/__pycache__/Platform.cpython-313.pyc new file mode 100644 index 0000000..3483d8a Binary files /dev/null and b/src/Entity/__pycache__/Platform.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/Player.cpython-313.pyc b/src/Entity/__pycache__/Player.cpython-313.pyc new file mode 100644 index 0000000..88a61cf Binary files /dev/null and b/src/Entity/__pycache__/Player.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/Projectile.cpython-313.pyc b/src/Entity/__pycache__/Projectile.cpython-313.pyc new file mode 100644 index 0000000..5c645ec Binary files /dev/null and b/src/Entity/__pycache__/Projectile.cpython-313.pyc differ diff --git a/src/Entity/__pycache__/SpeedBoost.cpython-313.pyc b/src/Entity/__pycache__/SpeedBoost.cpython-313.pyc new file mode 100644 index 0000000..0fe733a Binary files /dev/null and b/src/Entity/__pycache__/SpeedBoost.cpython-313.pyc differ diff --git a/src/Map/Editor/LevelEditor.py b/src/Map/Editor/LevelEditor.py index 79f15b5..0b325cb 100644 --- a/src/Map/Editor/LevelEditor.py +++ b/src/Map/Editor/LevelEditor.py @@ -611,7 +611,7 @@ class LevelEditor: self.selected_object, EditorCollectible ): if event.key == K_t: - types = ["coin"] + types = ["coin", "jump", "speed"] current_index = ( types.index(self.selected_object.collectible_type) if self.selected_object.collectible_type in types @@ -623,6 +623,10 @@ class LevelEditor: # Update appearance based on type if self.selected_object.collectible_type == "coin": self.selected_object.image.fill((255, 215, 0)) + elif self.selected_object.collectible_type == "jump": + self.selected_object.image.fill((0, 255, 0)) + elif self.selected_object.collectible_type == "speed": + self.selected_object.image.fill((0, 0, 255)) elif self.selected_object and isinstance(self.selected_object, EditorExit): if event.key == K_n: diff --git a/src/Map/Editor/__pycache__/EditorSprites.cpython-313.pyc b/src/Map/Editor/__pycache__/EditorSprites.cpython-313.pyc new file mode 100644 index 0000000..3da5c41 Binary files /dev/null and b/src/Map/Editor/__pycache__/EditorSprites.cpython-313.pyc differ diff --git a/src/Map/Editor/__pycache__/LevelEditor.cpython-313.pyc b/src/Map/Editor/__pycache__/LevelEditor.cpython-313.pyc new file mode 100644 index 0000000..384ed75 Binary files /dev/null and b/src/Map/Editor/__pycache__/LevelEditor.cpython-313.pyc differ diff --git a/src/Map/Infinite/InfiniteMapGenerator.py b/src/Map/Infinite/InfiniteMapGenerator.py index 54de8a2..4923692 100644 --- a/src/Map/Infinite/InfiniteMapGenerator.py +++ b/src/Map/Infinite/InfiniteMapGenerator.py @@ -70,13 +70,20 @@ class InfiniteMapGenerator: # Generate additional platforms num_platforms = 10 + difficulty * 2 last_x = 600 + last_y = 260 + max_jump_height = 120 + for i in range(num_platforms): width = random.randint( max(40, 100 - difficulty * 5), max(120, 300 - difficulty * 10) ) gap = random.randint(80, 200) x = last_x + gap - y = random.randint(150, 400) + + min_y = max(150, last_y - max_jump_height) + max_y = min(400, last_y + 80) + + y = random.randint(min_y, max_y) is_moving = random.random() < min(0.1 + difficulty * 0.05, 0.5) @@ -111,6 +118,7 @@ class InfiniteMapGenerator: platforms.append(platform) last_x = x + width + last_y = y return platforms diff --git a/src/Map/Infinite/__pycache__/InfiniteMapGenerator.cpython-313.pyc b/src/Map/Infinite/__pycache__/InfiniteMapGenerator.cpython-313.pyc new file mode 100644 index 0000000..2b1dba3 Binary files /dev/null and b/src/Map/Infinite/__pycache__/InfiniteMapGenerator.cpython-313.pyc differ diff --git a/src/Map/Infinite/__pycache__/InfiniteMapManager.cpython-313.pyc b/src/Map/Infinite/__pycache__/InfiniteMapManager.cpython-313.pyc new file mode 100644 index 0000000..1a1f351 Binary files /dev/null and b/src/Map/Infinite/__pycache__/InfiniteMapManager.cpython-313.pyc differ diff --git a/src/Map/Speedrun/SpeedrunTimer.py b/src/Map/Speedrun/SpeedrunTimer.py new file mode 100644 index 0000000..67f0c20 --- /dev/null +++ b/src/Map/Speedrun/SpeedrunTimer.py @@ -0,0 +1,109 @@ +import pygame +import time +import sqlite3 +from datetime import timedelta + + +class SpeedrunTimer: + def __init__(self, level_id, db_path="game.db"): + self.level_id = level_id + self.db_path = db_path + self.start_time = None + self.current_time = 0 + self.is_running = False + self.best_time = self._get_best_time() + self.color = (0, 255, 0) # Green by default + self.font = pygame.font.Font(None, 36) + self.collected_items = 0 + self.total_items = 0 + + def start(self): + """Start the timer""" + self.start_time = time.time() + self.is_running = True + + def stop(self): + """Stop the timer and return the final time""" + if self.is_running: + self.current_time = time.time() - self.start_time + self.is_running = False + return self.current_time + return self.current_time + + def update(self): + """Update the timer and its color""" + if self.is_running: + self.current_time = time.time() - self.start_time + if self.best_time is not None: + if self.current_time < self.best_time: + self.color = (0, 255, 0) # Green if ahead + else: + self.color = (255, 0, 0) # Red if behind + + def save_time(self, collected_items=0, total_items=0): + """Save the current time in the database""" + if not self.is_running and self.current_time > 0: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Create the table if it doesn't exist + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS speedrun ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level_id TEXT NOT NULL, + time REAL NOT NULL, + collected_items INTEGER DEFAULT 0, + total_items INTEGER DEFAULT 0, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + + # Insert the new time + cursor.execute( + "INSERT INTO speedrun (level_id, time, collected_items, total_items) VALUES (?, ?, ?, ?)", + (self.level_id, self.current_time, collected_items, total_items), + ) + + conn.commit() + conn.close() + + # Update the best time + self.best_time = self._get_best_time() + + def _get_best_time(self): + """Get the best time for this level from the database""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute( + "SELECT MIN(time) FROM speedrun WHERE level_id = ?", (self.level_id,) + ) + + best_time = cursor.fetchone()[0] + conn.close() + + return best_time + except (sqlite3.Error, TypeError): + return None + + def format_time(self, time_value): + """Format the time in minutes:seconds.milliseconds""" + if time_value is None: + return "00:00.000" + + td = timedelta(seconds=time_value) + minutes, seconds = divmod(td.seconds, 60) + milliseconds = td.microseconds // 1000 + return f"{minutes:02}:{seconds:02}.{milliseconds:03}" + + def draw(self, surface): + """Display the timer on the screen""" + if self.level_id == "NEXT_INFINITE_LEVEL": + return # No timer for infinite mode + + formatted_time = self.format_time(self.current_time) + text = self.font.render(formatted_time, True, self.color) + surface.blit(text, (20, surface.get_height() - 50)) diff --git a/src/Map/Speedrun/__pycache__/SpeedrunTimer.cpython-313.pyc b/src/Map/Speedrun/__pycache__/SpeedrunTimer.cpython-313.pyc new file mode 100644 index 0000000..eb6f603 Binary files /dev/null and b/src/Map/Speedrun/__pycache__/SpeedrunTimer.cpython-313.pyc differ diff --git a/src/Map/__pycache__/parser.cpython-313.pyc b/src/Map/__pycache__/parser.cpython-313.pyc new file mode 100644 index 0000000..8f3cbe5 Binary files /dev/null and b/src/Map/__pycache__/parser.cpython-313.pyc differ diff --git a/src/Map/parser.py b/src/Map/parser.py index 407ea4a..9bda5c6 100644 --- a/src/Map/parser.py +++ b/src/Map/parser.py @@ -8,6 +8,8 @@ from src.Entity.Enemy import Enemy from src.Entity.Checkpoint import Checkpoint from src.Entity.Exit import Exit from src.Entity.Coin import Coin +from src.Entity.JumpBoost import JumpBoost +from src.Entity.SpeedBoost import SpeedBoost class MapParser: @@ -122,9 +124,7 @@ class MapParser: self.platforms.add(platform) self.all_sprites.add(platform) - # Create collectibles (requires Collectible class implementation) - # In MapParser.create_map_objects() - # In MapParser.create_map_objects() + # Create collectibles if "collectibles" in map_data: for collectible_data in map_data["collectibles"]: if collectible_data["type"] == "coin": @@ -137,13 +137,31 @@ class MapParser: ) self.collectibles.add(coin) self.all_sprites.add(coin) + elif collectible_data["type"] == "jump": + sprite_path = collectible_data.get("sprite", "") + jump = JumpBoost( + pos=(collectible_data["x"], collectible_data["y"]), + texturePath=sprite_path, + ) + self.collectibles.add(jump) + self.all_sprites.add(jump) + elif collectible_data["type"] == "speed": + sprite_path = collectible_data.get("sprite", "") + speed = SpeedBoost( + pos=(collectible_data["x"], collectible_data["y"]), + texturePath=sprite_path, + ) + self.collectibles.add(speed) + self.all_sprites.add(speed) + # Create background image if "background" in map_data: + map_width = map_data.get("width", self.game_resources.WIDTH) + map_height = map_data.get("height", self.game_resources.HEIGHT) + if os.path.isfile(map_data["background"]): background = pygame.image.load(map_data["background"]).convert_alpha() - background = pygame.transform.scale( - background, (self.game_resources.WIDTH, self.game_resources.HEIGHT) - ) + background = pygame.transform.scale(background, (map_width, map_height)) self.background = background else: print(f"Background image not found: {map_data['background']}") diff --git a/src/Menu/BackgroundManager.py b/src/Menu/BackgroundManager.py new file mode 100644 index 0000000..65a6b07 --- /dev/null +++ b/src/Menu/BackgroundManager.py @@ -0,0 +1,45 @@ +import pygame +import random +import math + + +class BackgroundManager: + def __init__(self, width, height): + self.width = width + self.height = height + self.backgrounds = [ + "assets/map/background/forest_bg.jpg", + "assets/map/background/desert_bg.jpg", + "assets/map/background/mountain_bg.jpg", + "assets/map/background/cave_bg.png", + ] + + self.background_path = random.choice(self.backgrounds) + self.init_time = pygame.time.get_ticks() + + try: + # Load the background image + self.background = pygame.image.load(self.background_path).convert() + bg_width = width * 3 + bg_height = height * 3 + self.background = pygame.transform.scale( + self.background, (bg_width, bg_height) + ) + except Exception as e: + print(f"Erreur lors du chargement du fond d'écran: {e}") + self.background = None + + def draw(self, surface): + if self.background: + parallax_factor = 0.4 + time_factor = pygame.time.get_ticks() / 1000 + + center_x = (self.background.get_width() - surface.get_width()) / 2 + center_y = (self.background.get_height() - surface.get_height()) / 2 + + bg_x = -center_x + math.sin(time_factor) * 50 * parallax_factor + bg_y = -center_y + math.cos(time_factor) * 30 * parallax_factor + + surface.blit(self.background, (bg_x, bg_y)) + else: + surface.fill((0, 0, 0)) diff --git a/src/Menu/Leaderboard.py b/src/Menu/Leaderboard.py index aacc779..0914e64 100644 --- a/src/Menu/Leaderboard.py +++ b/src/Menu/Leaderboard.py @@ -1,33 +1,145 @@ import pygame +import sqlite3 +import os +from datetime import datetime + +from src.Menu.BackgroundManager import BackgroundManager from src.Menu.Button import Button +from src.Database.LevelDB import LevelDB class Leaderboard: - def __init__(self, HEIGHT, WIDTH, font): - self.HEIGHT = HEIGHT + """This class represents the leaderboard menu for the game.""" + + def __init__(self, WIDTH, HEIGHT, font, leaderboard_db, db_path="game.db"): self.WIDTH = WIDTH + self.HEIGHT = HEIGHT self.font = font - self.tabs = ["Mode Normal", "Mode Infini"] + self.db_path = db_path + self.leaderboard_db = leaderboard_db + + self.levels = self.get_available_levels() + self.level_tabs = [f"Level {level}" for level in self.levels] + + self.bg_manager = BackgroundManager(WIDTH, HEIGHT) + + # Define the tabs (levels + infinite mode) + self.tabs = self.level_tabs + ["Infinite mode"] self.current_tab = 0 - self.scores = { - 0: [("Player1", 1000), ("Player2", 800), ("Player3", 600)], - 1: [("Player1", 2000), ("Player2", 1500), ("Player3", 1200)], - } - self.back_button = Button("Retour", 20, self.HEIGHT - 70, 120, 50, "menu") - tab_width = 150 - self.tab_buttons = [ - Button( - self.tabs[0], self.WIDTH // 2 - tab_width, 80, tab_width, 40, "tab_0" - ), - Button(self.tabs[1], self.WIDTH // 2, 80, tab_width, 40, "tab_1"), - ] + + self.back_button = Button("Back", 20, self.HEIGHT - 70, 120, 50, "menu") + + # Add buttons for each tab + self.tab_buttons = [] + tab_width = min(150, (self.WIDTH - 100) / len(self.tabs)) + + for i, tab_name in enumerate(self.tabs): + x_pos = 50 + i * tab_width + self.tab_buttons.append( + Button(tab_name, x_pos, 80, tab_width, 40, f"tab_{i}") + ) + + self.load_scores() + + def get_available_levels(self): + """Get the list of available levels from the database.""" + try: + db = LevelDB() + levels = db.get_all_unlocked_levels() + db.close() + return sorted(set(levels)) # Remove duplicates and sort + except: + return [1] + + def load_scores(self): + """Load scores from the database for each level.""" + self.scores = {} + + # Load scores for each level + for i, level in enumerate(self.levels): + self.scores[i] = self.get_level_scores(str(level)) + + # Load scores for infinite mode + try: + # Get the TOP 10 scores for infinite mode + all_scores = self.leaderboard_db.get_top_10_scores() + + # Format the scores for display + formatted_scores = [] + for score, date in all_scores: + date_obj = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") + formatted_date = date_obj.strftime("%d/%m/%Y") + formatted_scores.append((formatted_date, score)) + + # Assign the formatted scores to the infinite mode tab + self.scores[len(self.levels)] = formatted_scores + except Exception as e: + print(f"Error loading infinite mode scores: {e}") + self.scores[len(self.levels)] = [] + + def get_level_scores(self, level_id): + """Get the top 10 scores for a specific level from the database.""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute( + """ + SELECT time, date, collected_items, total_items + FROM speedrun + WHERE level_id = ? + ORDER BY time ASC + LIMIT 10 + """, + (level_id,), + ) + + results = cursor.fetchall() + conn.close() + + # Format results + formatted_results = [] + for time, date, collected, total in results: + date_obj = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") + formatted_date = date_obj.strftime("%d/%m/%Y") + formatted_results.append((formatted_date, time, collected, total)) + + return formatted_results + except (sqlite3.Error, Exception) as e: + print(f"Error loading scores: {e}") + return [] + + def format_time(self, time_value): + """Format the time in minutes:seconds.milliseconds.""" + from datetime import timedelta + + td = timedelta(seconds=time_value) + minutes, seconds = divmod(td.seconds, 60) + milliseconds = int((time_value * 1000) % 1000) + return f"{minutes:02}:{seconds:02}.{milliseconds:03}" def draw(self, surface): - # Draw title - title = pygame.font.SysFont("Arial", 48).render( - "Classement", True, (0, 191, 255) + """Draw the leaderboard on the given surface.""" + # Refresh scores to ensure the latest data is displayed + self.load_scores() + + self.bg_manager.draw(surface) + + # Draw a semi-transparent panel + panel_rect = pygame.Rect(self.WIDTH // 2 - 250, 130, 500, self.HEIGHT - 200) + panel_surface = pygame.Surface( + (panel_rect.width, panel_rect.height), pygame.SRCALPHA ) + panel_surface.fill((10, 10, 40, 180)) + surface.blit(panel_surface, panel_rect) + + title_font = pygame.font.SysFont("Arial", 48, bold=True) + title = title_font.render("Leaderboard", True, (255, 255, 255)) + title_shadow = title_font.render("Leaderboard", True, (0, 0, 0)) + title_rect = title.get_rect(center=(self.WIDTH // 2, 40)) + shadow_rect = title_shadow.get_rect(center=(self.WIDTH // 2 + 2, 42)) + surface.blit(title_shadow, shadow_rect) surface.blit(title, title_rect) font = pygame.font.SysFont("Arial", 20) @@ -35,27 +147,97 @@ class Leaderboard: # Draw tabs for i, button in enumerate(self.tab_buttons): if i == self.current_tab: - pygame.draw.rect( - surface, - (100, 149, 237), - (button.x, button.y, button.width, button.height), - ) + # Highlight current tab + pygame.draw.rect(surface, (100, 100, 255), button.rect) button.draw(surface, font) + # Determine headers and positions based on the current tab + if self.current_tab == len(self.levels): # Infinite mode + headers = ["Rank", "Date", "Score"] + header_positions = [ + self.WIDTH // 2 - 150, + self.WIDTH // 2 - 50, + self.WIDTH // 2 + 100, + ] + else: # Level scores + headers = ["Rank", "Date", "Time", "Collected"] + header_positions = [ + self.WIDTH // 2 - 150, + self.WIDTH // 2 - 100, + self.WIDTH // 2 + 50, + self.WIDTH // 2 + 150, + ] + + y_pos = 200 + + # Draw column headers + for i, header in enumerate(headers): + header_text = font.render(header, True, (200, 200, 200)) + surface.blit(header_text, (header_positions[i], y_pos - 30)) + # Draw scores - y_pos = 150 - for i, (name, score) in enumerate(self.scores[self.current_tab]): - rank_text = self.font.render( - f"{i+1}. {name}: {score}", True, (255, 255, 255) + scores_for_tab = self.scores.get(self.current_tab, []) + + if not scores_for_tab: + no_scores_text = self.font.render( + "No time saved for this level", True, (255, 255, 255) ) surface.blit( - rank_text, (self.WIDTH // 2 - rank_text.get_width() // 2, y_pos) + no_scores_text, + (self.WIDTH // 2 - no_scores_text.get_width() // 2, y_pos + 40), ) - y_pos += 40 + else: + for i, score_data in enumerate(scores_for_tab): + row_bg = (30, 30, 60, 150) if i % 2 == 0 else (40, 40, 80, 150) + row_rect = pygame.Rect(self.WIDTH // 2 - 200, y_pos - 5, 400, 30) + row_surface = pygame.Surface( + (row_rect.width, row_rect.height), pygame.SRCALPHA + ) + row_surface.fill(row_bg) + surface.blit(row_surface, row_rect) + + # Rank + rank_text = self.font.render(f"{i + 1}.", True, (255, 255, 255)) + surface.blit(rank_text, (header_positions[0], y_pos)) + + if self.current_tab == len(self.levels): # Infinite mode + date, score = score_data + # Date + date_text = self.font.render(date, True, (255, 255, 255)) + surface.blit(date_text, (header_positions[1], y_pos)) + + # Score + score_text = self.font.render(str(score), True, (255, 255, 255)) + surface.blit(score_text, (header_positions[2], y_pos)) + else: # Level scores + date, time, collected, total = score_data + + # Date + date_text = self.font.render(date, True, (255, 255, 255)) + surface.blit(date_text, (header_positions[1], y_pos)) + + # Time + time_text = self.font.render( + self.format_time(time), True, (255, 255, 255) + ) + surface.blit(time_text, (header_positions[2], y_pos)) + + # Collected items + collected_color = (255, 255, 255) + if collected == total: + collected_color = (0, 255, 0) + + collected_text = self.font.render( + f"{collected}/{total}", True, collected_color + ) + surface.blit(collected_text, (header_positions[3], y_pos)) + + y_pos += 40 self.back_button.draw(surface, font) def handle_event(self, event): + """Handle events for the leaderboard menu.""" action = self.back_button.handle_event(event) if action: return action @@ -65,3 +247,25 @@ class Leaderboard: if action and action.startswith("tab_"): self.current_tab = int(action.split("_")[1]) return None + + def refresh_scores(self, previous_level=""): + """Refresh scores from the database.""" + if previous_level != "LEADERBOARD": + self.scores = {} + + # Get the list of levels from the directory + level_dir = "map/levels/" + try: + levels = [ + f.replace(".json", "") + for f in os.listdir(level_dir) + if f.endswith(".json") + ] + + # Get the scores for each level + for level in levels: + scores = self.get_level_scores(level) + if scores: + self.scores[int(level) - 1] = scores + except Exception as e: + print(f"Error while refreshing the score: {e}") diff --git a/src/Menu/LevelEditorSelectionMenu.py b/src/Menu/LevelEditorSelectionMenu.py index 56f5a32..b73243d 100644 --- a/src/Menu/LevelEditorSelectionMenu.py +++ b/src/Menu/LevelEditorSelectionMenu.py @@ -2,6 +2,7 @@ import pygame import os import re +from src.Menu.BackgroundManager import BackgroundManager from src.Menu.Button import Button @@ -21,6 +22,8 @@ class LevelEditorSelectionMenu: self.buttons = [] self.levels = [] + self.bg_manager = BackgroundManager(game_resources.WIDTH, game_resources.HEIGHT) + # Button dimensions self.button_width = 250 self.button_height = 60 @@ -131,6 +134,7 @@ class LevelEditorSelectionMenu: Args: surface: Pygame surface to draw on """ + self.bg_manager.draw(surface) # Draw title title = pygame.font.SysFont("Arial", 48).render( "Level Editor", True, (0, 191, 255) diff --git a/src/Menu/LevelSelectMenu.py b/src/Menu/LevelSelectMenu.py index 6b47595..80ae0e4 100644 --- a/src/Menu/LevelSelectMenu.py +++ b/src/Menu/LevelSelectMenu.py @@ -3,6 +3,7 @@ import os import re from src.Database.LevelDB import LevelDB +from src.Menu.BackgroundManager import BackgroundManager from src.Menu.Button import Button from src.game import clear_checkpoint_database, clear_level_progress @@ -24,6 +25,8 @@ class LevelSelectMenu: self.buttons = [] self.levels = [] + self.bg_manager = BackgroundManager(game_resources.WIDTH, game_resources.HEIGHT) + # Button dimensions self.button_width = 250 self.button_height = 60 @@ -168,6 +171,7 @@ class LevelSelectMenu: Args: surface: Pygame surface to draw on """ + self.bg_manager.draw(surface) # Draw title title = pygame.font.SysFont("Arial", 48).render( "Select Level", True, (0, 191, 255) diff --git a/src/Menu/Menu.py b/src/Menu/Menu.py index 287cb52..bfa3ef0 100644 --- a/src/Menu/Menu.py +++ b/src/Menu/Menu.py @@ -1,6 +1,8 @@ import pygame import random import math + +from src.Menu.BackgroundManager import BackgroundManager from src.Menu.Button import Button @@ -13,32 +15,12 @@ class Menu: button_spacing = 20 start_y = self.game_resources.HEIGHT // 2 - 100 - self.backgrounds = [ - "assets/map/background/forest_bg.jpg", - "assets/map/background/desert_bg.jpg", - "assets/map/background/mountain_bg.jpg", - "assets/map/background/cave_bg.png", - ] - - self.background_path = random.choice(self.backgrounds) - - try: - # Load the background image - self.background = pygame.image.load(self.background_path).convert() - - bg_width = game_resources.WIDTH * 3 - bg_height = game_resources.HEIGHT * 3 - self.background = pygame.transform.scale( - self.background, (bg_width, bg_height) - ) - except Exception as e: - print(f"Error while loading menu background: {e}") - self.background = None + self.bg_manager = BackgroundManager(game_resources.WIDTH, game_resources.HEIGHT) # Create buttons centered horizontally self.buttons.append( Button( - "Jouer", + "Play", self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, @@ -50,7 +32,7 @@ class Menu: start_y += button_height + button_spacing self.buttons.append( Button( - "Jouer en mode infini", + "Play in infinite mode", self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, @@ -62,7 +44,7 @@ class Menu: start_y += button_height + button_spacing self.buttons.append( Button( - "Classement", + "Leaderboard", self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, @@ -74,7 +56,7 @@ class Menu: start_y += button_height + button_spacing self.buttons.append( Button( - "Quitter", + "Quit", self.game_resources.WIDTH // 2 - button_width // 2, start_y, button_width, @@ -84,23 +66,11 @@ class Menu: ) def draw(self, surface): - if self.background: - parallax_factor = 0.4 - time_factor = pygame.time.get_ticks() / 1000 - - center_x = (self.background.get_width() - surface.get_width()) / 2 - center_y = (self.background.get_height() - surface.get_height()) / 2 - - bg_x = -center_x + math.sin(time_factor) * 50 * parallax_factor - bg_y = -center_y + math.cos(time_factor) * 30 * parallax_factor - - surface.blit(self.background, (bg_x, bg_y)) - else: - surface.fill((0, 0, 0)) + self.bg_manager.draw(surface) # Draw title title = pygame.font.SysFont("Arial", 72).render( - "Sanic et la princesse Zeldo", True, (0, 191, 255) + "Sanic and the princess Zeldo", True, (0, 191, 255) ) title_rect = title.get_rect( center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT // 4) diff --git a/src/Menu/__pycache__/BackgroundManager.cpython-313.pyc b/src/Menu/__pycache__/BackgroundManager.cpython-313.pyc new file mode 100644 index 0000000..77effe8 Binary files /dev/null and b/src/Menu/__pycache__/BackgroundManager.cpython-313.pyc differ diff --git a/src/Menu/__pycache__/Button.cpython-313.pyc b/src/Menu/__pycache__/Button.cpython-313.pyc new file mode 100644 index 0000000..5c6122f Binary files /dev/null and b/src/Menu/__pycache__/Button.cpython-313.pyc differ diff --git a/src/Menu/__pycache__/Leaderboard.cpython-313.pyc b/src/Menu/__pycache__/Leaderboard.cpython-313.pyc new file mode 100644 index 0000000..a7f965e Binary files /dev/null and b/src/Menu/__pycache__/Leaderboard.cpython-313.pyc differ diff --git a/src/Menu/__pycache__/LevelEditorSelectionMenu.cpython-313.pyc b/src/Menu/__pycache__/LevelEditorSelectionMenu.cpython-313.pyc new file mode 100644 index 0000000..c467b95 Binary files /dev/null and b/src/Menu/__pycache__/LevelEditorSelectionMenu.cpython-313.pyc differ diff --git a/src/Menu/__pycache__/LevelSelectMenu.cpython-313.pyc b/src/Menu/__pycache__/LevelSelectMenu.cpython-313.pyc new file mode 100644 index 0000000..71efe1c Binary files /dev/null and b/src/Menu/__pycache__/LevelSelectMenu.cpython-313.pyc differ diff --git a/src/Menu/__pycache__/Menu.cpython-313.pyc b/src/Menu/__pycache__/Menu.cpython-313.pyc new file mode 100644 index 0000000..53f570f Binary files /dev/null and b/src/Menu/__pycache__/Menu.cpython-313.pyc differ diff --git a/src/constant.py b/src/constant.py index 06a2c70..38c5ed9 100644 --- a/src/constant.py +++ b/src/constant.py @@ -16,6 +16,12 @@ class GameResources: self.life_icon_width = 50 self.fullscreen = False + try: + icon = pygame.image.load("assets/player/Sanic Head.png") + pygame.display.set_icon(icon) + except Exception as e: + print(f"Erreur lors du chargement de l'icône: {e}") + # Ressources self.platforms = pygame.sprite.Group() self.all_sprites = pygame.sprite.Group() diff --git a/src/game.py b/src/game.py index 6b4dc34..183afad 100644 --- a/src/game.py +++ b/src/game.py @@ -6,6 +6,8 @@ import sys from moviepy.decorators import preprocess_args from pygame.locals import * +from src.Database.InfiniteModeDB import InfiniteModeDB +from src.Database.LeaderboardDB import LeaderboardDB from src.Database.LevelDB import LevelDB from src.Entity.Platform import Platform from src.Entity.Player import Player @@ -25,6 +27,9 @@ def initialize_game(game_resources, map_file="map/levels/1.json"): Returns: tuple: (player, platform, platforms_group, all_sprites, background, checkpoints, exits) """ + checkpointDB = CheckpointDB() + checkpointDB.reset_level(map_file) + checkpointDB.close() parser = MapParser(game_resources) map_objects = parser.load_map(map_file) @@ -135,6 +140,9 @@ def start_infinite_mode(game_resources): game_resources.infinite_manager = infinite_manager game_resources.infinite_mode = True + # Open the temporary database + game_resources.infinite_mode_db = InfiniteModeDB() + # Generate the first level first_level = infinite_manager.start_infinite_mode() diff --git a/src/handler.py b/src/handler.py index c1a83c6..f1a5e11 100644 --- a/src/handler.py +++ b/src/handler.py @@ -1,10 +1,10 @@ import re - import pygame import sys from pygame.locals import * import numpy as np +from src.Database.LeaderboardDB import LeaderboardDB from src.Database.LevelDB import LevelDB from src.Entity.Enemy import Enemy from src.Menu.LevelSelectMenu import LevelSelectMenu @@ -22,65 +22,35 @@ from src.Camera import Camera from src.Database.CheckpointDB import CheckpointDB from src.Map.Editor.LevelEditor import LevelEditor from src.Menu.LevelEditorSelectionMenu import LevelEditorSelectionMenu +from src.Map.Speedrun.SpeedrunTimer import SpeedrunTimer -def handler(): - - # Initialize Pygame and game resources +def initialize_game_resources(): + """Initialize game resources and initial states""" game_resources = GameResources() displaysurface = game_resources.displaysurface - FramePerSec = game_resources.FramePerSec - font = game_resources.font - FPS = game_resources.FPS - WIDTH = game_resources.WIDTH - HEIGHT = game_resources.HEIGHT - ORIGINAL_WIDTH = game_resources.ORIGINAL_WIDTH - ORIGINAL_HEIGHT = game_resources.ORIGINAL_HEIGHT - fullscreen = game_resources.fullscreen + camera = Camera(game_resources.WIDTH, game_resources.HEIGHT, game_resources) - # Add camera initialization - camera = Camera(WIDTH, HEIGHT, game_resources) - - # Game states - MENU = 0 - PLAYING = 1 - INFINITE = 2 - LEADERBOARD = 3 - DEATH_SCREEN = 4 - - # Initialize death screen + # Initialize death screen resources death_timer = 0 death_display_time = 2 - checkpoint_data = None try: death_image = pygame.image.load("assets/player/dead.jpg") except Exception as e: - print(f"Erreur de chargement de l'image: {e}") + print(f"Error loading image: {e}") death_image = None try: death_sound = pygame.mixer.Sound("assets/sound/Death.mp3") death_display_time = death_sound.get_length() except Exception as e: - print(f"Erreur de chargement du son Death.mp3: {e}") + print(f"Error loading Death.mp3 sound: {e}") death_sound = None - death_display_time = 2 - - # Initialize game state and objects - current_state = MENU - main_menu = Menu(game_resources) - level_select_menu = None - level_file = "map/levels/1.json" - current_menu = "main" - leaderboard = Leaderboard(WIDTH, HEIGHT, font) - - clear_checkpoint_database() - projectiles = pygame.sprite.Group() + # Initialize joysticks pygame.joystick.quit() pygame.joystick.init() joysticks = [] - try: for i in range(pygame.joystick.get_count()): joystick = pygame.joystick.Joystick(i) @@ -89,12 +59,615 @@ def handler(): except pygame.error: print("Error while initializing joysticks") + clear_checkpoint_database() + projectiles = pygame.sprite.Group() + + # Game states initialization + current_state = 0 # MENU + current_menu = "main" + main_menu = Menu(game_resources) + level_select_menu = None + editor_select_menu = None + level_file = "map/levels/1.json" + leaderboard_db = LeaderboardDB() + leaderboard = Leaderboard( + game_resources.WIDTH, game_resources.HEIGHT, game_resources.font, leaderboard_db + ) + + try: + pygame.mixer.music.load("assets/sound/main_music.mp3") + pygame.mixer.music.set_volume(0.2) + pygame.mixer.music.play(-1) + except Exception as e: + print(f"Error loading main music: {e}") + + return ( + game_resources, + displaysurface, + camera, + death_timer, + death_display_time, + death_image, + death_sound, + current_state, + current_menu, + main_menu, + level_select_menu, + level_file, + leaderboard, + projectiles, + joysticks, + editor_select_menu, + leaderboard_db, + ) + + +def handle_system_events( + event, current_state, fullscreen, displaysurface, ORIGINAL_WIDTH, ORIGINAL_HEIGHT +): + """Handle system events like quit, resolution changes, etc.""" + if event.type == QUIT: + pygame.quit() + sys.exit() + elif event.type == KEYDOWN: + if event.key == K_ESCAPE: + if current_state in [1, 2]: # PLAYING, INFINITE + current_state = 0 # MENU + else: + pygame.quit() + sys.exit() + elif event.key == K_F11: + fullscreen = not fullscreen + if fullscreen: + # Store current window size before going fullscreen + ORIGINAL_WIDTH, ORIGINAL_HEIGHT = displaysurface.get_size() + displaysurface = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) + else: + # Return to windowed mode with previous size + displaysurface = pygame.display.set_mode( + (ORIGINAL_WIDTH, ORIGINAL_HEIGHT), pygame.RESIZABLE + ) + elif event.type == VIDEORESIZE: + if not fullscreen: + displaysurface = pygame.display.set_mode( + (event.w, event.h), pygame.RESIZABLE + ) + # Update window dimensions + ORIGINAL_WIDTH, ORIGINAL_HEIGHT = event.w, event.h + + return current_state, fullscreen, displaysurface, ORIGINAL_WIDTH, ORIGINAL_HEIGHT + + +def handle_game_events( + event, + current_state, + death_timer, + death_sound, + level_file, + game_resources, + projectiles, +): + """Handle game-specific events like player death and projectile creation""" + checkpoint_data = None + + if event.type == USEREVENT: + if event.dict.get("action") == "player_death": + current_state = 4 # DEATH_SCREEN + death_timer = 0 + if death_sound: + death_sound.play() + + is_infinite_mode = ( + hasattr(game_resources, "infinite_mode") + and game_resources.infinite_mode + ) + + if not is_infinite_mode: + db = CheckpointDB() + checkpoint_data = db.get_checkpoint(level_file) + else: + checkpoint_data = None + + if event.dict.get("action") == "create_projectile": + projectile = event.dict.get("projectile") + projectiles.add(projectile) + + return current_state, death_timer, checkpoint_data, projectiles + + +def handle_menu_events( + event, + current_state, + current_menu, + main_menu, + level_select_menu, + game_resources, + level_file, +): + """Handle menu interaction events""" + P1, PT1, platforms, all_sprites, background, checkpoints, exits, collectibles = [ + None + ] * 8 + editor_select_menu = None + + if current_menu == "main": + action = main_menu.handle_event(event) + if action == "level_select": + level_select_menu = LevelSelectMenu(game_resources) + current_menu = "level_select" + elif action == "infinite": + current_state = 2 # INFINITE + elif action == "leaderboard": + current_state = 3 # LEADERBOARD + elif action == "quit": + pygame.quit() + sys.exit() + elif current_menu == "level_select": + action = level_select_menu.handle_event(event) + if action == "back_to_main": + current_menu = "main" + elif isinstance(action, dict) and action.get("action") == "select_level": + level_file = action.get("level_file") + ( + P1, + PT1, + platforms, + all_sprites, + background, + checkpoints, + exits, + collectibles, + ) = initialize_game(game_resources, level_file) + + level_id = level_file.split("/")[-1].split(".")[0] + speedrun_timer = SpeedrunTimer(level_id) + speedrun_timer.start() + speedrun_timer.total_items = len( + [ + c + for c in collectibles + if hasattr(c, "__class__") + and c.__class__.__name__ not in ["JumpBoost", "SpeedBoost"] + ] + ) + speedrun_timer.collected_items = 0 + + projectiles = pygame.sprite.Group() + current_state = 1 # PLAYING + return ( + current_state, + current_menu, + level_select_menu, + level_file, + P1, + PT1, + platforms, + all_sprites, + background, + checkpoints, + exits, + collectibles, + projectiles, + editor_select_menu, + speedrun_timer, + ) + elif action == "open_editor": + editor_select_menu = LevelEditorSelectionMenu(game_resources) + current_state = "editor_select" + return ( + current_state, + current_menu, + level_select_menu, + level_file, + P1, + PT1, + platforms, + all_sprites, + background, + checkpoints, + exits, + collectibles, + None, + editor_select_menu, + None, + ) + + return ( + current_state, + current_menu, + level_select_menu, + level_file, + P1, + PT1, + platforms, + all_sprites, + background, + checkpoints, + exits, + collectibles, + None, + editor_select_menu, + None, + ) + + +def handle_leaderboard_events(event, current_state, leaderboard): + """Handle leaderboard events""" + action = leaderboard.handle_event(event) + if action == "menu": + current_state = 0 # MENU + return current_state + + +def handle_editor_events( + event, current_state, editor_select_menu, current_menu, game_resources +): + """Handle level editor events""" + level_editor = None + action = editor_select_menu.handle_event(event) + + if action == "back_to_levels": + current_state = 0 # MENU + current_menu = "level_select" + elif isinstance(action, dict): + if action["action"] == "edit_level": + level_editor = LevelEditor(game_resources, action["level_file"]) + current_state = "level_editor" + elif action["action"] == "new_level": + level_editor = LevelEditor(game_resources) + current_state = "level_editor" + + return current_state, current_menu, level_editor + + +def update_playing_state( + P1, platforms, projectiles, WIDTH, HEIGHT, camera, all_sprites +): + """Update game state while playing""" + # Update player + P1.move() + P1.update() + P1.attack() + projectiles.update(WIDTH, HEIGHT, P1, camera) + + # Update camera to follow player + camera.update(P1) + + # Handle moving platforms + for platform in platforms: + if platform.is_moving and platform.movement_type == "linear": + if platform.movement_points[0]["x"] - platform.movement_points[1]["x"] == 0: + dir = 0 + else: + dir = 1 + if P1.rect.colliderect(platform.rect) and P1.pos.y == platform.rect.y: + P1.pos.x += platform.movement_speed * platform.coeff + + platform.move_linear( + dir, + platform.movement_points, + platform.movement_speed, + platform.wait_time, + platform.coeff, + ) + + if platform.is_moving and platform.movement_type == "circular": + if ( + P1.rect.colliderect(platform.rect) + and P1.pos.y == platform.rect.y + and platform.clockwise + ): + P1.pos.x = P1.pos.x + platform.radius * np.cos(platform.angle) + P1.pos.y = P1.pos.y + platform.radius * np.sin(platform.angle) + + if ( + P1.rect.colliderect(platform.rect) + and P1.pos.y == platform.rect.y + and not platform.clockwise + ): + P1.pos.x = P1.pos.x + platform.radius * np.cos(platform.angle) + P1.pos.y = P1.pos.y + platform.radius * np.sin(-platform.angle) + + platform.move_circular( + platform.center, + platform.angular_speed, + platform.radius, + platform.clockwise, + ) + + # Update all sprites + for sprite in all_sprites: + if isinstance(sprite, Enemy): + sprite.update(P1) + projectiles.update(WIDTH, HEIGHT, P1, camera, sprite) + else: + sprite.update() + + +def draw_background(displaysurface, background, camera, WIDTH, HEIGHT): + """Draw background with parallax effect""" + bg_width = WIDTH * 1.5 + bg_height = HEIGHT * 1.5 + + # Resize if necessary + if background.get_width() != bg_width or background.get_height() != bg_height: + background = pygame.transform.scale(background, (bg_width, bg_height)) + + # Parallax effect (smaller factor makes background move slower) + parallax_factor = 0.3 + bg_x = -(camera.camera.x * parallax_factor) % bg_width + bg_y = -(camera.camera.y * parallax_factor) % bg_height + + # Draw background in all directions to create seamless effect + displaysurface.blit(background, (bg_x, bg_y)) + + if bg_x > 0: + displaysurface.blit(background, (bg_x - bg_width, bg_y)) + if bg_x + bg_width < WIDTH: + displaysurface.blit(background, (bg_x + bg_width, bg_y)) + + if bg_y > 0: + displaysurface.blit(background, (bg_x, bg_y - bg_height)) + if bg_x > 0: + displaysurface.blit(background, (bg_x - bg_width, bg_y - bg_height)) + if bg_x + bg_width < WIDTH: + displaysurface.blit(background, (bg_x + bg_width, bg_y - bg_height)) + + if bg_y + bg_height < HEIGHT: + displaysurface.blit(background, (bg_x, bg_y + bg_height)) + if bg_x > 0: + displaysurface.blit(background, (bg_x - bg_width, bg_y + bg_height)) + if bg_x + bg_width < WIDTH: + displaysurface.blit(background, (bg_x + bg_width, bg_y + bg_height)) + + +def draw_playing_state( + displaysurface, + background, + all_sprites, + P1, + camera, + WIDTH, + HEIGHT, + font, + projectiles, + checkpoints, + exits, + collectibles, + game_resources, + level_file, + FramePerSec, + speedrun_timer=None, +): + """Draw game state while playing""" + # Draw background + if background: + draw_background(displaysurface, background, camera, WIDTH, HEIGHT) + + # Draw all sprites with camera offset + for entity in all_sprites: + camera_adjusted_rect = entity.rect.copy() + camera_adjusted_rect.x += camera.camera.x + camera_adjusted_rect.y += camera.camera.y + displaysurface.blit(entity.surf, camera_adjusted_rect) + + # Draw projectiles with camera offset + for projectile in projectiles: + camera_adjusted_rect = projectile.rect.copy() + camera_adjusted_rect.x += camera.camera.x + camera_adjusted_rect.y += camera.camera.y + displaysurface.blit(projectile.surf, camera_adjusted_rect) + + # Handle checkpoints + if checkpoints is not None: + checkpoints_hit = pygame.sprite.spritecollide(P1, checkpoints, False) + for checkpoint in checkpoints_hit: + checkpoint.activate() + + # Handle exit collisions + result = handle_exits(P1, exits, game_resources, level_file, speedrun_timer) + + # Handle collectibles + collectibles_hit = pygame.sprite.spritecollide(P1, collectibles, False) + for collectible in collectibles_hit: + # Vérifier le type de collectible et appeler la méthode appropriée + if ( + hasattr(collectible, "__class__") + and collectible.__class__.__name__ == "JumpBoost" + ): + collectible.on_collision(P1) + elif ( + hasattr(collectible, "__class__") + and collectible.__class__.__name__ == "SpeedBoost" + ): + collectible.on_collision(P1, game_resources) + else: + # Pour les pièces standard et autres collectibles + collectible.on_collision() + P1.collect_coin(displaysurface, speedrun_timer) + + # Draw UI elements + draw_ui_elements(displaysurface, P1, FramePerSec, font, speedrun_timer) + + return result + + +def handle_exits(P1, exits, game_resources, level_file, speedrun_timer=None): + """Handle collisions with level exits""" + exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else [] + for exit in exits_hit: + if speedrun_timer and speedrun_timer.is_running: + collected_coins = speedrun_timer.collected_items + total_coins = speedrun_timer.total_items + + speedrun_timer.stop() + speedrun_timer.save_time(collected_coins, total_coins) + if hasattr(game_resources, "infinite_mode") and game_resources.infinite_mode: + # Infinite mode: load the next level without going back to menu + if hasattr(game_resources, "infinite_mode_db"): + # Zeldo : add 100 points + game_resources.infinite_mode_db.add_score("player", 100) + # Add coins points also + game_resources.infinite_mode_db.add_score("player", P1.coins * 10) + result = handle_exit_collision(exit, game_resources, level_file) + return {"action": "continue_infinite", "result": result} + else: + # Normal mode: unlock the next level and return to menu + current_level_match = re.search(r"(\d+)\.json$", level_file) + if current_level_match: + current_level = int(current_level_match.group(1)) + next_level = current_level + 1 + + # Unlock next level + db = LevelDB() + db.unlock_level(next_level) + db.close() + + # Return to level select menu + return { + "action": "return_to_level_select", + "current_state": 0, # MENU + "current_menu": "level_select", + } + return None + + +def draw_ui_elements(displaysurface, P1, FramePerSec, font, speedrun_timer=None): + """Draw UI elements like FPS, player position, health, etc.""" + # FPS counter + fps = int(FramePerSec.get_fps()) + fps_text = font.render(f"FPS: {fps}", True, (255, 255, 255)) + displaysurface.blit(fps_text, (10, 10)) + + # Player position + pos_text = font.render( + f"X: {int(P1.pos.x)}, Y: {int(P1.pos.y)}", True, (255, 255, 255) + ) + displaysurface.blit(pos_text, (10, 40)) + + # Player UI elements + P1.draw_dash_cooldown_bar(displaysurface) + P1.draw_lives(displaysurface) + P1.draw_coins(displaysurface) + P1.draw_projectiles_amount(displaysurface) + + if speedrun_timer: + speedrun_timer.draw(displaysurface) + + +def handle_death_screen( + P1, + displaysurface, + death_timer, + dt, + death_image, + death_display_time, + checkpoint_data, + level_file, + game_resources, + WIDTH, + HEIGHT, + leaderboard_db, +): + """Handle player death screen""" + # Fill background + displaysurface.fill((0, 0, 0)) + + # Display death image + if death_image: + scaled_image = pygame.transform.scale(death_image, (WIDTH, HEIGHT)) + image_rect = scaled_image.get_rect(center=(WIDTH // 2, HEIGHT // 2)) + displaysurface.blit(scaled_image, image_rect) + + # Update death timer + death_timer += dt + + # Check if death screen should end + if death_timer >= death_display_time: + if checkpoint_data: + # Restart from checkpoint + P1, platforms, all_sprites, background, checkpoints, collectibles = ( + reset_game_with_checkpoint(level_file, game_resources) + ) + projectiles = pygame.sprite.Group() + return { + "action": "restart_level", + "death_timer": 0, + "current_state": 1, # PLAYING + "P1": P1, + "platforms": platforms, + "all_sprites": all_sprites, + "background": background, + "checkpoints": checkpoints, + "collectibles": collectibles, + "projectiles": projectiles, + } + else: + if hasattr(game_resources, "infinite_mode_db"): + # Save score to database + game_resources.infinite_mode_db.add_score("player", P1.coins * 10) + # Get all scores from the database + all_scores = game_resources.infinite_mode_db.get_all() + game_resources.infinite_mode_db.clear_InfiniteModeDB() + game_resources.infinite_mode_db.close() + # Calculate total points, add them to leaderboard table + if(leaderboard_db): + total = 0 + for i in range(len(all_scores)): + total += all_scores[i][1] + leaderboard_db.add_score("player", total) + + # Return to menu + if hasattr(game_resources, "infinite_mode"): + game_resources.infinite_mode = False + return { + "action": "return_to_menu", + "death_timer": 0, + "current_state": 0, # MENU + } + + return {"action": None, "death_timer": death_timer} + + +def handler(): + """Main function that handles the game flow""" + # Game state constants + MENU, PLAYING, INFINITE, LEADERBOARD, DEATH_SCREEN = 0, 1, 2, 3, 4 + previous_state = None + + # Initialize game resources and states + ( + game_resources, + displaysurface, + camera, + death_timer, + death_display_time, + death_image, + death_sound, + current_state, + current_menu, + main_menu, + level_select_menu, + level_file, + leaderboard, + projectiles, + joysticks, + editor_select_menu, + leaderboard_db, + ) = initialize_game_resources() + + # Initialize editor variables + level_editor = None + speedrun_timer = None + # Main game loop running = True while running: try: - events = [] - dt = FramePerSec.get_time() / 1000.0 + # Get delta time + dt = game_resources.FramePerSec.get_time() / 1000.0 + + # Get events try: events = pygame.event.get() except Exception as e: @@ -103,85 +676,188 @@ def handler(): pygame.joystick.init() continue + # Process events for event in events: - if event.type == QUIT: - running = False - pygame.quit() - sys.exit() - elif event.type == KEYDOWN: - if event.key == K_ESCAPE: - if current_state in [PLAYING, INFINITE]: - current_state = MENU - else: - pygame.quit() - sys.exit() - elif event.key == K_F11: - fullscreen = not fullscreen - if fullscreen: - # Store current window size before going fullscreen - ORIGINAL_WIDTH, ORIGINAL_HEIGHT = displaysurface.get_size() - displaysurface = pygame.display.set_mode( - (0, 0), pygame.FULLSCREEN - ) - else: - # Return to windowed mode with previous size - displaysurface = pygame.display.set_mode( - (ORIGINAL_WIDTH, ORIGINAL_HEIGHT), pygame.RESIZABLE - ) - elif ( - event.type == VIDEORESIZE - ): # Fixed indentation - moved out of K_F11 condition - if not fullscreen: - displaysurface = pygame.display.set_mode( - (event.w, event.h), pygame.RESIZABLE - ) - # Update window dimensions - ORIGINAL_WIDTH, ORIGINAL_HEIGHT = event.w, event.h - elif event.type == USEREVENT: - if event.dict.get("action") == "player_death": - current_state = DEATH_SCREEN - death_timer = 0 - if death_sound: - death_sound.play() + # Process system events (quit, resolution changes, etc.) + ( + current_state, + game_resources.fullscreen, + displaysurface, + game_resources.ORIGINAL_WIDTH, + game_resources.ORIGINAL_HEIGHT, + ) = handle_system_events( + event, + current_state, + game_resources.fullscreen, + displaysurface, + game_resources.ORIGINAL_WIDTH, + game_resources.ORIGINAL_HEIGHT, + ) - is_infinite_mode = ( - hasattr(game_resources, "infinite_mode") - and game_resources.infinite_mode - ) - - if not is_infinite_mode: - db = CheckpointDB() - checkpoint_data = db.get_checkpoint(level_file) - else: - checkpoint_data = None - - if event.dict.get("action") == "create_projectile": - projectile = event.dict.get("projectile") - projectiles.add(projectile) - - # Handle menu events + # Process game events based on current state if current_state == MENU: - if current_menu == "main": - action = main_menu.handle_event(event) - if action == "level_select": - level_select_menu = LevelSelectMenu(game_resources) - current_menu = "level_select" - elif action == "infinite": - current_state = INFINITE - elif action == "leaderboard": - current_state = LEADERBOARD - elif action == "quit": - pygame.quit() - sys.exit() - elif current_menu == "level_select": - action = level_select_menu.handle_event(event) - if action == "back_to_main": - current_menu = "main" - elif ( - isinstance(action, dict) - and action.get("action") == "select_level" - ): - level_file = action.get("level_file") + # Handle menu interactions + result = handle_menu_events( + event, + current_state, + current_menu, + main_menu, + level_select_menu, + game_resources, + level_file, + ) + + current_state, current_menu, level_select_menu, level_file = result[ + :4 + ] + if result[4]: # If level has been selected + ( + P1, + PT1, + platforms, + all_sprites, + background, + checkpoints, + exits, + collectibles, + ) = result[4:12] + projectiles = result[12] + editor_select_menu = result[13] + speedrun_timer = result[14] + + elif current_state == LEADERBOARD: + current_state = handle_leaderboard_events( + event, current_state, leaderboard + ) + + elif current_state == "editor_select": + # Create editor_select_menu if it doesn't exist + if editor_select_menu is None: + editor_select_menu = LevelEditorSelectionMenu(game_resources) + + current_state, current_menu, level_editor = handle_editor_events( + event, + current_state, + editor_select_menu, + current_menu, + game_resources, + ) + + elif current_state == "level_editor": + if level_editor is not None: + result = level_editor.handle_event(event) + if result == "back_to_levels": + current_state = "editor_select" + if editor_select_menu is None: + editor_select_menu = LevelEditorSelectionMenu( + game_resources + ) + + # Process general game events (player death, projectiles, etc.) + if event.type == USEREVENT: + current_state, death_timer, checkpoint_data, projectiles = ( + handle_game_events( + event, + current_state, + death_timer, + death_sound, + level_file, + game_resources, + projectiles, + ) + ) + + elif event.type == pygame.USEREVENT + 2: + if hasattr(P1, "active_jump_boost") and P1.active_jump_boost: + P1.jump_power = P1.active_jump_boost["original_power"] + P1.jump_boost_active = False + P1.active_jump_boost = None + + elif event.type == pygame.USEREVENT + 3: # Speed boost expiration + if hasattr(P1, "active_speed_boost") and P1.active_speed_boost: + # Restore original movement speed + game_resources.ACC = P1.active_speed_boost["original_ACC"] + # Remove visual feedback + P1.speed_boost_active = False + # Clear boost data + P1.active_speed_boost = None + + # Clear screen + displaysurface.fill((0, 0, 0)) + + # Update and render based on current state + if current_state == MENU: + if current_menu == "main": + main_menu.draw(displaysurface) + elif current_menu == "level_select": + if level_select_menu is None: + level_select_menu = LevelSelectMenu(game_resources) + level_select_menu.draw(displaysurface) + + elif current_state == "editor_select": + if editor_select_menu is None: + editor_select_menu = LevelEditorSelectionMenu(game_resources) + editor_select_menu.draw(displaysurface) + + elif current_state == "level_editor": + if level_editor is not None: + level_editor.draw(displaysurface) + + elif current_state == LEADERBOARD: + if previous_state != "LEADERBOARD": + leaderboard.refresh_scores(previous_state) + previous_state = "LEADERBOARD" + leaderboard.draw(displaysurface) + + elif current_state == PLAYING: + previous_state = "PLAYING" + # Update game state + update_playing_state( + P1, + platforms, + projectiles, + game_resources.WIDTH, + game_resources.HEIGHT, + camera, + all_sprites, + ) + + if speedrun_timer: + speedrun_timer.update() + + # Draw game state and process exit collisions + exit_result = draw_playing_state( + displaysurface, + background, + all_sprites, + P1, + camera, + game_resources.WIDTH, + game_resources.HEIGHT, + game_resources.font, + projectiles, + checkpoints, + exits, + collectibles, + game_resources, + level_file, + game_resources.FramePerSec, + speedrun_timer, + ) + + # Handle level exit result + if exit_result: + if exit_result.get("action") == "return_to_level_select": + current_state = exit_result["current_state"] + current_menu = exit_result["current_menu"] + level_select_menu = LevelSelectMenu(game_resources) + elif exit_result.get("action") == "continue_infinite": + # Récupérer le résultat du handle_exit_collision + infinite_result = exit_result["result"] + # Utiliser le résultat pour continuer en mode infini + if infinite_result: + # Utiliser les valeurs retournées par handle_exit_collision + # Adapter selon la structure du tuple retourné ( P1, PT1, @@ -191,302 +867,59 @@ def handler(): checkpoints, exits, collectibles, - ) = initialize_game(game_resources, level_file) - projectiles.empty() - current_state = PLAYING - elif action == "open_editor": - editor_select_menu = LevelEditorSelectionMenu( - game_resources - ) - current_state = "editor_select" + ) = infinite_result - # Handle leaderboard events - elif current_state == LEADERBOARD: - action = leaderboard.handle_event(event) - if action == "menu": - current_state = MENU + elif current_state == INFINITE: + previous_state = "INFINITE" + # Start infinite mode and switch to playing + ( + P1, + PT1, + platforms, + all_sprites, + background, + checkpoints, + exits, + collectibles, + ) = start_infinite_mode(game_resources) + current_state = PLAYING - elif current_state == "editor_select": - action = editor_select_menu.handle_event(event) - if action == "back_to_levels": - current_state = MENU - current_menu = "level_select" - elif isinstance(action, dict): - if action["action"] == "edit_level": - level_editor = LevelEditor( - game_resources, action["level_file"] - ) - current_state = "level_editor" - elif action["action"] == "new_level": - level_editor = LevelEditor(game_resources) - current_state = "level_editor" + elif current_state == DEATH_SCREEN: + # Handle death screen + death_result = handle_death_screen( + P1, + displaysurface, + death_timer, + dt, + death_image, + death_display_time, + checkpoint_data, + level_file, + game_resources, + game_resources.WIDTH, + game_resources.HEIGHT, + leaderboard_db + ) + + death_timer = death_result["death_timer"] + + if death_result["action"] == "restart_level": + current_state = death_result["current_state"] + P1 = death_result["P1"] + platforms = death_result["platforms"] + all_sprites = death_result["all_sprites"] + background = death_result["background"] + checkpoints = death_result["checkpoints"] + collectibles = death_result["collectibles"] + projectiles = death_result["projectiles"] + + elif death_result["action"] == "return_to_menu": + current_state = death_result["current_state"] + + # Update display + pygame.display.update() + game_resources.FramePerSec.tick(game_resources.FPS) - elif current_state == "level_editor": - result = level_editor.handle_event(event) - if result == "back_to_levels": - current_state = "editor_select" except Exception as e: - print(f"Error while processing events: {e}") + print(f"Error in main game loop: {e}") continue - - # Clear screen - displaysurface.fill((0, 0, 0)) - - # Draw appropriate screen based on state - if current_state == MENU: - if current_menu == "main": - main_menu.draw(displaysurface) - elif current_menu == "level_select": - level_select_menu.draw(displaysurface) - elif current_state == "editor_select": - editor_select_menu.draw(displaysurface) - elif current_state == "level_editor": - level_editor.draw(displaysurface) - elif current_state == LEADERBOARD: - leaderboard.draw(displaysurface) - - elif current_state == PLAYING: - # Regular game code - P1.move() - P1.update() - P1.attack() - projectiles.update(WIDTH, HEIGHT, P1, camera) - - # Update camera to follow player - camera.update(P1) - - # Clear screen - displaysurface.fill((0, 0, 0)) - - for platform in platforms: - if platform.is_moving and platform.movement_type == "linear": - if ( - platform.movement_points[0]["x"] - - platform.movement_points[1]["x"] - == 0 - ): - dir = 0 - else: - dir = 1 - if ( - P1.rect.colliderect(platform.rect) - and P1.pos.y == platform.rect.y - ): - P1.pos.x += platform.movement_speed * platform.coeff - - platform.move_linear( - dir, - platform.movement_points, - platform.movement_speed, - platform.wait_time, - platform.coeff, - ) - - if platform.is_moving and platform.movement_type == "circular": - if ( - P1.rect.colliderect(platform.rect) - and P1.pos.y == platform.rect.y - and platform.clockwise - ): - P1.pos.x = P1.pos.x + platform.radius * np.cos(platform.angle) - P1.pos.y = P1.pos.y + platform.radius * np.sin(platform.angle) - - if ( - P1.rect.colliderect(platform.rect) - and P1.pos.y == platform.rect.y - and not platform.clockwise - ): - P1.pos.x = P1.pos.x + platform.radius * np.cos(platform.angle) - P1.pos.y = P1.pos.y + platform.radius * np.sin(-platform.angle) - - platform.move_circular( - platform.center, - platform.angular_speed, - platform.radius, - platform.clockwise, - ) - - if background: - bg_width = WIDTH * 1.5 - bg_height = HEIGHT * 1.5 - - # Resize the background if necessary - if ( - background.get_width() != bg_width - or background.get_height() != bg_height - ): - background = pygame.transform.scale( - background, (bg_width, bg_height) - ) - - # Parallax effect factor (if small, the background moves slower) - parallax_factor = 0.3 - - # Calculate the background position based on camera position - bg_x = -(camera.camera.x * parallax_factor) % bg_width - bg_y = -(camera.camera.y * parallax_factor) % bg_height - - displaysurface.blit(background, (bg_x, bg_y)) - - # Draw the background in all four corners to create a seamless effect - if bg_x > 0: - displaysurface.blit(background, (bg_x - bg_width, bg_y)) - if bg_x + bg_width < WIDTH: - displaysurface.blit(background, (bg_x + bg_width, bg_y)) - - if bg_y > 0: - displaysurface.blit(background, (bg_x, bg_y - bg_height)) - if bg_x > 0: - displaysurface.blit( - background, (bg_x - bg_width, bg_y - bg_height) - ) - if bg_x + bg_width < WIDTH: - displaysurface.blit( - background, (bg_x + bg_width, bg_y - bg_height) - ) - - if bg_y + bg_height < HEIGHT: - displaysurface.blit(background, (bg_x, bg_y + bg_height)) - if bg_x > 0: - displaysurface.blit( - background, (bg_x - bg_width, bg_y + bg_height) - ) - if bg_x + bg_width < WIDTH: - displaysurface.blit( - background, (bg_x + bg_width, bg_y + bg_height) - ) - - # Draw all sprites with camera offset applied - for entity in all_sprites: - # Calculate position adjusted for camera - camera_adjusted_rect = entity.rect.copy() - camera_adjusted_rect.x += camera.camera.x - camera_adjusted_rect.y += camera.camera.y - displaysurface.blit(entity.surf, camera_adjusted_rect) - - for sprite in all_sprites: - if isinstance(sprite, Enemy): - sprite.update(P1) - projectiles.update(WIDTH, HEIGHT, P1, camera, sprite) - else: - sprite.update() - - for projectile in projectiles: - # Calculate position adjusted for camera (comme pour les autres sprites) - camera_adjusted_rect = projectile.rect.copy() - camera_adjusted_rect.x += camera.camera.x - camera_adjusted_rect.y += camera.camera.y - displaysurface.blit(projectile.surf, camera_adjusted_rect) - - if checkpoints is not None: - checkpoints_hit = pygame.sprite.spritecollide(P1, checkpoints, False) - else: - checkpoints_hit = [] - for checkpoint in checkpoints_hit: - checkpoint.activate() - - exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else [] - for exit in exits_hit: - if ( - hasattr(game_resources, "infinite_mode") - and game_resources.infinite_mode - ): - # Mod infinit : load the next level without the menu - ( - P1, - P1T, - platforms, - all_sprites, - background, - checkpoints, - exits, - collectibles, - ) = handle_exit_collision(exit, game_resources, level_file) - else: - # Mod normal : unlock the next level and return to the menu - current_level_match = re.search(r"(\d+)\.json$", level_file) - if current_level_match: - current_level = int(current_level_match.group(1)) - next_level = current_level + 1 - - # Unlock next level - db = LevelDB() - db.unlock_level(next_level) - db.close() - - # Return to level select menu - current_state = MENU - current_menu = "level_select" - level_select_menu = LevelSelectMenu(game_resources) - - # Display FPS and coordinates (fixed position UI elements) - fps = int(FramePerSec.get_fps()) - fps_text = font.render(f"FPS: {fps}", True, (255, 255, 255)) - displaysurface.blit(fps_text, (10, 10)) - - coins_hit = pygame.sprite.spritecollide( - P1, collectibles, False - ) # Set to False to handle removal in on_collision - for coin in coins_hit: - coin.on_collision() # This will handle the coin removal - P1.collect_coin( - displaysurface - ) # This updates the player's coin counter - - P1.draw_dash_cooldown_bar(displaysurface) - - pos_text = font.render( - f"X: {int(P1.pos.x)}, Y: {int(P1.pos.y)}", True, (255, 255, 255) - ) - displaysurface.blit(pos_text, (10, 40)) - - P1.draw_dash_cooldown_bar(displaysurface) - P1.draw_lives(displaysurface) - P1.draw_coins(displaysurface) - P1.draw_projectiles_amount(displaysurface) - - elif current_state == INFINITE: - ( - P1, - P1T, - platforms, - all_sprites, - background, - checkpoints, - exits, - collectibles, - ) = start_infinite_mode(game_resources) - current_state = PLAYING - - elif current_state == LEADERBOARD: - leaderboard.draw(displaysurface) - - elif current_state == DEATH_SCREEN: - displaysurface.fill((0, 0, 0)) - - if death_image: - scaled_image = pygame.transform.scale(death_image, (WIDTH, HEIGHT)) - image_rect = scaled_image.get_rect(center=(WIDTH // 2, HEIGHT // 2)) - displaysurface.blit(scaled_image, image_rect) - - # Timer for death screen - death_timer += dt - if death_timer >= death_display_time: - if checkpoint_data: - ( - P1, - platforms, - all_sprites, - background, - checkpoints, - collectibles, - ) = reset_game_with_checkpoint(level_file, game_resources) - projectiles.empty() - current_state = PLAYING - else: - if hasattr(game_resources, "infinite_mode"): - game_resources.infinite_mode = False - current_state = MENU - - pygame.display.update() - FramePerSec.tick(FPS) diff --git a/temp_audio.mp3 b/temp_audio.mp3 deleted file mode 100644 index a0b5a11..0000000 Binary files a/temp_audio.mp3 and /dev/null differ