mirror of
https://github.com/BreizhHardware/project_sanic.git
synced 2026-03-18 21:50:33 +01:00
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,4 +10,6 @@
|
||||
checkpoint.db
|
||||
checkpoint.db-journal
|
||||
game.db
|
||||
map/infinite/*
|
||||
map/infinite/*
|
||||
|
||||
temp_audio.mp3
|
||||
BIN
assets/map/collectibles/jump.png
Normal file
BIN
assets/map/collectibles/jump.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
assets/map/collectibles/speed.png
Normal file
BIN
assets/map/collectibles/speed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
BIN
assets/player/Sanic.gif
Normal file
BIN
assets/player/Sanic.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
@@ -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
112
src/Entity/JumpBoost.py
Normal 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
|
||||
@@ -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
110
src/Entity/SpeedBoost.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]):
|
||||
|
||||
45
src/Menu/BackgroundManager.py
Normal file
45
src/Menu/BackgroundManager.py
Normal 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))
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user