mirror of
https://github.com/BreizhHardware/project_sanic.git
synced 2026-01-18 16:47:25 +01:00
BIN
assets/map/enemy/boss.gif
Normal file
BIN
assets/map/enemy/boss.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
assets/map/enemy/turret.gif
Normal file
BIN
assets/map/enemy/turret.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -11,7 +11,7 @@
|
||||
"y": 780,
|
||||
"width": 1800,
|
||||
"height": 200,
|
||||
"texture": "assets/map/platform/grass_texture.jpg"
|
||||
"texture": "assets/map/platform/grass_texture.png"
|
||||
},
|
||||
{
|
||||
"id": "main_ground_2",
|
||||
@@ -19,7 +19,7 @@
|
||||
"y": 900,
|
||||
"width": 1800,
|
||||
"height": 200,
|
||||
"texture": "assets/map/platform/grass_texture.jpg"
|
||||
"texture": "assets/map/platform/stone_texture.png"
|
||||
},
|
||||
{
|
||||
"id": "platform1",
|
||||
@@ -27,7 +27,7 @@
|
||||
"y": 600,
|
||||
"width": 200,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"texture": "assets/map/platform/grass_texture.png",
|
||||
"is_moving": false
|
||||
},
|
||||
{
|
||||
@@ -36,7 +36,7 @@
|
||||
"y": 500,
|
||||
"width": 150,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"texture": "assets/map/platform/wood_texture.png",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "linear",
|
||||
@@ -54,7 +54,7 @@
|
||||
"y": 750,
|
||||
"width": 150,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"texture": "assets/map/platform/grass_texture.png",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "linear",
|
||||
@@ -72,7 +72,7 @@
|
||||
"y": 400,
|
||||
"width": 100,
|
||||
"height": 20,
|
||||
"texture": "assets/map/platform/grass_texture.jpg",
|
||||
"texture": "assets/map/platform/wood_texture.png",
|
||||
"is_moving": true,
|
||||
"movement": {
|
||||
"type": "circular",
|
||||
@@ -124,7 +124,7 @@
|
||||
"behavior": "stationary",
|
||||
"attack_interval": 2.0,
|
||||
"attack_range": 300,
|
||||
"sprite_sheet": "assets/map/enemy/turret_enemy.png",
|
||||
"sprite_sheet": "assets/map/enemy/turret.gif",
|
||||
"size": [50,100]
|
||||
}
|
||||
],
|
||||
@@ -143,14 +143,6 @@
|
||||
"x": 400,
|
||||
"y": 540,
|
||||
"sprite": "assets/map/collectibles/Sanic_Coin.png"
|
||||
},
|
||||
{
|
||||
"id": "power_up1",
|
||||
"type": "speed_boost",
|
||||
"x": 900,
|
||||
"y": 450,
|
||||
"duration": 5.0,
|
||||
"sprite": "assets/map/collectibles/speed_boost.png"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -171,13 +163,13 @@
|
||||
},
|
||||
|
||||
"exits": [
|
||||
{
|
||||
"x": 2300,
|
||||
"y": 700,
|
||||
"width": 50,
|
||||
"height": 80,
|
||||
"next_level": "Level 2",
|
||||
"sprite": "assets/map/exit/Zeldo.png"
|
||||
{
|
||||
"x": 2300,
|
||||
"y": 700,
|
||||
"width": 50,
|
||||
"height": 80,
|
||||
"next_level": "Level 2",
|
||||
"sprite": "assets/map/exit/Zeldo.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
black==25.1.0
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
mypy-extensions==1.0.0
|
||||
numpy==2.2.4
|
||||
packaging==24.2
|
||||
pathspec==0.12.1
|
||||
pillow==11.1.0
|
||||
platformdirs==4.3.7
|
||||
pygame==2.6.1
|
||||
pygame==2.6.1
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import pygame
|
||||
import os
|
||||
from PIL import Image, ImageSequence
|
||||
import random
|
||||
from src.Entity.Entity import Entity
|
||||
from pygame.math import Vector2 as vec
|
||||
@@ -7,7 +9,8 @@ from src.Entity.Projectile import Projectile
|
||||
|
||||
class Enemy(Entity):
|
||||
def __init__(self, enemy_data):
|
||||
super().__init__()
|
||||
self.size = enemy_data.get("size", [50, 50])
|
||||
super().__init__(self.size)
|
||||
|
||||
# Base attributes
|
||||
self.enemy_type = enemy_data.get("type", "turret")
|
||||
@@ -15,19 +18,35 @@ class Enemy(Entity):
|
||||
self.damage = enemy_data.get("damage", 1)
|
||||
self.behavior = enemy_data.get("behavior", "stationary")
|
||||
self.speed = enemy_data.get("speed", 1.5)
|
||||
self.size = enemy_data.get("size", [50, 50])
|
||||
|
||||
# Initial position
|
||||
self.pos = vec(enemy_data.get("x", 0), enemy_data.get("y", 0))
|
||||
|
||||
# Animation attributes
|
||||
self.frames = []
|
||||
self.current_frame = 0
|
||||
self.animation_speed = enemy_data.get("animation_speed", 0.1)
|
||||
self.animation_timer = 0
|
||||
|
||||
sprite_path = enemy_data.get("sprite_sheet", "assets/enemy/default_enemy.png")
|
||||
try:
|
||||
self.surf = pygame.image.load(sprite_path).convert_alpha()
|
||||
self.surf = pygame.transform.scale(self.surf, self.size)
|
||||
except:
|
||||
# Default sprite
|
||||
self.surf = pygame.Surface((40, 40))
|
||||
self.surf.fill((255, 0, 0))
|
||||
|
||||
# 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 self.frames:
|
||||
self.surf = self.frames[0]
|
||||
else:
|
||||
# Default sprite
|
||||
self.surf = pygame.Surface((40, 40))
|
||||
self.surf.fill((255, 0, 0))
|
||||
else:
|
||||
try:
|
||||
self.surf = pygame.image.load(sprite_path).convert_alpha()
|
||||
self.surf = pygame.transform.scale(self.surf, self.size)
|
||||
except:
|
||||
# Default sprite
|
||||
self.surf = pygame.Surface((40, 40))
|
||||
self.surf.fill((255, 0, 0))
|
||||
|
||||
# Initial rectangle
|
||||
self.rect = self.surf.get_rect(center=(self.pos.x, self.pos.y))
|
||||
@@ -45,7 +64,26 @@ class Enemy(Entity):
|
||||
self.is_attacking = False
|
||||
self.detected_player = False
|
||||
|
||||
def update(self, player=None):
|
||||
def load_gif_frames(self, gif_path):
|
||||
"""Load frames from a GIF file"""
|
||||
try:
|
||||
gif = Image.open(gif_path)
|
||||
frame_count = 0
|
||||
|
||||
for frame in ImageSequence.Iterator(gif):
|
||||
frame_surface = pygame.image.fromstring(
|
||||
frame.convert("RGBA").tobytes(), frame.size, "RGBA"
|
||||
)
|
||||
frame_surface = pygame.transform.scale(frame_surface, (80, 80))
|
||||
self.frames.append(frame_surface)
|
||||
frame_count += 1
|
||||
|
||||
print(f"Chargé {frame_count} frames depuis {gif_path}")
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du chargement du GIF: {e}")
|
||||
self.frames = []
|
||||
|
||||
def update(self, player=None, dt=1 / 60):
|
||||
"""Updates enemy's status and position"""
|
||||
if player:
|
||||
self.check_collision_with_player(player)
|
||||
@@ -57,6 +95,14 @@ class Enemy(Entity):
|
||||
elif self.behavior == "stationary" and player:
|
||||
self.stationary_attack(player)
|
||||
|
||||
# Animation management for turret enemies
|
||||
if self.enemy_type == "turret" and self.frames:
|
||||
self.animation_timer += dt
|
||||
if self.animation_timer >= self.animation_speed:
|
||||
self.animation_timer = 0
|
||||
self.current_frame = (self.current_frame + 1) % len(self.frames)
|
||||
self.surf = self.frames[self.current_frame]
|
||||
|
||||
# Update rect position based on entity position
|
||||
self.rect.centerx = self.pos.x
|
||||
self.rect.centery = self.pos.y
|
||||
|
||||
@@ -348,6 +348,7 @@ class LevelEditor:
|
||||
"health": 1,
|
||||
"damage": 1,
|
||||
"sprite_sheet": f"assets/map/enemy/{enemy_type}_enemy.png",
|
||||
"size": [50, 100],
|
||||
}
|
||||
|
||||
if enemy_type == "walker":
|
||||
@@ -365,6 +366,7 @@ class LevelEditor:
|
||||
enemy_data["behavior"] = "stationary"
|
||||
enemy_data["attack_interval"] = 2.0
|
||||
enemy_data["attack_range"] = 300
|
||||
enemy_data["sprite_sheet"] = "assets/map/enemy/turret.gif"
|
||||
|
||||
level_data["enemies"].append(enemy_data)
|
||||
|
||||
@@ -600,7 +602,7 @@ class LevelEditor:
|
||||
self.selected_object, EditorCollectible
|
||||
):
|
||||
if event.key == K_t:
|
||||
types = ["coin", "speed_boost", "health", "shield"]
|
||||
types = ["coin"]
|
||||
current_index = (
|
||||
types.index(self.selected_object.collectible_type)
|
||||
if self.selected_object.collectible_type in types
|
||||
@@ -612,12 +614,6 @@ class LevelEditor:
|
||||
# Update appearance based on type
|
||||
if self.selected_object.collectible_type == "coin":
|
||||
self.selected_object.image.fill((255, 215, 0)) # Gold
|
||||
elif self.selected_object.collectible_type == "speed_boost":
|
||||
self.selected_object.image.fill((0, 0, 255)) # Blue
|
||||
elif self.selected_object.collectible_type == "health":
|
||||
self.selected_object.image.fill((255, 0, 0)) # Red
|
||||
elif self.selected_object.collectible_type == "shield":
|
||||
self.selected_object.image.fill((128, 128, 128)) # Gray
|
||||
|
||||
elif self.selected_object and isinstance(self.selected_object, EditorExit):
|
||||
if event.key == K_n:
|
||||
|
||||
@@ -120,13 +120,41 @@ class InfiniteMapGenerator:
|
||||
enemy_types = ["walker", "flyer", "turret"]
|
||||
|
||||
for i in range(num_enemies):
|
||||
type = random.choice(enemy_types)
|
||||
enemy = {
|
||||
"id": f"enemy{i+1}",
|
||||
"type": random.choice(enemy_types),
|
||||
"type": type,
|
||||
"x": random.randint(600, self.width - 200),
|
||||
"y": random.randint(100, 400),
|
||||
"patrol_distance": random.randint(100, 300),
|
||||
}
|
||||
if type == "flyer":
|
||||
enemy["sprite_sheet"] = "assets/map/enemy/flying_enemy.png"
|
||||
enemy["health"] = 1
|
||||
enemy["damage"] = 1
|
||||
enemy["behavior"] = "chase"
|
||||
enemy["detection_radius"] = random.randint(100, 500)
|
||||
enemy["speed"] = 2.0
|
||||
enemy["size"] = [50, 50]
|
||||
elif type == "walker":
|
||||
enemy["sprite_sheet"] = "assets/map/enemy/walker_enemy.png"
|
||||
enemy["health"] = 1
|
||||
enemy["damage"] = 1
|
||||
enemy["behavior"] = "patrol"
|
||||
enemy["patrol_points"] = [
|
||||
{"x": enemy["x"], "y": enemy["y"]},
|
||||
{"x": enemy["x"] + enemy["patrol_distance"], "y": enemy["y"]},
|
||||
]
|
||||
enemy["speed"] = 1.5
|
||||
enemy["size"] = [50, 50]
|
||||
elif type == "turret":
|
||||
enemy["sprite_sheet"] = "assets/map/enemy/turret.gif"
|
||||
enemy["health"] = 1
|
||||
enemy["damage"] = 1
|
||||
enemy["behavior"] = "stationary"
|
||||
enemy["attack_interval"] = random.uniform(0.5, 3.0)
|
||||
enemy["attack_range"] = random.randint(100, 500)
|
||||
enemy["size"] = [50, 50]
|
||||
enemies.append(enemy)
|
||||
|
||||
return enemies
|
||||
@@ -134,7 +162,7 @@ class InfiniteMapGenerator:
|
||||
def _generate_collectibles(self, difficulty):
|
||||
collectibles = []
|
||||
num_collectibles = 5 + difficulty
|
||||
collectible_types = ["coin", "health", "shield"]
|
||||
collectible_types = ["coin"]
|
||||
|
||||
for i in range(num_collectibles):
|
||||
rand = random.choice(collectible_types)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import pygame
|
||||
import os
|
||||
from PIL import Image, ImageSequence
|
||||
from src.Entity.Platform import Platform
|
||||
from src.Entity.Player import Player
|
||||
from src.Entity.Enemy import Enemy
|
||||
@@ -8,6 +9,7 @@ from src.Entity.Checkpoint import Checkpoint
|
||||
from src.Entity.Exit import Exit
|
||||
from src.Entity.Coin import Coin
|
||||
|
||||
|
||||
class MapParser:
|
||||
def __init__(self, game_resources):
|
||||
self.game_resources = game_resources
|
||||
@@ -19,12 +21,31 @@ class MapParser:
|
||||
self.checkpoints = pygame.sprite.Group()
|
||||
self.player = None
|
||||
|
||||
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.image.load(
|
||||
"assets/player/Sanic Base.png"
|
||||
).convert_alpha()
|
||||
self.princess_image = pygame.image.load(
|
||||
"assets/map/exit/Zeldo.png"
|
||||
).convert_alpha()
|
||||
|
||||
self.cinematic_played = False
|
||||
|
||||
def load_map(self, map_file):
|
||||
"""Load and parse a map from JSON file"""
|
||||
try:
|
||||
with open(map_file, "r") as file:
|
||||
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
|
||||
|
||||
# Create all game objects from map data
|
||||
self.create_map_objects(map_data, map_file)
|
||||
|
||||
@@ -108,14 +129,16 @@ class MapParser:
|
||||
print(f"Found {len(map_data['collectibles'])} collectibles")
|
||||
for collectible_data in map_data["collectibles"]:
|
||||
if collectible_data["type"] == "coin":
|
||||
print(f"Creating coin at ({collectible_data['x']}, {collectible_data['y']})")
|
||||
print(
|
||||
f"Creating coin at ({collectible_data['x']}, {collectible_data['y']})"
|
||||
)
|
||||
sprite_path = collectible_data.get("sprite", "")
|
||||
print(f"Using sprite path: {sprite_path}")
|
||||
|
||||
# Create and add the coin
|
||||
coin = Coin(
|
||||
pos=(collectible_data["x"], collectible_data["y"]),
|
||||
texturePath=sprite_path
|
||||
texturePath=sprite_path,
|
||||
)
|
||||
self.collectibles.add(coin)
|
||||
self.all_sprites.add(coin)
|
||||
@@ -166,3 +189,45 @@ 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))
|
||||
|
||||
screen.fill((0, 0, 0))
|
||||
for i, line in enumerate(lore_text):
|
||||
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):
|
||||
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)
|
||||
|
||||
@@ -85,26 +85,22 @@ class Menu:
|
||||
|
||||
def draw(self, surface):
|
||||
if self.background:
|
||||
# Réduire le facteur de parallaxe
|
||||
parallax_factor = 0.4
|
||||
time_factor = pygame.time.get_ticks() / 1000
|
||||
|
||||
# Calculer le centre du background
|
||||
center_x = (self.background.get_width() - surface.get_width()) / 2
|
||||
center_y = (self.background.get_height() - surface.get_height()) / 2
|
||||
|
||||
# Appliquer un léger mouvement de parallaxe autour du centre
|
||||
bg_x = -center_x + math.sin(time_factor) * 50 * parallax_factor
|
||||
bg_y = -center_y + math.cos(time_factor) * 30 * parallax_factor
|
||||
|
||||
# Afficher le background
|
||||
surface.blit(self.background, (bg_x, bg_y))
|
||||
else:
|
||||
surface.fill((0, 0, 0))
|
||||
|
||||
# Draw title
|
||||
title = pygame.font.SysFont("Arial", 72).render(
|
||||
"Project Sanic", True, (0, 191, 255)
|
||||
"Sanic et la princesse Zeldo", True, (0, 191, 255)
|
||||
)
|
||||
title_rect = title.get_rect(
|
||||
center=(self.game_resources.WIDTH // 2, self.game_resources.HEIGHT // 4)
|
||||
|
||||
@@ -146,8 +146,16 @@ def handler():
|
||||
if death_sound:
|
||||
death_sound.play()
|
||||
|
||||
db = CheckpointDB()
|
||||
checkpoint_data = db.get_checkpoint(level_file)
|
||||
is_infinite_mode = (
|
||||
hasattr(game_resources, "infinite_mode")
|
||||
and game_resources.infinite_mode
|
||||
)
|
||||
|
||||
if not is_infinite_mode:
|
||||
db = CheckpointDB()
|
||||
checkpoint_data = db.get_checkpoint(level_file)
|
||||
else:
|
||||
checkpoint_data = None
|
||||
|
||||
if event.dict.get("action") == "create_projectile":
|
||||
projectile = event.dict.get("projectile")
|
||||
@@ -479,6 +487,8 @@ def handler():
|
||||
projectiles.empty()
|
||||
current_state = PLAYING
|
||||
else:
|
||||
if hasattr(game_resources, "infinite_mode"):
|
||||
game_resources.infinite_mode = False
|
||||
current_state = MENU
|
||||
|
||||
pygame.display.update()
|
||||
|
||||
Reference in New Issue
Block a user