Merge pull request #43 from BreizhHardware/dev

Dev to main
This commit is contained in:
Clément Hervouet
2025-04-10 23:25:08 +02:00
committed by GitHub
52 changed files with 1496 additions and 288 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored
View File

@@ -15,4 +15,5 @@ map/infinite/*
temp_audio.mp3
output.prof
**/*.pyc
**/*.pyc
__pycache__/

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/map/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/map/background/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/map/enemy/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/map/platform/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/player/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/sound/execuse_me.mp3 Normal file

Binary file not shown.

View File

@@ -177,7 +177,7 @@
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [50,100]
"size": [80,80]
},
{
"id": "enemy3_01",
@@ -206,7 +206,7 @@
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [50,100]
"size": [80,80]
}
],

View File

@@ -272,7 +272,7 @@
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [50,100]
"size": [80,80]
},
{
"id": "enemy2_02",
@@ -343,7 +343,7 @@
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [50,100]
"size": [80,80]
},
{
"id": "enemy1_05",
@@ -356,7 +356,7 @@
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [50,100]
"size": [80,80]
},
{
"id": "enemy1_06",

View File

@@ -1,44 +1,895 @@
{
"name": "Level 3",
"width": 2400,
"height": 800,
"background": "assets/map/background/forest_bg.jpg",
"width": 10500,
"height": 1500,
"background": "assets/map/background/desert_bg.jpg",
"gravity": 1.0,
"platforms": [
{
"id": "platform1",
"x": 100,
"y": 140,
"width": 540,
"height": 160,
"texture": "assets/map/platform/grass_texture.png",
"id": "main_ground",
"x": -1000,
"y": 800,
"width": 1500,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform1_01",
"x": 500,
"y": 550,
"width": 300,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform2",
"x": 320,
"y": 40,
"id": "platform1_02",
"x": 900,
"y": 400,
"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": 900, "y": 100},
{"x": 900, "y": 400}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "main_ground_2",
"x": 1200,
"y": 200,
"width": 1400,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform2_01",
"x": 2100,
"y": 0,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "main_ground_3",
"x": 2900,
"y": 400,
"width": 600,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform3_01",
"x": 3800,
"y": 600,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "circular",
"center": {"x": 3800, "y": 600},
"radius": 2,
"speed": 0.02,
"clockwise": false
}
},
{
"id": "platform3_02",
"x": 4100,
"y": 300,
"width": 300,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform3_03",
"x": 4400,
"y": 200,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 4400, "y": 200},
{"x": 4900, "y": 200}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "platform3_04",
"x": 5100,
"y": 100,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform3_05",
"x": 3600,
"y": 800,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "main_ground_4",
"x": 2900,
"y": 1000,
"width": 700,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform4_01",
"x": 2600,
"y": 1200,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "circular",
"center": {"x": 2600, "y": 1200},
"radius": 2,
"speed": 0.02,
"clockwise": false
}
},
{
"id": "platform4_02",
"x": 2000,
"y": 900,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform4_03",
"x": 2100,
"y": 1300,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform4_04",
"x": 2400,
"y": 1400,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 2400, "y": 1400},
{"x": 2400, "y": 1800}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "platform4_05",
"x": 3000,
"y": 1800,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 2600, "y": 1800},
{"x": 3000, "y": 1800}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "platform4_06",
"x": 3200,
"y": 1800,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 3200, "y": 1800},
{"x": 3600, "y": 1800}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "main_ground_5",
"x": 3800,
"y": 1900,
"width": 1000,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform5_01",
"x": 4900,
"y": 1900,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 4900, "y": 1900},
{"x": 5400, "y": 1900}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "platform5_02",
"x": 5600,
"y": 1900,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform5_03",
"x": 5900,
"y": 1900,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 5900, "y": 1100},
{"x": 5900, "y": 1900}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "main_ground_6",
"x": 3900,
"y": 1000,
"width": 900,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform6_01",
"x": 4900,
"y": 1000,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 4900, "y": 1000},
{"x": 5400, "y": 1000}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "platform6_02",
"x": 5600,
"y": 1100,
"width": 500,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "main_ground_7",
"x": 6300,
"y": 1100,
"width": 800,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform7_01",
"x": 6500,
"y": 900,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform7_02",
"x": 6700,
"y": 800,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform7_03",
"x": 6300,
"y": 700,
"width": 400,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform7_04",
"x": 7400,
"y": 800,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "circular",
"center": {"x": 7400, "y": 800},
"radius": 2,
"speed": 0.02,
"clockwise": true
}
},
{
"id": "platform7_05",
"x": 7700,
"y": 800,
"width": 300,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform7_06",
"x": 8100,
"y": 800,
"width": 400,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform7_07",
"x": 8500,
"y": 700,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 8500, "y": 700},
{"x": 8900, "y": 700}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "platform7_08",
"x": 7700,
"y": 1200,
"width": 300,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform7_09",
"x": 8100,
"y": 1200,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 8100, "y": 1200},
{"x": 8600, "y": 1200}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "main_ground_8",
"x": 9100,
"y": 800,
"width": 400,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform8_01",
"x": 9600,
"y": 700,
"width": 200,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": true,
"movement": {
"type": "linear",
"points": [
{"x": 9600, "y": 700},
{"x": 9600, "y": 1100}
],
"speed": 3.0,
"wait_time": 1.0
}
},
{
"id": "main_ground_9",
"x": 8900,
"y": 1300,
"width": 2600,
"height": 200,
"texture": "assets/map/platform/stone_texture.png"
},
{
"id": "platform9_01",
"x": 10300,
"y": 1100,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform9_02",
"x": 10400,
"y": 1000,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
},
{
"id": "platform9_03",
"x": 10500,
"y": 1100,
"width": 100,
"height": 20,
"texture": "assets/map/platform/wood_texture.png",
"is_moving": false
}
],
"enemies": [],
"checkpoints": [],
"exits": [
"enemies": [
{
"x": 355,
"y": -760,
"width": 50,
"height": 80,
"next_level": "map/levels/1.json",
"sprite": "assets/map/exit/Zeldo.png"
"id": "enemy_01",
"type": "flyer",
"x": 1200,
"y": 0,
"health": 1,
"damage": 1,
"behavior": "chase",
"detection_radius": 200,
"speed": 2.0,
"sprite_sheet": "assets/map/enemy/flying_enemy.png",
"size": [50,50]
},
{
"id": "enemy_02",
"type": "turret",
"x": 1400,
"y": 75,
"health": 1,
"damage": 1,
"behavior": "stationary",
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [75,100]
},
{
"id": "enemy_03",
"type": "walker",
"x": 1600,
"y": 75,
"health": 1,
"damage": 1,
"behavior": "patrol",
"patrol_points": [
{"x": 1600, "y": 75},
{"x": 1900, "y": 75}
],
"speed": 1.5,
"sprite_sheet": "assets/map/enemy/walker_enemy.png",
"size": [50,50]
},
{
"id": "enemy_04",
"type": "flyer",
"x": 2000,
"y": -100,
"health": 1,
"damage": 1,
"behavior": "chase",
"detection_radius": 200,
"speed": 2.0,
"sprite_sheet": "assets/map/enemy/flying_enemy.png",
"size": [50,50]
},
{
"id": "enemy_05",
"type": "walker",
"x": 2900,
"y": 275,
"health": 1,
"damage": 1,
"behavior": "patrol",
"patrol_points": [
{"x": 2900, "y": 275},
{"x": 3300, "y": 275}
],
"speed": 1.5,
"sprite_sheet": "assets/map/enemy/walker_enemy.png",
"size": [50,50]
},
{
"id": "enemy_06",
"type": "flyer",
"x": 3400,
"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": "enemy_07",
"type": "turret",
"x": 3000,
"y": 875,
"health": 1,
"damage": 1,
"behavior": "stationary",
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [75,100]
},
{
"id": "enemy_08",
"type": "turret",
"x": 2100,
"y": 875,
"health": 1,
"damage": 1,
"behavior": "stationary",
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [75,100]
},
{
"id": "enemy_09",
"type": "flyer",
"x": 3100,
"y": 1600,
"health": 1,
"damage": 1,
"behavior": "chase",
"detection_radius": 200,
"speed": 2.0,
"sprite_sheet": "assets/map/enemy/flying_enemy.png",
"size": [50,50]
},
{
"id": "enemy_10",
"type": "walker",
"x": 3900,
"y": 1775,
"health": 1,
"damage": 1,
"behavior": "patrol",
"patrol_points": [
{"x": 3900, "y": 1775},
{"x": 4200, "y": 1775}
],
"speed": 1.5,
"sprite_sheet": "assets/map/enemy/walker_enemy.png",
"size": [50,50]
},
{
"id": "enemy_11",
"type": "turret",
"x": 4500,
"y": 1775,
"health": 1,
"damage": 1,
"behavior": "stationary",
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [75,100]
},
{
"id": "enemy_12",
"type": "walker",
"x": 3900,
"y": 875,
"health": 1,
"damage": 1,
"behavior": "patrol",
"patrol_points": [
{"x": 3900, "y": 875},
{"x": 4200, "y": 875}
],
"speed": 1.5,
"sprite_sheet": "assets/map/enemy/walker_enemy.png",
"size": [50,50]
},
{
"id": "enemy_13",
"type": "flyer",
"x": 4100,
"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": "enemy_14",
"type": "walker",
"x": 4200,
"y": 875,
"health": 1,
"damage": 1,
"behavior": "patrol",
"patrol_points": [
{"x": 4200, "y": 875},
{"x": 4500, "y": 875}
],
"speed": 1.5,
"sprite_sheet": "assets/map/enemy/walker_enemy.png",
"size": [50,50]
},
{
"id": "enemy_15",
"type": "turret",
"x": 4300,
"y": 875,
"health": 1,
"damage": 1,
"behavior": "stationary",
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [75,100]
},
{
"id": "enemy_16",
"type": "turret",
"x": 6300,
"y": 675,
"health": 1,
"damage": 1,
"behavior": "stationary",
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [75,100]
},
{
"id": "enemy_17",
"type": "flyer",
"x": 7900,
"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": "enemy_18",
"type": "flyer",
"x": 7900,
"y": 700,
"health": 1,
"damage": 1,
"behavior": "chase",
"detection_radius": 200,
"speed": 2.0,
"sprite_sheet": "assets/map/enemy/flying_enemy.png",
"size": [50,50]
},
{
"id": "enemy_19",
"type": "turret",
"x": 9400,
"y": 675,
"health": 1,
"damage": 1,
"behavior": "stationary",
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [75,100]
},
{
"id": "enemy_20",
"type": "walker",
"x": 8900,
"y": 1175,
"health": 1,
"damage": 1,
"behavior": "patrol",
"patrol_points": [
{"x": 8900, "y": 1175},
{"x": 9200, "y": 1175}
],
"speed": 1.5,
"sprite_sheet": "assets/map/enemy/walker_enemy.png",
"size": [50,50]
},
{
"id": "enemy_21",
"type": "flyer",
"x": 9100,
"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": "enemy_22",
"type": "turret",
"x": 9800,
"y": 1175,
"health": 1,
"damage": 1,
"behavior": "stationary",
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret.gif",
"size": [75,100]
},
{
"id": "boss",
"type": "boss",
"x": 10000,
"y": 1200,
"health": 3,
"damage": 1,
"behavior": "boss",
"attack_interval": 1.0,
"attack_range": 1000,
"sprite_sheet": "assets/map/enemy/boss.gif",
"size": [200,200]
}
],
"collectibles": [],
"collectibles": [
{
"id": "coin1",
"type": "coin",
"x": 2200,
"y": -50,
"sprite": "assets/map/collectibles/Sanic_Coin.png"
},
{
"id": "coin2",
"type": "coin",
"x": 5100,
"y": 50,
"sprite": "assets/map/collectibles/Sanic_Coin.png"
},
{
"id": "coin3",
"type": "coin",
"x": 4700,
"y": 1750,
"sprite": "assets/map/collectibles/Sanic_Coin.png"
},
{
"id": "coin4",
"type": "coin",
"x": 6500,
"y": 650,
"sprite": "assets/map/collectibles/Sanic_Coin.png"
},
{
"id": "coin5",
"type": "coin",
"x": 9200,
"y": 650,
"sprite": "assets/map/collectibles/Sanic_Coin.png"
},
{
"id": "jump1",
"type": "jump",
"x": 400,
"y": 650,
"sprite": "assets/map/collectibles/jump.png"
},
{
"id": "jump2",
"type": "jump",
"x": 700,
"y": 500,
"sprite": "assets/map/collectibles/jump.png"
},
{
"id": "speed1",
"type": "speed",
"x": 2300,
"y": -100,
"sprite": "assets/map/collectibles/speed.png"
}
],
"checkpoints": [
{
"id": "checkpoint1",
"x": 6000,
"y": 975,
"width": 50,
"height": 125,
"sprite": "assets/map/checkpoints/checkpoint.png"
}
],
"spawn_point": {
"x": 260.0,
"y": 0.0
}
"x": 50,
"y": 650
},
"exits": [
{
"x": 11300,
"y": 1150,
"width": 50,
"height": 80,
"next_level": "Level 2",
"sprite": "assets/map/exit/Zeldo.png"
}
]
}

View File

@@ -135,6 +135,19 @@
"attack_interval": 2.0,
"attack_range": 300,
"sprite_sheet": "assets/map/enemy/turret_enemy.png"
},
{
"id": "boss",
"type": "boss",
"x": 2000,
"y": 0,
"health": 3,
"damage": 1,
"behavior": "boss",
"attack_interval": 1.0,
"attack_range": 1000,
"sprite_sheet": "assets/map/enemy/boss.gif",
"size": [200,200]
}
],

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -45,7 +45,7 @@ class CheckpointDB:
try:
self.cursor.execute(
"INSERT OR REPLACE INTO checkpoints (map_name, pos_x, pos_y, timestamp) VALUES (?, ?, ?, strftime('%s'))",
(map_name, pos_x, pos_y),
(map_name, pos_x, pos_y + 100),
)
self.conn.commit()
except Exception as e:

View File

@@ -1,5 +1,4 @@
import pygame
import os
from PIL import Image, ImageSequence
import random
from src.Entity.Entity import Entity
@@ -31,13 +30,13 @@ class Enemy(Entity):
sprite_path = enemy_data.get("sprite_sheet", "assets/enemy/default_enemy.png")
# Load sprite sheet or GIF depending on enemy type and file extension
if sprite_path.lower().endswith(".gif") and self.enemy_type == "turret":
self.load_gif_frames(sprite_path)
if sprite_path.lower().endswith(".gif"):
self.load_gif_frames(sprite_path, self.size)
if self.frames:
self.surf = self.frames[0]
else:
# Default sprite
self.surf = pygame.Surface((40, 40))
self.surf = pygame.Surface(self.size)
self.surf.fill((255, 0, 0))
else:
try:
@@ -45,7 +44,7 @@ class Enemy(Entity):
self.surf = pygame.transform.scale(self.surf, self.size)
except:
# Default sprite
self.surf = pygame.Surface((40, 40))
self.surf = pygame.Surface(self.size)
self.surf.fill((255, 0, 0))
# Initial rectangle
@@ -64,7 +63,7 @@ class Enemy(Entity):
self.is_attacking = False
self.detected_player = False
def load_gif_frames(self, gif_path):
def load_gif_frames(self, gif_path, size=(80, 80)):
"""Load frames from a GIF file"""
try:
gif = Image.open(gif_path)
@@ -74,7 +73,7 @@ class Enemy(Entity):
frame_surface = pygame.image.fromstring(
frame.convert("RGBA").tobytes(), frame.size, "RGBA"
)
frame_surface = pygame.transform.scale(frame_surface, (80, 80))
frame_surface = pygame.transform.scale(frame_surface, size)
self.frames.append(frame_surface)
frame_count += 1
@@ -93,9 +92,11 @@ class Enemy(Entity):
self.chase(player)
elif self.behavior == "stationary" and player:
self.stationary_attack(player)
elif self.behavior == "boss" and player:
self.boss(player)
# Animation management for turret enemies
if self.enemy_type == "turret" and self.frames:
if (self.enemy_type == "turret" or self.enemy_type == "boss") and self.frames:
self.animation_timer += dt
if self.animation_timer >= self.animation_speed:
self.animation_timer = 0
@@ -153,6 +154,8 @@ class Enemy(Entity):
if self.attack_timer >= self.attack_interval:
self.attack_timer = 0
# Easter egg sound
pygame.mixer.Sound("assets/sound/execuse_me.mp3").play()
self.attack(player)
def attack(self, player):
@@ -160,7 +163,7 @@ class Enemy(Entity):
self.is_attacking = True
# For turret-type enemies, create a projectile
if self.enemy_type == "turret":
if self.enemy_type == "turret" or self.enemy_type == "boss":
# Calculate direction to player
direction = vec(player.pos.x - self.pos.x, player.pos.y - self.pos.y)
@@ -210,3 +213,27 @@ class Enemy(Entity):
# KB LOL MINECRAFT
knockback_direction = 1 if player.pos.x > self.pos.x else -1
player.vel.x = knockback_direction * 8
def boss(self, player, FPS=60):
"""
Boss behavior: combine horizontal chase with turret-like attacks
"""
# Follow the player horizontally (x axis)
if abs(player.pos.x - self.pos.x) > 50:
direction = 1 if player.pos.x > self.pos.x else -1
self.pos.x += direction * self.speed
self.direction = direction
# Attack the player if within range
distance_to_player = vec(
player.pos.x - self.pos.x, player.pos.y - self.pos.y
).length()
if distance_to_player <= self.attack_range:
self.attack_timer += 1 / FPS
if self.attack_timer >= self.attack_interval:
self.attack_timer = 0
self.attack(player)
self.detected_player = distance_to_player <= self.detection_radius

View File

@@ -11,6 +11,7 @@ class Entity(pygame.sprite.Sprite):
self.pos = vec(pos)
self.vel = vec(0, 0)
self.acc = vec(0, 0)
self.alive = True
# Default surface
self.surf = pygame.Surface(size)

View File

@@ -24,9 +24,11 @@ class Exit(Entity):
sprite_path (str, optional): Path to the sprite image for the exit
"""
super().__init__(pos=(x, y), size=(width, height), color=(0, 255, 0))
self.next_level = next_level # Store the next level to load
self.next_level = next_level
self.active = True # Flag to prevent multiple triggers
self.player = None # Will store the player reference
self.player = None
self.boss = None
self.locked = False
# Load sprite if provided
if sprite_path:
@@ -46,16 +48,33 @@ class Exit(Entity):
"""
self.player = player
def set_boss(self, boss):
"""
Set the boss to this exit. The exit will be locked until the boss is defeated.
Args:
boss: The boss entity to check defeat status with
"""
self.boss = boss
self.locked = True
def update(self):
"""
Check for collision with the player and trigger level completion.
"""
# Check if the boss is defeated
if self.boss:
if hasattr(self.boss, "alive") and not self.boss.alive:
self.locked = False
elif not hasattr(self.boss, "alive"):
self.locked = False
# Skip collision check if player reference is not set
if not self.player or not self.active:
return
# Check if player is colliding with exit
if self.rect.colliderect(self.player.rect):
if self.rect.colliderect(self.player.rect) and not self.locked:
# Play the video and return to menu
self.play_video_and_return_to_menu("assets/map/exit/Zeldo Motus.mp4")
self.active = False # Prevent multiple triggers

View File

@@ -0,0 +1,44 @@
import pygame
class FloatingText:
def __init__(self, text, player, game_resources, duration=2000):
self.text = text
self.player = player
self.game_resources = game_resources
self.creation_time = pygame.time.get_ticks()
self.duration = duration
self.font = pygame.font.SysFont("Arial", 24, bold=True)
self.color = (255, 50, 0)
self.offset_y = -50
self.alpha = 255
def update(self):
current_time = pygame.time.get_ticks()
elapsed = current_time - self.creation_time
# Fade out
if elapsed > self.duration * 0.7:
fade_time = self.duration * 0.3
self.alpha = max(0, 255 * (1 - (elapsed - self.duration * 0.7) / fade_time))
return elapsed < self.duration
def draw(self, surface, camera_offset=None):
if camera_offset is None:
camera_offset = pygame.math.Vector2(0, 0)
pos_x = self.player.rect.centerx - camera_offset.x
pos_y = self.player.rect.top - camera_offset.y + self.offset_y
text_surface = self.font.render(self.text, True, self.color)
text_surface.set_alpha(self.alpha)
text_rect = text_surface.get_rect(center=(pos_x, pos_y))
bg_rect = text_rect.inflate(20, 10)
bg_surface = pygame.Surface((bg_rect.width, bg_rect.height), pygame.SRCALPHA)
bg_color = (0, 0, 0, int(self.alpha * 0.7))
pygame.draw.rect(bg_surface, bg_color, bg_surface.get_rect(), border_radius=5)
surface.blit(bg_surface, bg_rect)
surface.blit(text_surface, text_rect)

View File

@@ -4,6 +4,8 @@ import pygame
import os
from PIL import Image, ImageSequence
from pygame.math import Vector2 as vec
from src.Entity.FloatingText import FloatingText
from src.Entity.Projectile import Projectile
@@ -19,6 +21,8 @@ class Player(Entity):
self.jump_button = 0
self.dash_button = 1
self.attack_button = 2
self.menu_button = 3
try:
if pygame.joystick.get_count() > 0:
@@ -63,6 +67,7 @@ class Player(Entity):
self.life_icon = None
self.rect = self.surf.get_rect()
self.floating_texts = []
# Load images
self.load_images()
@@ -435,6 +440,8 @@ class Player(Entity):
elif self.vel.x < 0:
self.facing_right = False
self.floating_texts = [text for text in self.floating_texts if text.update()]
def take_damage(self, amount=1):
"""Reduce life number if not invulnerable"""
if not self.invulnerable:
@@ -535,156 +542,177 @@ class Player(Entity):
self.is_attacking = False
current_time = pygame.time.get_ticks()
pressed_keys = pygame.key.get_pressed()
if pressed_keys[K_q] and pressed_keys[K_c]:
if current_time - self.last_attack_time >= self.attack_cooldown:
self.is_attacking = True
self.attack_start_time = current_time
self.last_attack_time = current_time
# Calculate direction to player
direction = vec(self.pos.x, self.pos.y)
projectile = Projectile(
pos=vec(self.pos.x, self.pos.y),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
if pressed_keys[K_d] and pressed_keys[K_c]:
if current_time - self.last_attack_time >= self.attack_cooldown:
self.is_attacking = True
self.attack_start_time = current_time
self.last_attack_time = current_time
# Calculate direction to player
direction = vec(self.pos.x, self.pos.y)
projectile = Projectile(
pos=vec(self.pos.x, self.pos.y),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
joystick_attack = False
if self.has_joystick and self.joystick:
try:
if self.joystick.get_numbuttons() > self.attack_button:
joystick_attack = self.joystick.get_button(self.attack_button)
except pygame.error:
pass
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
# Calculate direction to player
direction = vec(-self.pos.x, 0)
projectile = Projectile(
pos=vec(self.pos.x - 50, self.pos.y - 50),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
texturePath="assets/player/Boule de feu.png",
size=(50, 50),
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
if self.projectiles > 0:
self.is_attacking = True
self.attack_start_time = current_time
self.last_attack_time = current_time
# Calculate direction to player
direction = vec(-self.pos.x, 0)
projectile = Projectile(
pos=vec(self.pos.x - 50, self.pos.y - 50),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
texturePath="assets/player/Boule de feu.png",
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
self.projectiles -= 1
if (
joystick_attack
and current_time - self.last_attack_time >= self.attack_cooldown
and self.projectiles > 0
):
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
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
# Calculate direction to player
# Direction en fonction de où le personnage est tourné
if self.facing_right:
direction = vec(self.pos.x, 0)
projectile = Projectile(
pos=vec(self.pos.x + 50, self.pos.y - 50),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
texturePath="assets/player/Boule de feu.png",
size=(50, 50),
position = vec(self.pos.x + 50, self.pos.y - 50)
else:
direction = vec(-self.pos.x, 0)
position = vec(self.pos.x - 50, self.pos.y - 50)
projectile = Projectile(
pos=position,
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
texturePath="assets/player/Boule de feu.png",
size=(50, 50),
)
# Ajouter le projectile au groupe de sprites
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
self.projectiles -= 1
if (
pressed_keys[K_q]
and pressed_keys[K_c]
and current_time - self.last_attack_time >= self.attack_cooldown
):
self.is_attacking = True
self.attack_start_time = current_time
self.last_attack_time = current_time
# Calculate direction to player
direction = vec(self.pos.x, self.pos.y)
projectile = Projectile(
pos=vec(self.pos.x, self.pos.y),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
if self.projectiles > 0:
self.is_attacking = True
self.attack_start_time = current_time
self.last_attack_time = current_time
# Calculate direction to player
direction = vec(self.pos.x, 0)
projectile = Projectile(
pos=vec(self.pos.x + 50, self.pos.y - 50),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
texturePath="assets/player/Boule de feu.png",
)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
self.projectiles -= 1
)
if (
pressed_keys[K_d]
and pressed_keys[K_c]
and current_time - self.last_attack_time >= self.attack_cooldown
):
self.is_attacking = True
self.attack_start_time = current_time
self.last_attack_time = current_time
# Calculate direction to player
direction = vec(self.pos.x, self.pos.y)
projectile = Projectile(
pos=vec(self.pos.x, self.pos.y),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
if (
pressed_keys[K_q]
and pressed_keys[K_v]
and current_time - self.last_attack_time >= self.attack_cooldown
and self.projectiles > 0
):
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
# Calculate direction to player
direction = vec(-self.pos.x, 0)
projectile = Projectile(
pos=vec(self.pos.x - 50, self.pos.y - 50),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
texturePath="assets/player/Boule de feu.png",
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
self.projectiles -= 1
if (
pressed_keys[K_d]
and pressed_keys[K_v]
and current_time - self.last_attack_time >= self.attack_cooldown
and self.projectiles > 0
):
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
# Calculate direction to player
direction = vec(self.pos.x, 0)
projectile = Projectile(
pos=vec(self.pos.x + 50, self.pos.y - 50),
direction=direction,
speed=2,
damage=1,
color=(165, 42, 42),
enemy_proj=False,
texturePath="assets/player/Boule de feu.png",
)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
self.projectiles -= 1
def add_projectiles(self):
"""Set player projectiles to 3"""
"""Set player projectiles to 3 and show floating text"""
self.projectiles = 3
self.floating_texts.append(
FloatingText("+3 fireball", self, self.game_resources)
)
def draw_projectiles_amount(self, surface):
"""Draws the projectiles counter with icon in the top left corner"""

136
src/Map/cinematic.py Normal file
View File

@@ -0,0 +1,136 @@
import pygame
from PIL import Image, ImageSequence
class Cinematic:
"""Class to handle cinematics in the game"""
# Class variable to track if cinematics have been played (shared across all instances)
played_cinematics = {"Level 1": False, "Level 2": False, "Level 3": False}
def __init__(self):
"""Initialize cinematic resources"""
# Load resources
self.player_image = pygame.image.load(
"assets/player/Sanic Base.png"
).convert_alpha()
self.princess_image = pygame.image.load(
"assets/map/exit/Zeldo.png"
).convert_alpha()
# Prepare the boss GIF
self.boss_gif = Image.open("assets/map/enemy/boss.gif")
self.boss_frames = [
frame.copy() for frame in ImageSequence.Iterator(self.boss_gif)
]
self.boss_frame_index = 0
self.player_image = pygame.transform.scale(self.player_image, (200, 200))
self.princess_image = pygame.transform.scale(self.princess_image, (200, 200))
def _create_gradient_background(
self, screen, start_color=(0, 0, 128), end_color=(0, 0, 0)
):
"""Create a gradient background for the cinematic"""
width, height = screen.get_size()
background = pygame.Surface((width, height))
for y in range(height):
ratio = y / height
r = int(start_color[0] * (1 - ratio) + end_color[0] * ratio)
g = int(start_color[1] * (1 - ratio) + end_color[1] * ratio)
b = int(start_color[2] * (1 - ratio) + end_color[2] * ratio)
pygame.draw.line(background, (r, g, b), (0, y), (width, y))
return background
def _display_cinematic_text(self, screen, lore_text, level_name):
"""Helper function to display cinematic text with animations"""
font = pygame.font.Font(None, 36)
pygame.mixer.init()
cinematic_voice = pygame.mixer.Sound("assets/sound/cinematic_voice.mp3")
gradient_bg = self._create_gradient_background(screen)
screen.blit(gradient_bg, (0, 0))
for i, line in enumerate(lore_text):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
return False # Skip the cinematic if any key is pressed
# Play the voice audio
cinematic_voice.play()
# Display character images based on text content
if "Sanic" in line:
screen.blit(self.player_image, (100, 400))
if "Zeldo" in line:
screen.blit(self.princess_image, (700, 400))
if "Wheatly" in line or "Wheatley" in line:
for _ in range(46):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
return False # Skip the cinematic if any key is pressed
boss_frame = self.boss_frames[self.boss_frame_index]
boss_frame = boss_frame.convert("RGBA")
boss_frame = pygame.image.fromstring(
boss_frame.tobytes(), boss_frame.size, boss_frame.mode
)
boss_frame = pygame.transform.scale(boss_frame, (200, 200))
screen.blit(boss_frame, (400, 400))
pygame.display.flip()
pygame.time.wait(100)
self.boss_frame_index = (self.boss_frame_index + 1) % len(
self.boss_frames
)
text_surface = font.render(line, True, (255, 255, 255))
screen.blit(text_surface, (50, 50 + i * 40))
pygame.display.flip()
pygame.time.wait(2000)
cinematic_voice.stop()
# Mark this cinematic as played
Cinematic.played_cinematics[level_name] = True
return True
def play_cinematic(self, game_resources, level_name):
"""Play the cinematic for levels"""
# Check if this cinematic has already been played
if Cinematic.played_cinematics.get(level_name, False):
return
screen = game_resources.displaysurface
if level_name == "Level 1":
lore_text = [
"Once upon a time in a land far away...",
"A brave hero named Sanic...",
"And a beautiful princess named Zeldo...",
"Has been captured by the evil boss...",
"Wheatly !!!",
"Sanic must rescue Zeldo...",
]
self._display_cinematic_text(screen, lore_text, level_name)
elif level_name == "Level 2":
lore_text = [
"When Sanic arrives at Zeldo's position...",
"He realizes that it's a trap...",
"Zeldo is actually a fake...",
"And the real Zeldo is in another castle...",
]
self._display_cinematic_text(screen, lore_text, level_name)
elif level_name == "Level 3":
lore_text = [
"Sanic must face the evil boss Wheatley...",
"To rescue the real princess Zeldo...",
"Will Sanic succeed?",
]
self._display_cinematic_text(screen, lore_text, level_name)
else:
print(f"No cinematic available for {level_name}.")

View File

@@ -10,6 +10,7 @@ from src.Entity.Exit import Exit
from src.Entity.Coin import Coin
from src.Entity.JumpBoost import JumpBoost
from src.Entity.SpeedBoost import SpeedBoost
from src.Map.cinematic import Cinematic
class MapParser:
@@ -34,8 +35,7 @@ class MapParser:
self.princess_image = pygame.image.load(
"assets/map/exit/Zeldo.png"
).convert_alpha()
self.cinematic_played = False
self.cinematic = Cinematic()
def load_map(self, map_file):
"""Load and parse a map from JSON file"""
@@ -44,9 +44,8 @@ class MapParser:
map_data = json.load(file)
# If it's level 1, play the cinematic
if map_data.get("name") == "Level 1" and not self.cinematic_played:
self.play_cinematic(self.game_resources)
self.cinematic_played = True
if map_data.get("name"):
self.cinematic.play_cinematic(self.game_resources, map_data.get("name"))
# Create all game objects from map data
self.create_map_objects(map_data, map_file)
@@ -198,61 +197,3 @@ class MapParser:
self.player.pos.x = spawn["x"]
self.player.pos.y = spawn["y"]
self.all_sprites.add(self.player)
def play_cinematic(self, game_resources):
"""Play the cinematic for level 1"""
screen = game_resources.displaysurface
font = pygame.font.Font(None, 36)
lore_text = [
"Once upon a time in a land far away...",
"A brave hero named Sanic...",
"And a beautiful princess named Zeldo...",
"Has been captured by the evil boss...",
"Wheatly !!!",
"Sanic must rescue Zeldo...",
]
self.player_image = pygame.transform.scale(self.player_image, (200, 200))
self.princess_image = pygame.transform.scale(self.princess_image, (200, 200))
# Initialize the mixer
pygame.mixer.init()
cinematic_voice = pygame.mixer.Sound("assets/sound/cinematic_voice.mp3")
screen.fill((0, 0, 0))
for i, line in enumerate(lore_text):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
return # Skip the cinematic if any key is pressed
# Play the voice audio
cinematic_voice.play()
if "Sanic" in line:
screen.blit(self.player_image, (100, 400))
if "Zeldo" in line:
screen.blit(self.princess_image, (700, 400))
if "Wheatly" in line:
for _ in range(46):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
return # Skip the cinematic if any key is pressed
boss_frame = self.boss_frames[self.boss_frame_index]
boss_frame = boss_frame.convert("RGBA")
boss_frame = pygame.image.fromstring(
boss_frame.tobytes(), boss_frame.size, boss_frame.mode
)
boss_frame = pygame.transform.scale(boss_frame, (200, 200))
screen.blit(boss_frame, (400, 400))
pygame.display.flip()
pygame.time.wait(100)
self.boss_frame_index = (self.boss_frame_index + 1) % len(
self.boss_frames
)
text_surface = font.render(line, True, (255, 255, 255))
screen.blit(text_surface, (50, 50 + i * 40))
pygame.display.flip()
pygame.time.wait(2000)
cinematic_voice.stop()

View File

@@ -0,0 +1,97 @@
import pygame
import math
from src.Menu.BackgroundManager import BackgroundManager
class InstructionsScreen:
def __init__(self, game_resources):
self.game_resources = game_resources
self.bg_manager = BackgroundManager(game_resources.WIDTH, game_resources.HEIGHT)
self.title_font = pygame.font.SysFont("Arial", 72)
self.text_font = pygame.font.SysFont("Arial", 32)
self.blink_timer = 0
self.blink_speed = 0.5
def draw(self, surface):
self.bg_manager.draw(surface)
def render_text_with_outline(text, font, text_color, outline_color):
text_surface = font.render(text, True, text_color)
outline_surface = font.render(text, True, outline_color)
w, h = text_surface.get_size()
outline_surf = pygame.Surface((w + 2, h + 2), pygame.SRCALPHA)
# Dessiner le contour en décalant le texte
offsets = [
(1, 1),
(1, -1),
(-1, 1),
(-1, -1),
(1, 0),
(-1, 0),
(0, 1),
(0, -1),
]
for dx, dy in offsets:
outline_surf.blit(outline_surface, (dx + 1, dy + 1))
# Dessiner le texte principal au centre
outline_surf.blit(text_surface, (1, 1))
return outline_surf
title_surf = render_text_with_outline(
"Game control", self.title_font, (255, 255, 255), (0, 0, 0)
)
title_rect = title_surf.get_rect(center=(self.game_resources.WIDTH // 2, 100))
surface.blit(title_surf, title_rect)
instructions = [
"Q : Move left",
"D : Move right",
"A : Dash",
"Espace : Jump",
"V: Attack",
"Escape : Pause / Menu",
"Controller : Use the left joystick to move",
"B: Dash",
"A: Jump",
"X: Attack",
"Y: Pause / Menu",
]
y_offset = 180
line_spacing = 40
for line in instructions:
text_surf = render_text_with_outline(
line, self.text_font, (255, 255, 255), (0, 0, 0)
)
text_rect = text_surf.get_rect(
center=(self.game_resources.WIDTH // 2, y_offset)
)
surface.blit(text_surf, text_rect)
y_offset += line_spacing
self.blink_timer += 0.01
alpha = abs(math.sin(self.blink_timer * self.blink_speed)) * 255
skip_text = render_text_with_outline(
"Press any key to continue", self.text_font, (255, 220, 0), (0, 0, 0)
)
skip_text.set_alpha(int(alpha))
skip_rect = skip_text.get_rect(
center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT - 100)
)
surface.blit(skip_text, skip_rect)
def handle_event(self, event):
if (
event.type == pygame.KEYDOWN
or event.type == pygame.MOUSEBUTTONDOWN
or event.type == pygame.JOYBUTTONDOWN
):
return "menu"
return None

View File

@@ -20,7 +20,7 @@ class GameResources:
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}")
print(f"Error loading icons: {e}")
# Ressources
self.platforms = pygame.sprite.Group()
@@ -28,7 +28,7 @@ class GameResources:
self.exits = pygame.sprite.Group()
self.vec = pygame.math.Vector2
self.displaysurface = pygame.display.set_mode(
(self.WIDTH, self.HEIGHT), pygame.RESIZABLE
(self.WIDTH, self.HEIGHT), pygame.RESIZABLE, vsync=1
)
pygame.display.set_caption("Project Sanic")
self.FramePerSec = pygame.time.Clock()

View File

@@ -61,6 +61,14 @@ def initialize_game(game_resources, map_file="map/levels/1.json"):
for exit_obj in exits:
exit_obj.set_player(map_objects["player"])
enemies = map_objects.get("enemies", None)
if exits and enemies:
for enemy in enemies:
if hasattr(enemy, "enemy_type") and enemy.enemy_type == "boss":
for exit_obj in exits:
exit_obj.set_boss(enemy)
break
background = map_objects.get("background", None)
# If no background is found, use a default black background

View File

@@ -23,6 +23,7 @@ from src.Database.CheckpointDB import CheckpointDB
from src.Map.Editor.LevelEditor import LevelEditor
from src.Menu.LevelEditorSelectionMenu import LevelEditorSelectionMenu
from src.Map.Speedrun.SpeedrunTimer import SpeedrunTimer
from src.Menu.InstructionsScreen import InstructionsScreen
def initialize_game_resources():
@@ -63,8 +64,9 @@ def initialize_game_resources():
projectiles = pygame.sprite.Group()
# Game states initialization
current_state = 0 # MENU
current_state = 5 # INSTRUCTIONS
current_menu = "main"
instructions_screen = InstructionsScreen(game_resources)
main_menu = Menu(game_resources)
level_select_menu = None
editor_select_menu = None
@@ -99,6 +101,7 @@ def initialize_game_resources():
joysticks,
editor_select_menu,
leaderboard_db,
instructions_screen,
)
@@ -134,6 +137,16 @@ def handle_system_events(
)
# Update window dimensions
ORIGINAL_WIDTH, ORIGINAL_HEIGHT = event.w, event.h
elif event.type == pygame.JOYBUTTONDOWN:
try:
if event.button == 4: # Triangle sur la plupart des manettes
if current_state in [1, 2]: # PLAYING, INFINITE
current_state = 0 # MENU
else:
pygame.quit()
sys.exit()
except Exception as e:
print(f"Error while handling joystick button: {e}")
return current_state, fullscreen, displaysurface, ORIGINAL_WIDTH, ORIGINAL_HEIGHT
@@ -393,10 +406,16 @@ def draw_background(displaysurface, background, camera, WIDTH, HEIGHT):
if background.get_width() != bg_width or background.get_height() != bg_height:
background = pygame.transform.scale(background, (bg_width, bg_height))
# Parallax effect (smaller factor makes background move slower)
# Parallax effect
parallax_factor = 0.3
bg_x = -(camera.camera.x * parallax_factor) % bg_width
bg_y = -(camera.camera.y * parallax_factor) % bg_height
bg_x = (camera.camera.x * parallax_factor) % bg_width
bg_y = (camera.camera.y * parallax_factor) % bg_height
if bg_x > 0:
bg_x -= bg_width
if bg_y > 0:
bg_y -= bg_height
# Draw background in all directions to create seamless effect
displaysurface.blit(background, (bg_x, bg_y))
@@ -486,6 +505,9 @@ def draw_playing_state(
collectible.on_collision()
P1.collect_coin(displaysurface, speedrun_timer)
for text in P1.floating_texts:
text.draw(displaysurface)
# Draw UI elements
draw_ui_elements(displaysurface, P1, FramePerSec, font, speedrun_timer)
@@ -496,39 +518,43 @@ def handle_exits(P1, exits, game_resources, level_file, speedrun_timer=None):
"""Handle collisions with level exits"""
exits_hit = pygame.sprite.spritecollide(P1, exits, False) if exits else []
for exit in exits_hit:
if speedrun_timer and speedrun_timer.is_running:
collected_coins = speedrun_timer.collected_items
total_coins = speedrun_timer.total_items
if not exit.locked:
if speedrun_timer and speedrun_timer.is_running:
collected_coins = speedrun_timer.collected_items
total_coins = speedrun_timer.total_items
speedrun_timer.stop()
speedrun_timer.save_time(collected_coins, total_coins)
if hasattr(game_resources, "infinite_mode") and game_resources.infinite_mode:
# Infinite mode: load the next level without going back to menu
if hasattr(game_resources, "infinite_mode_db"):
# Zeldo : add 100 points
game_resources.infinite_mode_db.add_score("player", 100)
# Add coins points also
game_resources.infinite_mode_db.add_score("player", P1.coins * 10)
result = handle_exit_collision(exit, game_resources, level_file)
return {"action": "continue_infinite", "result": result}
else:
# Normal mode: unlock the next level and return to menu
current_level_match = re.search(r"(\d+)\.json$", level_file)
if current_level_match:
current_level = int(current_level_match.group(1))
next_level = current_level + 1
speedrun_timer.stop()
speedrun_timer.save_time(collected_coins, total_coins)
if (
hasattr(game_resources, "infinite_mode")
and game_resources.infinite_mode
):
# Infinite mode: load the next level without going back to menu
if hasattr(game_resources, "infinite_mode_db"):
# Zeldo : add 100 points
game_resources.infinite_mode_db.add_score("player", 100)
# Add coins points also
game_resources.infinite_mode_db.add_score("player", P1.coins * 10)
result = handle_exit_collision(exit, game_resources, level_file)
return {"action": "continue_infinite", "result": result}
else:
# Normal mode: unlock the next level and return to menu
current_level_match = re.search(r"(\d+)\.json$", level_file)
if current_level_match:
current_level = int(current_level_match.group(1))
next_level = current_level + 1
# Unlock next level
db = LevelDB()
db.unlock_level(next_level)
db.close()
# Unlock next level
db = LevelDB()
db.unlock_level(next_level)
db.close()
# Return to level select menu
return {
"action": "return_to_level_select",
"current_state": 0, # MENU
"current_menu": "level_select",
}
# Return to level select menu
return {
"action": "return_to_level_select",
"current_state": 0, # MENU
"current_menu": "level_select",
}
return None
@@ -611,7 +637,7 @@ def handle_death_screen(
game_resources.infinite_mode_db.clear_InfiniteModeDB()
game_resources.infinite_mode_db.close()
# Calculate total points, add them to leaderboard table
if(leaderboard_db):
if leaderboard_db:
total = 0
for i in range(len(all_scores)):
total += all_scores[i][1]
@@ -632,7 +658,7 @@ def handle_death_screen(
def handler():
"""Main function that handles the game flow"""
# Game state constants
MENU, PLAYING, INFINITE, LEADERBOARD, DEATH_SCREEN = 0, 1, 2, 3, 4
MENU, PLAYING, INFINITE, LEADERBOARD, DEATH_SCREEN, INSTRUCTIONS = 0, 1, 2, 3, 4, 5
previous_state = None
# Initialize game resources and states
@@ -654,6 +680,7 @@ def handler():
joysticks,
editor_select_menu,
leaderboard_db,
instructions_screen,
) = initialize_game_resources()
# Initialize editor variables
@@ -674,6 +701,7 @@ def handler():
print(f"Error while getting events: {e}")
pygame.joystick.quit()
pygame.joystick.init()
events = []
continue
# Process events
@@ -753,6 +781,13 @@ def handler():
game_resources
)
elif current_state == INSTRUCTIONS:
for event in events:
result = instructions_screen.handle_event(event)
if result == "menu":
current_state = MENU
instructions_screen.draw(displaysurface)
# Process general game events (player death, projectiles, etc.)
if event.type == USEREVENT:
current_state, death_timer, checkpoint_data, projectiles = (
@@ -898,7 +933,7 @@ def handler():
game_resources,
game_resources.WIDTH,
game_resources.HEIGHT,
leaderboard_db
leaderboard_db,
)
death_timer = death_result["death_timer"]
@@ -916,6 +951,13 @@ def handler():
elif death_result["action"] == "return_to_menu":
current_state = death_result["current_state"]
elif current_state == INSTRUCTIONS:
for event in events:
result = instructions_screen.handle_event(event)
if result == "menu":
current_state = MENU
instructions_screen.draw(displaysurface)
# Update display
pygame.display.update()
game_resources.FramePerSec.tick(game_resources.FPS)