Merge pull request #39 from BreizhHardware/dev

dev to main
This commit is contained in:
Félix MARQUET
2025-04-10 12:00:13 +02:00
committed by GitHub
61 changed files with 2412 additions and 758 deletions

7
.gitignore vendored
View File

@@ -10,4 +10,9 @@
checkpoint.db
checkpoint.db-journal
game.db
map/infinite/*
map/infinite/*
temp_audio.mp3
output.prof
**/*.pyc

2
CHATGPT.MD Normal file
View File

@@ -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.

View File

@@ -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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 51 KiB

BIN
assets/player/Sanic.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

BIN
assets/sound/Coin.mp3 Normal file

Binary file not shown.

BIN
assets/sound/Jump.mp3 Normal file

Binary file not shown.

BIN
assets/sound/main_music.mp3 Normal file

Binary file not shown.

View File

@@ -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",

View File

@@ -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
}
]
}

9
profiler.py Normal file
View File

@@ -0,0 +1,9 @@
import cProfile
from src.handler import handler
def main():
handler()
cProfile.run("main()", "output.prof")

View File

@@ -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}")

View File

@@ -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()

View File

@@ -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()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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"}
)

112
src/Entity/JumpBoost.py Normal file
View File

@@ -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

View File

@@ -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

110
src/Entity/SpeedBoost.py Normal file
View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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:

Binary file not shown.

View File

@@ -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

View File

@@ -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))

Binary file not shown.

View File

@@ -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']}")

View File

@@ -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))

View File

@@ -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}")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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()

View File

@@ -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()

File diff suppressed because it is too large Load Diff

Binary file not shown.