Merge pull request #40 from BreizhHardware/dev_felix

Dev felix
This commit is contained in:
Clément Hervouet
2025-04-10 11:58:34 +02:00
committed by GitHub
16 changed files with 531 additions and 177 deletions

5
.gitignore vendored
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 51 KiB

BIN
assets/sound/main_music.mp3 Normal file

Binary file not shown.

View File

@@ -224,9 +224,9 @@
{
"id": "checkpoint1",
"x": 2200,
"y": 700,
"y": 600,
"width": 50,
"height": 50,
"height": 125,
"sprite": "assets/map/checkpoints/checkpoint.png"
}
],

View File

@@ -1,189 +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
}
],
"checkpoints": [
"speed": 2.0,
"sprite_sheet": "assets/map/enemy/flying_enemy.png",
"size": [50,50]
},
{
"id": "checkpoint1",
"x": 1080,
"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,
"width": 50,
"height": 50,
"sprite": "assets/map/checkpoints/checkpoint_uncheck.png"
}
],
"exits": [
"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]
},
{
"x": 2225,
"y": 500,
"width": 50,
"height": 80,
"next_level": "map/levels/1.json",
"sprite": "assets/map/exit/Zeldo.png"
"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": "jump",
"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": 1500,
"y": 400,
"x": 6500,
"y": 1000,
"sprite": "assets/map/collectibles/jump.png"
},
{
"id": "speed",
"id": "speed1",
"type": "speed",
"x": 1000,
"y": 400,
"x": 8000,
"y": 1000,
"sprite": "assets/map/collectibles/speed.png"
}
],
"checkpoints": [
{
"id": "checkpoint1",
"x": 5300,
"y": 1075,
"width": 50,
"height": 125,
"sprite": "assets/map/checkpoints/checkpoint.png"
}
],
"spawn_point": {
"x": 50.0,
"y": 350.0
}
"x": 50,
"y": 650
},
"exits": [
{
"x": 10450,
"y": 1000,
"width": 50,
"height": 80,
"next_level": "Level 2",
"sprite": "assets/map/exit/Zeldo.png"
}
]
}

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

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

View File

@@ -15,8 +15,8 @@ class JumpBoost(Entity):
self.collected = False
# Jump boost properties
self.boost_factor = 1.5 # 50% increase in jump power
self.boost_duration = 3 # Duration in seconds
self.boost_factor = 1.5
self.boost_duration = 10
# Create initial surface
self.surf = pygame.Surface(size, pygame.SRCALPHA)

View File

@@ -83,7 +83,9 @@ class Player(Entity):
self.attack_start_time = 0
self.attack_cooldown = 2000
# Initialize mixer
self.facing_right = True
# Initilize mixer
pygame.mixer.init()
def load_images(self):
@@ -209,23 +211,46 @@ class Player(Entity):
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()
@@ -259,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:
@@ -268,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
@@ -277,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
@@ -405,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:
@@ -413,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
@@ -432,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,
(
@@ -441,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))

View File

@@ -16,7 +16,7 @@ class SpeedBoost(Entity):
# Speed boost properties
self.boost_factor = 2
self.boost_duration = 3
self.boost_duration = 10
# Create initial surface
self.surf = pygame.Surface(size, pygame.SRCALPHA)
@@ -76,16 +76,16 @@ class SpeedBoost(Entity):
Args:
player: The player object to apply the boost to
game_ressources: Game resources object containing player speed
game_resources: Game resources object containing player speed
"""
if not self.collected:
self.collected = True
# Store original movement speed
original_ACC = game_ressources.ACC
original_ACC = game_resources.ACC
# Apply boost effect
game_ressources.ACC *= self.boost_factor
game_resources.ACC *= self.boost_factor
# Set visual feedback
player.speed_boost_active = True

View File

@@ -124,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":
@@ -158,11 +156,12 @@ class MapParser:
# 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

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

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

View File

@@ -74,6 +74,13 @@ def initialize_game_resources():
game_resources.WIDTH, game_resources.HEIGHT, game_resources.font, leaderboard_db
)
try:
pygame.mixer.music.load("assets/sound/main_music.mp3")
pygame.mixer.music.set_volume(0.2)
pygame.mixer.music.play(-1)
except Exception as e:
print(f"Error loading main music: {e}")
return (
game_resources,
displaysurface,
@@ -458,9 +465,7 @@ def draw_playing_state(
checkpoint.activate()
# Handle exit collisions
result = handle_exits(
P1, exits, game_resources, level_file, speedrun_timer, collectibles
)
result = handle_exits(P1, exits, game_resources, level_file, speedrun_timer)
# Handle collectibles
collectibles_hit = pygame.sprite.spritecollide(P1, collectibles, False)
@@ -487,9 +492,7 @@ def draw_playing_state(
return result
def handle_exits(
P1, exits, game_resources, level_file, speedrun_timer=None, collectibles=[]
):
def handle_exits(P1, exits, game_resources, level_file, speedrun_timer=None):
"""Handle collisions with level exits"""
exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else []
for exit in exits_hit:

Binary file not shown.