Merge pull request #34 from BreizhHardware/dev_felix

Dev felix
This commit is contained in:
Clément Hervouet
2025-04-09 13:53:31 +02:00
committed by GitHub
17 changed files with 604 additions and 143 deletions

4
.gitignore vendored
View File

@@ -10,4 +10,6 @@
checkpoint.db
checkpoint.db-journal
game.db
map/infinite/*
map/infinite/*
temp_audio.mp3

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
assets/player/Sanic.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View File

@@ -166,7 +166,22 @@
"sprite": "assets/map/exit/Zeldo.png"
}
],
"collectibles": [],
"collectibles": [
{
"id": "jump",
"type": "jump",
"x": 1500,
"y": 400,
"sprite": "assets/map/collectibles/jump.png"
},
{
"id": "speed",
"type": "speed",
"x": 1000,
"y": 400,
"sprite": "assets/map/collectibles/speed.png"
}
],
"spawn_point": {
"x": 50.0,
"y": 350.0

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 # 50% increase in jump power
self.boost_duration = 3 # Duration in seconds
# 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
@@ -76,86 +84,125 @@ class Player(Entity):
self.attack_cooldown = 2000
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()
@@ -250,7 +297,7 @@ class Player(Entity):
if jump and not self.jumping:
jump_sound = pygame.mixer.Sound("assets/sound/Jump.mp3")
jump_sound.play()
self.vel.y = -30
self.vel.y = -self.jump_power
self.jumping = True
# Apply friction
@@ -435,14 +482,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 == 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"""

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 = 3
# 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_ressources: Game resources object containing player speed
"""
if not self.collected:
self.collected = True
# Store original movement speed
original_ACC = game_ressources.ACC
# Apply boost effect
game_ressources.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

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:

View File

@@ -14,6 +14,8 @@ class SpeedrunTimer:
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"""
@@ -38,7 +40,7 @@ class SpeedrunTimer:
else:
self.color = (255, 0, 0) # Red if behind
def save_time(self):
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)
@@ -51,6 +53,8 @@ class SpeedrunTimer:
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
)
"""
@@ -58,8 +62,8 @@ class SpeedrunTimer:
# Insert the new time
cursor.execute(
"INSERT INTO speedrun (level_id, time) VALUES (?, ?)",
(self.level_id, self.current_time),
"INSERT INTO speedrun (level_id, time, collected_items, total_items) VALUES (?, ?, ?, ?)",
(self.level_id, self.current_time, collected_items, total_items),
)
conn.commit()

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:
@@ -137,6 +139,23 @@ 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:
if os.path.isfile(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,6 +1,9 @@
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
@@ -17,6 +20,8 @@ class Leaderboard:
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
@@ -64,7 +69,7 @@ class Leaderboard:
cursor.execute(
"""
SELECT time, date
SELECT time, date, collected_items, total_items
FROM speedrun
WHERE level_id = ?
ORDER BY time ASC
@@ -78,10 +83,10 @@ class Leaderboard:
# Format results
formatted_results = []
for time, date in 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))
formatted_results.append((formatted_date, time, collected, total))
return formatted_results
except (sqlite3.Error, Exception) as e:
@@ -99,10 +104,23 @@ class Leaderboard:
def draw(self, surface):
"""Draw the leaderboard on the given surface."""
title = pygame.font.SysFont("Arial", 48).render(
"Classement", True, (0, 191, 255)
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)
@@ -114,8 +132,22 @@ class Leaderboard:
pygame.draw.rect(surface, (100, 100, 255), button.rect)
button.draw(surface, font)
# Draw column headers
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
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
scores_for_tab = self.scores.get(self.current_tab, [])
if not scores_for_tab:
@@ -127,18 +159,38 @@ class Leaderboard:
(self.WIDTH // 2 - no_scores_text.get_width() // 2, y_pos + 40),
)
else:
for i, (date, time) in enumerate(scores_for_tab):
for i, (date, time, collected, total) 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, (self.WIDTH // 2 - 150, y_pos))
surface.blit(rank_text, (header_positions[0], y_pos))
# Date
date_text = self.font.render(date, True, (255, 255, 255))
surface.blit(date_text, (self.WIDTH // 2 - 100, y_pos))
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, (self.WIDTH // 2 + 50, y_pos))
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
@@ -155,3 +207,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)

View File

@@ -212,6 +212,15 @@ def handle_menu_events(
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
@@ -446,13 +455,28 @@ def draw_playing_state(
checkpoint.activate()
# Handle exit collisions
result = handle_exits(P1, exits, game_resources, level_file, speedrun_timer)
result = handle_exits(
P1, exits, game_resources, level_file, speedrun_timer, collectibles
)
# Handle collectibles
coins_hit = pygame.sprite.spritecollide(P1, collectibles, False)
for coin in coins_hit:
coin.on_collision()
P1.collect_coin(displaysurface)
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)
@@ -460,14 +484,18 @@ def draw_playing_state(
return result
def handle_exits(P1, exits, game_resources, level_file, speedrun_timer=None):
def handle_exits(
P1, exits, game_resources, level_file, speedrun_timer=None, collectibles=[]
):
"""Handle collisions with level exits"""
exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else []
for exit in exits_hit:
if speedrun_timer:
speedrun_timer.stop()
speedrun_timer.save_time()
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
result = handle_exit_collision(exit, game_resources, level_file)
@@ -578,6 +606,7 @@ 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
(
@@ -710,6 +739,21 @@ def handler():
)
)
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))
@@ -732,9 +776,13 @@ def handler():
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,
@@ -794,6 +842,7 @@ def handler():
) = infinite_result
elif current_state == INFINITE:
previous_state = "INFINITE"
# Start infinite mode and switch to playing
(
P1,