diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e43f6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/src/game/__pycache__ +/src/game/modes/__pycache__ +/src/game/modules/__pycache__ +/src/game/ui/__pycache__ +/src/utils/__pycache__ +/survival-shooter/src/game/__pycache__ +/survival-shooter/src/game/__pycache__ +/src/game/systems/__pycache__ +/src/game/core/__pycache__ diff --git a/plan.txt b/plan.txt index 79aacf2..eb2cf6d 100644 --- a/plan.txt +++ b/plan.txt @@ -60,4 +60,63 @@ - Sprites du joueur avec rotation - Sprites des ennemis - Logo du jeu -- Interface graphique cohérente \ No newline at end of file +- Interface graphique cohérente + +8. Système de Modules et Progression +- Modules permanents (achetables) : + * Interface d'achat avec pièces + * Niveaux d'amélioration (1 à 3) + * Sauvegarde des modules débloqués +- Modules temporaires (coffres) : + * Menu de sélection en jeu + * Effets cumulables avec les modules permanents +- Types d'effets : + * Tir rapide (cooldown réduit) + * Bouclier amélioré (cooldown réduit) + * Boost de dégâts + * Boost de vitesse + * Régénération de santé + * Multi-tir (projectiles multiples) + * Attraction de pièces + * Tirs critiques + * Tirs explosifs + * Bouclier réfléchissant + * Ralentissement d'ennemis + * Bonus de pièces + * Tirs perçants + +9. Économie et Collectibles +- Système de pièces : + * Drop sur les ennemis tués + * Interface d'affichage + * Sauvegarde entre les sessions +- Coffres : + * Spawn aléatoire + * Sélection de modules temporaires +- Système d'attraction magnétique pour les pièces + +10. Personnages et Statistiques +- Système de personnages débloquables : + * Stats différentes (santé, vitesse, dégâts) + * Sprites personnalisés + * Sauvegarde du personnage sélectionné +- Application des statistiques de base aux personnages + +11. Modes de Jeu +- Mode Classique (survie continue) +- Mode Survie (vagues progressives avec records) +- Mode Boss Rush (combats de boss consécutifs) +- Sélection de mode via menu dédié +- Records spécifiques pour chaque mode : + * Temps de survie (Classique) + * Vague maximale (Survie) + * Boss vaincus (Boss Rush) + +12. Système de Sauvegarde +- Sauvegarde des progrès en JSON : + * High scores pour chaque mode + * Modules débloqués et leur niveau + * Personnages débloqués + * Économie (pièces accumulées) +- Chargement automatique au démarrage +- Sauvegarde automatique à la fermeture \ No newline at end of file diff --git a/prompt.txt b/prompt.txt index e00267a..6bb409b 100644 --- a/prompt.txt +++ b/prompt.txt @@ -84,3 +84,50 @@ ses réflexes et sa précision. - Méthodes réutilisables pour les éléments communs - Meilleure organisation du code UI - Séparation des responsabilités menu/jeu + +## Prompt 14 - Système de Modes de Jeu +- Implémentation de trois modes distincts (Classique, Survie, Boss Rush) +- Menu de sélection de mode avec descriptions visuelles +- Interface adaptée à chaque mode de jeu +- Records spécifiques à chaque mode +- HUD personnalisé selon le mode actif + +## Prompt 15 - Boutique de Personnages +- Système de personnages avec statistiques différentes +- Interface de sélection avec aperçu des personnages +- Animation des personnages dans la boutique +- Système de déblocage de personnages avec des pièces +- Affichage des statistiques comparatives + +## Prompt 16 - Système d'Économie +- Implémentation du système de pièces +- Drop de pièces à la mort des ennemis +- Animation de collecte des pièces +- Interface d'affichage du solde +- Sauvegarde de l'économie entre les sessions + +## Prompt 17 - Modules Permanents +- Boutique de modules améliorables +- Interface d'achat avec description des effets +- Système de niveaux d'amélioration +- Application des effets au joueur + +## Prompt 18 - Modules Temporaires et Coffres +- Système de coffres à la mort des ennemis +- Menu de sélection de modules temporaires en jeu +- Cumul des effets temporaires et permanents +- Diversité des bonus temporaires + +## Prompt 19 - Boss +- Barre de vie dédiée pour les boss + +## Prompt 20 - Interaction avec l'Environnement +- Système de magnétisme pour les pièces + +## Prompt 21 - Système de Notifications +- Interface de notification dans le coin de l'écran +- Animation de fade-in et fade-out pour les notifications +- File d'attente pour gérer plusieurs notifications +- Support des icônes personnalisées +- Adaptable à plusieurs contextes (boutique, combats, etc.) +- Utile pour la gestion des hauts faits (achievements) diff --git a/pyproject.toml b/pyproject.toml index b2c530d..eadb338 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,4 +32,13 @@ survival-shooter ├── README.md ├── plan.txt ├── prompt.txt -└── requirements.txt \ No newline at end of file +└── requirements.txt + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index 03ddd5d..6d7bec6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -numpy==2.2.3 -pygame==2.5.2 +numpy==2.2.4 +pygame==2.6.1 diff --git a/save_data.json b/save_data.json new file mode 100644 index 0000000..5365bb6 --- /dev/null +++ b/save_data.json @@ -0,0 +1 @@ +{"coins": 68079, "achievements": {"first_kill": {"unlocked": true, "progress": 0}, "survivor": {"unlocked": false, "progress": 0}, "rich": {"unlocked": true, "progress": 100}, "collector": {"unlocked": false, "progress": 0}, "master": {"unlocked": false, "progress": 0}}} \ No newline at end of file diff --git a/survival-shooter/src/__init__.py b/src/__init__.py similarity index 100% rename from survival-shooter/src/__init__.py rename to src/__init__.py diff --git a/survival-shooter/src/assets/02459f64-4057-459c-b1a3-3f601195d141.ico b/src/assets/02459f64-4057-459c-b1a3-3f601195d141.ico similarity index 100% rename from survival-shooter/src/assets/02459f64-4057-459c-b1a3-3f601195d141.ico rename to src/assets/02459f64-4057-459c-b1a3-3f601195d141.ico diff --git a/survival-shooter/src/assets/__init__.py b/src/assets/__init__.py similarity index 100% rename from survival-shooter/src/assets/__init__.py rename to src/assets/__init__.py diff --git a/src/assets/achievements/first_kill.png b/src/assets/achievements/first_kill.png new file mode 100644 index 0000000..9ac0d11 Binary files /dev/null and b/src/assets/achievements/first_kill.png differ diff --git a/src/assets/achievements/rich.png b/src/assets/achievements/rich.png new file mode 100644 index 0000000..9ac0d11 Binary files /dev/null and b/src/assets/achievements/rich.png differ diff --git a/src/assets/achievements/survivor.png b/src/assets/achievements/survivor.png new file mode 100644 index 0000000..9ac0d11 Binary files /dev/null and b/src/assets/achievements/survivor.png differ diff --git a/survival-shooter/src/assets/sprites/player.png b/src/assets/characters/default.png similarity index 100% rename from survival-shooter/src/assets/sprites/player.png rename to src/assets/characters/default.png diff --git a/src/assets/characters/speeder.png b/src/assets/characters/speeder.png new file mode 100644 index 0000000..18b40a8 Binary files /dev/null and b/src/assets/characters/speeder.png differ diff --git a/src/assets/characters/tank.png b/src/assets/characters/tank.png new file mode 100644 index 0000000..18b40a8 Binary files /dev/null and b/src/assets/characters/tank.png differ diff --git a/src/assets/icons/chest.png b/src/assets/icons/chest.png new file mode 100644 index 0000000..93f06f6 Binary files /dev/null and b/src/assets/icons/chest.png differ diff --git a/src/assets/icons/coin.png b/src/assets/icons/coin.png new file mode 100644 index 0000000..db76b41 Binary files /dev/null and b/src/assets/icons/coin.png differ diff --git a/src/assets/icons/coin_1.png b/src/assets/icons/coin_1.png new file mode 100644 index 0000000..9ac0d11 Binary files /dev/null and b/src/assets/icons/coin_1.png differ diff --git a/survival-shooter/src/assets/logo.ico b/src/assets/logo.ico similarity index 100% rename from survival-shooter/src/assets/logo.ico rename to src/assets/logo.ico diff --git a/src/assets/sounds/achievement.wav b/src/assets/sounds/achievement.wav new file mode 100644 index 0000000..dc4b3f1 Binary files /dev/null and b/src/assets/sounds/achievement.wav differ diff --git a/src/assets/sounds/coin.wav b/src/assets/sounds/coin.wav new file mode 100644 index 0000000..dc4b3f1 Binary files /dev/null and b/src/assets/sounds/coin.wav differ diff --git a/src/assets/sounds/enemy_death.wav b/src/assets/sounds/enemy_death.wav new file mode 100644 index 0000000..cbcab4e Binary files /dev/null and b/src/assets/sounds/enemy_death.wav differ diff --git a/survival-shooter/src/assets/sounds/enemy_shoot.wav b/src/assets/sounds/enemy_shoot.wav similarity index 100% rename from survival-shooter/src/assets/sounds/enemy_shoot.wav rename to src/assets/sounds/enemy_shoot.wav diff --git a/survival-shooter/src/assets/sounds/game_music.mp3 b/src/assets/sounds/game_music.mp3 similarity index 100% rename from survival-shooter/src/assets/sounds/game_music.mp3 rename to src/assets/sounds/game_music.mp3 diff --git a/src/assets/sounds/hit.wav b/src/assets/sounds/hit.wav new file mode 100644 index 0000000..5e12382 Binary files /dev/null and b/src/assets/sounds/hit.wav differ diff --git a/survival-shooter/src/assets/sounds/player_hurt.wav b/src/assets/sounds/player_hurt.wav similarity index 100% rename from survival-shooter/src/assets/sounds/player_hurt.wav rename to src/assets/sounds/player_hurt.wav diff --git a/src/assets/sounds/purchase.wav b/src/assets/sounds/purchase.wav new file mode 100644 index 0000000..dc4b3f1 Binary files /dev/null and b/src/assets/sounds/purchase.wav differ diff --git a/survival-shooter/src/assets/sounds/shoot.wav b/src/assets/sounds/shoot.wav similarity index 100% rename from survival-shooter/src/assets/sounds/shoot.wav rename to src/assets/sounds/shoot.wav diff --git a/src/assets/sprites/background.png b/src/assets/sprites/background.png new file mode 100644 index 0000000..46b134b --- /dev/null +++ b/src/assets/sprites/background.png @@ -0,0 +1 @@ +ÿþ \ No newline at end of file diff --git a/src/assets/sprites/bullet.png b/src/assets/sprites/bullet.png new file mode 100644 index 0000000..46b134b --- /dev/null +++ b/src/assets/sprites/bullet.png @@ -0,0 +1 @@ +ÿþ \ No newline at end of file diff --git a/src/assets/sprites/enemy.png b/src/assets/sprites/enemy.png new file mode 100644 index 0000000..46b134b --- /dev/null +++ b/src/assets/sprites/enemy.png @@ -0,0 +1 @@ +ÿþ \ No newline at end of file diff --git a/src/assets/sprites/enemy_bullet.png b/src/assets/sprites/enemy_bullet.png new file mode 100644 index 0000000..46b134b --- /dev/null +++ b/src/assets/sprites/enemy_bullet.png @@ -0,0 +1 @@ +ÿþ \ No newline at end of file diff --git a/src/assets/sprites/player.png b/src/assets/sprites/player.png new file mode 100644 index 0000000..18b40a8 Binary files /dev/null and b/src/assets/sprites/player.png differ diff --git a/src/data/game_save.json b/src/data/game_save.json new file mode 100644 index 0000000..efdc858 --- /dev/null +++ b/src/data/game_save.json @@ -0,0 +1 @@ +{"highscore": 10200, "wave": 1, "score": 0, "records": {"classic": {"best_survival_time": 112}, "survival": {"best_wave": 3}, "boss_rush": {"best_bosses_killed": 0}}, "characters": {"characters": {"default": {"unlocked": true, "selected": false}, "speed": {"unlocked": true, "selected": false}, "tank": {"unlocked": true, "selected": false}, "glass": {"unlocked": true, "selected": true}}}} \ No newline at end of file diff --git a/survival-shooter/src/data/highscore.json b/src/data/highscore.json similarity index 100% rename from survival-shooter/src/data/highscore.json rename to src/data/highscore.json diff --git a/survival-shooter/src/game/__init__.py b/src/game/__init__.py similarity index 100% rename from survival-shooter/src/game/__init__.py rename to src/game/__init__.py diff --git a/src/game/core/__init__.py b/src/game/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/survival-shooter/src/game/enemy.py b/src/game/core/enemy.py similarity index 58% rename from survival-shooter/src/game/enemy.py rename to src/game/core/enemy.py index 9cd9096..cf75d38 100644 --- a/survival-shooter/src/game/enemy.py +++ b/src/game/core/enemy.py @@ -2,14 +2,20 @@ import math import random from utils.constants import * -from game.weapon import Bullet +from game.core.weapon import Bullet class Enemy(pygame.sprite.Sprite): def __init__(self, health, speed): super().__init__() self.max_health = health # Ajout de la santé maximale self.health = health - self.speed = speed + self.base_speed = speed # Vitesse de base + self.speed = speed # Vitesse actuelle (peut être modifiée par des effets) + + # Attributs pour le ralentissement + self.slow_factor = 0 # Facteur de ralentissement (0 = pas de ralentissement, 0.5 = 50% de ralentissement) + self.slow_duration = 0 # Durée du ralentissement en ms + self.is_slowed = False # État de ralentissement # Configuration de la barre de vie self.health_bar_height = 5 @@ -36,6 +42,7 @@ def __init__(self, health, speed): scaled_points = [(center + (x-center)*0.7, center + (y-center)*0.7) for x, y in points] pygame.draw.polygon(self.image, ENEMY_COLOR, scaled_points) + self.original_image = self.image.copy() # Garder une copie de l'image originale self.rect = self.image.get_rect() self.spawn() @@ -56,6 +63,9 @@ def spawn(self): self.rect.y = random.randint(0, SCREEN_HEIGHT) def move(self, target_position): + # Mise à jour des effets de ralentissement + self.update_slow_effect() + # Calcul du vecteur de direction vers le joueur dx = target_position[0] - self.rect.centerx dy = target_position[1] - self.rect.centery @@ -120,6 +130,31 @@ def draw(self, screen): pygame.draw.rect(screen, (0, 255, 0), (bar_x, bar_y, bar_width, self.health_bar_height)) + def update_slow_effect(self): + """Met à jour l'effet de ralentissement""" + current_time = pygame.time.get_ticks() + + if self.slow_duration > 0 and current_time >= self.slow_duration: + # L'effet de ralentissement est terminé + self.is_slowed = False + self.slow_factor = 0 + self.slow_duration = 0 + self.speed = self.base_speed + # Restaurer l'apparence normale + self.image = self.original_image.copy() + elif self.slow_factor > 0: + # L'effet de ralentissement est actif + if not self.is_slowed: + self.is_slowed = True + # Appliquer le ralentissement + self.speed = self.base_speed * (1 - self.slow_factor) + # Modifier l'apparence pour indiquer le ralentissement + slowed_image = self.original_image.copy() + blue_overlay = pygame.Surface(slowed_image.get_size(), pygame.SRCALPHA) + blue_overlay.fill((0, 0, 255, 100)) # Teinte bleue pour indiquer le ralentissement + slowed_image.blit(blue_overlay, (0, 0), special_flags=pygame.BLEND_RGBA_ADD) + self.image = slowed_image + class ShootingEnemy(Enemy): def __init__(self, health, speed): super().__init__(health, speed) @@ -146,20 +181,62 @@ def __init__(self, health, speed): scaled_points = [(center + (x-center)*0.7, center + (y-center)*0.7) for x, y in points] pygame.draw.polygon(self.image, SHOOTER_ENEMY_COLOR, scaled_points) + self.original_image = self.image.copy() # Garder une copie de l'image originale + def update(self, player_pos): super().move(player_pos) current_time = pygame.time.get_ticks() - if current_time - self.last_shot >= self.shoot_cooldown: + + # Ajuster le cooldown de tir en fonction du ralentissement + effective_cooldown = self.shoot_cooldown + if self.is_slowed: + # Un ennemi ralenti tire moins souvent + effective_cooldown = self.shoot_cooldown * (1 + self.slow_factor) + + if current_time - self.last_shot >= effective_cooldown: self.shoot(player_pos) def shoot(self, player_pos): dx = player_pos[0] - self.rect.centerx dy = player_pos[1] - self.rect.centery - distance = math.sqrt(dx**2 + dy**2) - if distance != 0: - direction = (dx/distance, dy/distance) - bullet = Bullet(self.rect.centerx, self.rect.centery, direction, is_enemy=True) - self.bullets.add(bullet) - if hasattr(self, 'game') and self.game.enemy_shoot_sound and not self.game.sound_muted: - self.game.enemy_shoot_sound.play() - self.last_shot = pygame.time.get_ticks() \ No newline at end of file + # Calculer l'angle au lieu du vecteur de direction + angle = math.atan2(dy, dx) + bullet = Bullet(self.rect.centerx, self.rect.centery, angle, ENEMY_BULLET_DAMAGE, is_enemy=True) + self.bullets.add(bullet) + if hasattr(self, 'game') and self.game.enemy_shoot_sound and not self.game.sound_muted: + self.game.enemy_shoot_sound.play() + self.last_shot = pygame.time.get_ticks() + + def check_bullet_shield_collision(self, player): + """Vérifie les collisions entre les balles ennemies et le bouclier du joueur""" + # Si le joueur n'a pas de bouclier actif, pas besoin de vérifier + if not player.shield_active: + return False + + # Vérifier chaque balle + for bullet in self.bullets: + # Calculer la distance entre la balle et le centre du joueur + dx = bullet.rect.centerx - player.rect.centerx + dy = bullet.rect.centery - player.rect.centery + distance = math.sqrt(dx * dx + dy * dy) + + # Si la balle est à l'intérieur du rayon du bouclier + shield_radius = PLAYER_SIZE + 10 # Même valeur que dans player.draw + if distance < shield_radius: + # Déterminer si la balle rebondit ou est détruite + if hasattr(player, 'reflect_chance') and random.random() < player.reflect_chance: + # La balle rebondit vers l'ennemi + bullet.direction_x *= -1 + bullet.direction_y *= -1 + + # Jouer un son si disponible + if hasattr(self, 'game') and hasattr(self.game, 'hit_sound') and self.game.hit_sound and not self.game.sound_muted: + self.game.hit_sound.play() + + return True + else: + # La balle est simplement détruite + bullet.kill() + return True + + return False \ No newline at end of file diff --git a/src/game/core/game.py b/src/game/core/game.py new file mode 100644 index 0000000..a04ab0d --- /dev/null +++ b/src/game/core/game.py @@ -0,0 +1,996 @@ +import pygame +import sys +import random +import os +import math +import json +from utils.constants import * +from game.core.player import Player +from game.systems.economy import EconomyManager +from game.core.enemy import Enemy, ShootingEnemy +from utils.sound_generator import SoundGenerator +from game.ui.character_shop import CharacterShop +from game.ui.notification_manager import NotificationManager +from game.ui.menu_manager import MenuManager +from game.modules.module_manager import ModuleManager +from game.modules.module_menu import ModuleMenu +from game.modules.module_selection_menu import ModuleSelectionMenu +from game.ui.game_mode_menu import GameModeMenu + +# Import des modes de jeu +from game.modes.classic_mode import ClassicMode +from game.modes.survival_mode import SurvivalMode +from game.modes.boss_rush_mode import BossRushMode + +def check_assets(): + print("Vérification des assets:") + print(f"Dossier de travail actuel: {os.getcwd()}") + print(f"Le fichier existe: {os.path.exists(FIREBALL_SPRITE)}") + print(f"Chemin complet: {os.path.abspath(FIREBALL_SPRITE)}") + +def check_sounds(): + print("Vérification des fichiers sons:") + sound_files = [ + SHOOT_SOUND, + ENEMY_SHOOT_SOUND, + HIT_SOUND, + ENEMY_DEATH_SOUND, + PLAYER_HURT_SOUND, + GAME_MUSIC + ] + for sound_file in sound_files: + print(f"Fichier {sound_file} existe: {os.path.exists(sound_file)}") + +check_assets() +check_sounds() + +class Game: + def __init__(self): + # Initialisation de Pygame et du mixer avec plus de canaux + pygame.init() + pygame.mixer.quit() # Réinitialiser le mixer + pygame.mixer.init(44100, -16, 2, 1024) # Augmentation du buffer + pygame.mixer.set_num_channels(64) # Réduction du nombre de canaux + + # Configuration de l'écran + self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("ALIEN WAVE") + + # Chargement du logo avec gestion d'erreur + try: + self.icon = pygame.image.load(GAME_LOGO) + pygame.display.set_icon(self.icon) + except Exception as e: + print(f"Erreur lors du chargement du logo: {e}") + + self.clock = pygame.time.Clock() + + # Initialisation des polices + self.font = pygame.font.Font(None, 36) + self.big_font = pygame.font.Font(None, 72) + + # Génération des sons s'ils n'existent pas + sound_gen = SoundGenerator() + sound_gen.generate_all_sounds(SOUND_DIR) + + # Chargement des sons + try: + self.shoot_sound = pygame.mixer.Sound(SHOOT_SOUND) + self.enemy_shoot_sound = pygame.mixer.Sound(ENEMY_SHOOT_SOUND) + self.hit_sound = pygame.mixer.Sound(HIT_SOUND) + self.enemy_death_sound = pygame.mixer.Sound(ENEMY_DEATH_SOUND) + self.player_hurt_sound = pygame.mixer.Sound(PLAYER_HURT_SOUND) + self.coin_sound = pygame.mixer.Sound(COIN_SOUND) + self.achievement_sound = pygame.mixer.Sound(ACHIEVEMENT_SOUND) + self.purchase_sound = pygame.mixer.Sound(PURCHASE_SOUND) + + # Configuration du volume et durée des sons + for sound in [self.shoot_sound, self.enemy_shoot_sound, + self.hit_sound, self.enemy_death_sound, + self.player_hurt_sound, self.coin_sound, + self.achievement_sound, self.purchase_sound]: + sound.set_volume(0.1) + sound.fadeout(100) # Fade out après 100ms + + # Musique de fond + self.game_music = GAME_MUSIC + self.load_background_music() + self.play_background_music() + except Exception as e: + print(f"Erreur lors du chargement des sons: {e}") + self.shoot_sound = None + self.enemy_shoot_sound = None + self.hit_sound = None + self.enemy_death_sound = None + self.player_hurt_sound = None + self.coin_sound = None + self.achievement_sound = None + self.purchase_sound = None + self.game_music = None + + # État du jeu + self.game_state = MENU + self.sound_muted = False + self.last_spawn = 0 + self.spawn_cooldown = 1000 + self.enemies_killed = 0 + self.highscore = self.load_highscore() + self.show_module_menu = False # Nouvel état pour le menu des modules + + # Boutons pour l'écran Game Over + from game.ui.button import Button + self.game_over_buttons = { + 'restart': Button("Recommencer", SCREEN_WIDTH // 2, SCREEN_HEIGHT * 2 // 3, 200, 50), + 'menu': Button("Menu Principal", SCREEN_WIDTH // 2, SCREEN_HEIGHT * 2 // 3 + 70, 200, 50), + 'quit': Button("Quitter le jeu", SCREEN_WIDTH // 2, SCREEN_HEIGHT * 2 // 3 + 140, 200, 50) + } + + # Variables de vague + self.wave = 1 + self.wave_enemies_left = ENEMIES_PER_WAVE + self.wave_transition = False + self.wave_start_time = 0 + self.wave_transition_start = 0 + + # Initialisation des managers dans le bon ordre + self.economy_manager = EconomyManager(self) + self.notification_manager = NotificationManager(self) + self.module_manager = ModuleManager(self) + self.character_shop = CharacterShop(self) + + # Initialisation des menus + self.module_menu = ModuleMenu(self) + self.module_selection = ModuleSelectionMenu(self) + self.menu_manager = MenuManager(self) + + # Initialisation des sprites + self.player = Player(self) + self.enemies = pygame.sprite.Group() + self.pickups = pygame.sprite.Group() # Nouveau groupe pour les objets à ramasser + + # Initialisation des étoiles + self.num_stars = 150 + self.star_speeds = [0.3, 0.5, 0.8] + self.stars = [] + self.generate_stars() + + # Initialisation des modes de jeu + self.game_modes = { + 'classic': ClassicMode(self), + 'survival': SurvivalMode(self), + 'boss_rush': BossRushMode(self) + } + self.current_game_mode = self.game_modes['classic'] # Mode par défaut + + # Initialisation du menu des modes de jeu + self.game_mode_menu = GameModeMenu(self.screen, self) + + # Chargement de l'état du jeu + self.load_game_state() + + # Afficher une notification de bienvenue + self.notification_manager.add_notification( + "Bienvenue dans ALIEN WAVE", + "Survivez aussi longtemps que possible!", + None + ) + + def generate_stars(self): + """Génère les étoiles initiales""" + for _ in range(self.num_stars): + x = random.randint(0, SCREEN_WIDTH) + y = random.randint(0, SCREEN_HEIGHT) + speed = random.choice(self.star_speeds) + brightness = random.randint(100, 255) + size = random.randint(1, 3) + angle = random.uniform(0, 2 * math.pi) # Angle aléatoire pour chaque étoile + self.stars.append({ + 'pos': [x, y], + 'speed': speed, + 'brightness': brightness, + 'size': size, + 'angle': angle # Nouvelle propriété pour la direction + }) + + def update_stars(self): + """Met à jour la position des étoiles avec un mouvement constant""" + for star in self.stars: + # Mouvement constant vers le bas avec une légère dérive + star['pos'][1] += star['speed'] + star['pos'][0] += math.sin(star['angle']) * 0.3 + + # Fait réapparaître les étoiles en haut quand elles sortent de l'écran + if star['pos'][1] > SCREEN_HEIGHT: + star['pos'][1] = 0 + star['pos'][0] = random.randint(0, SCREEN_WIDTH) + star['brightness'] = random.randint(100, 255) # Nouvelle luminosité + + # Garde les étoiles dans les limites horizontales + if star['pos'][0] < 0: + star['pos'][0] = SCREEN_WIDTH + elif star['pos'][0] > SCREEN_WIDTH: + star['pos'][0] = 0 + + def draw_stars(self): + """Dessine les étoiles""" + for star in self.stars: + color = (star['brightness'], star['brightness'], star['brightness']) + pygame.draw.circle(self.screen, color, + (int(star['pos'][0]), int(star['pos'][1])), + star['size']) + + def run(self): + running = True + while running: + # Récupérer les événements une seule fois par frame + events = pygame.event.get() + + # Vérifier si on doit quitter le jeu + for event in events: + if event.type == pygame.QUIT: + running = False + + # Gestion spécifique à l'état de Game Over + if self.game_state == GAME_OVER: + self.handle_game_over_events(events) + + # Gestion standard des événements pour les autres états + else: + for event in events: + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_m: + self.toggle_sound() + elif event.key == pygame.K_ESCAPE: + if self.game_state == PLAYING: + self.game_state = PAUSED + elif self.game_state == PAUSED: + self.game_state = PLAYING + self.show_module_menu = False # Fermer le menu des modules + elif self.game_state == 'mode_selection': + self.game_state = MENU + elif event.key == pygame.K_SPACE: + if self.game_state == MENU: + self.game_mode_menu.show() + self.game_state = 'mode_selection' + elif event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # Clic gauche + mouse_x, mouse_y = event.pos + button_x = SCREEN_WIDTH - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING + button_y = SCREEN_HEIGHT - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING + if (button_x <= mouse_x <= button_x + SOUND_BUTTON_SIZE and + button_y <= mouse_y <= button_y + SOUND_BUTTON_SIZE): + self.toggle_sound() + elif event.button == 3: # Clic droit + if self.game_state == PLAYING: + self.player.activate_shield() + + # Gestion des événements du menu + if self.game_state == MENU: + action = self.menu_manager.handle_event(event) + if action == 'start_game': + self.game_mode_menu.show() + self.game_state = 'mode_selection' + elif action == 'quit': + running = False + elif action == 'restart_game': + self.current_game_mode.initialize() + self.game_state = PLAYING + elif action == 'resume_game': + self.game_state = PLAYING + elif action == 'go_to_main_menu': + self.game_state = MENU + elif self.game_state == 'mode_selection': + action = self.game_mode_menu.handle_event(event) + if action: + if action == 'back': + self.game_state = MENU + elif action.startswith('mode_'): + mode_id = action[5:] # Enlève le préfixe 'mode_' + if self.select_game_mode(mode_id): + self.game_state = PLAYING + self.current_game_mode.initialize() + elif self.game_state == PLAYING or self.game_state == PAUSED: + self.module_selection.handle_event(event) + + # Gestion des entrées continues (mouvement et tir) + if self.game_state == PLAYING: + # Mise à jour du mode de jeu actuel + self.current_game_mode.update() + + # Gestion du mouvement + keys = pygame.key.get_pressed() + dx = dy = 0 + if keys[pygame.K_z] or keys[pygame.K_UP]: + dy -= 1 + if keys[pygame.K_s] or keys[pygame.K_DOWN]: + dy += 1 + if keys[pygame.K_q] or keys[pygame.K_LEFT]: + dx -= 1 + if keys[pygame.K_d] or keys[pygame.K_RIGHT]: + dx += 1 + if dx != 0 or dy != 0: + self.player.move(dx, dy) + + # Gestion du tir continu + mouse_buttons = pygame.mouse.get_pressed() + if mouse_buttons[0]: # Clic gauche maintenu + self.player.shoot() + if keys[pygame.K_SPACE]: # Barre d'espace maintenue + self.player.shoot() + + # Effacement de l'écran + self.screen.fill(BG_COLOR) + + # Mise à jour et dessin des étoiles + self.update_stars() + self.draw_stars() + + # Mise à jour des notifications + self.notification_manager.update() + + # Gestion des états du jeu + if self.game_state == MENU: + self.menu_manager.draw() + elif self.game_state == 'mode_selection': + self.game_mode_menu.draw() + elif self.game_state == PLAYING: + # Mise à jour du jeu + self.player.update() + if not self.current_game_mode.wave_transition if hasattr(self.current_game_mode, 'wave_transition') else True: + self.spawn_enemy() + self.update_enemies() + self.update_effects() + + # Attirer les pièces si l'effet magnet est actif + self.player.attract_coins(self.pickups) + + # Mise à jour des pickups + self.pickups.update(self.player) + + # Rendu du jeu + self.current_game_mode.draw(self.screen) + self.pickups.draw(self.screen) # Affichage des objets à ramasser + self.draw_hud() + + # Dessiner le menu de sélection des modules s'il est visible + self.module_selection.draw(self.screen) + elif self.game_state == PAUSED: + # Afficher d'abord le jeu en arrière-plan + self.current_game_mode.draw(self.screen) + self.draw_hud() + + # Afficher un fond semi-transparent + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + overlay.fill((0, 0, 0)) + overlay.set_alpha(128) + self.screen.blit(overlay, (0, 0)) + + # Si le menu des modules est visible, on l'affiche + if self.show_module_menu: + self.module_selection.draw(self.screen) + # Sinon on affiche le menu pause normal + else: + # Afficher le texte "PAUSE" + pause_text = self.big_font.render("PAUSE", True, WHITE) + pause_rect = pause_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 3)) + self.screen.blit(pause_text, pause_rect) + + # Instructions + resume_text = self.font.render("Appuyez sur ÉCHAP pour reprendre", True, WHITE) + resume_rect = resume_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT * 2 // 3)) + self.screen.blit(resume_text, resume_rect) + elif self.game_state == GAME_OVER: + self.draw_game_over() + + # Mise à jour des boutons de Game Over (état de survol) + mouse_pos = pygame.mouse.get_pos() + for button in self.game_over_buttons.values(): + button.update_hover_state() + + # Afficher les notifications (indépendamment de l'état du jeu) + self.notification_manager.draw(self.screen) + + # Mise à jour de l'écran + pygame.display.flip() + self.clock.tick(FPS) + + # Sauvegarder l'état du jeu avant de quitter + self.save_game_state() + + # Nettoyage et fermeture + pygame.quit() + sys.exit() + + def toggle_sound(self): + """Active/désactive le son""" + self.sound_muted = not self.sound_muted + if self.sound_muted: + pygame.mixer.music.pause() + else: + pygame.mixer.music.unpause() + + def play_background_music(self): + """Joue la musique de fond en boucle""" + try: + pygame.mixer.music.play(-1) # -1 pour jouer en boucle + except Exception as e: + print(f"Erreur lors de la lecture de la musique de fond: {e}") + + def load_highscore(self): + """Charge le meilleur score""" + try: + if os.path.exists(SAVE_FILE): + with open(SAVE_FILE, 'r') as f: + data = json.load(f) + return data.get('highscore', 0) + except Exception as e: + print(f'Erreur lors du chargement du meilleur score: {e}') + return 0 + + def load_game_state(self): + """Charge l'état du jeu depuis un fichier""" + try: + if os.path.exists(SAVE_FILE): + with open(SAVE_FILE, 'r') as f: + data = json.load(f) + self.highscore = data.get('highscore', 0) + + # Chargement des records pour chaque mode + records = data.get('records', {}) + if 'classic' in records: + self.game_modes['classic'].best_survival_time = records['classic'].get('best_survival_time', 0) + if 'survival' in records: + self.game_modes['survival'].best_wave = records['survival'].get('best_wave', 0) + if 'boss_rush' in records: + self.game_modes['boss_rush'].best_bosses_killed = records['boss_rush'].get('best_bosses_killed', 0) + + # Chargement de l'état des personnages + character_state = data.get('characters', {}) + if hasattr(self, 'character_shop') and character_state: + self.character_shop.load_state(character_state) + + # S'assurer que le joueur utilise le bon personnage après le chargement + if hasattr(self, 'player'): + # Réinitialiser le joueur pour prendre en compte le personnage chargé + self.player.apply_character_stats() + # Mettre à jour l'image du joueur si nécessaire + if self.character_shop.selected_character: + try: + self.player.original_image = self.character_shop.selected_character.sprite.copy() + self.player.image = self.player.original_image.copy() + print(f"Image du joueur mise à jour pour le personnage {self.character_shop.selected_character.id}") + except Exception as e: + print(f"Erreur lors de la mise à jour de l'image du joueur: {e}") + + print('État du jeu chargé avec succès') + except Exception as e: + print(f'Erreur lors du chargement de l\'état du jeu: {e}') + + def game_over(self): + """Gère la fin de partie""" + self.game_state = GAME_OVER + self.update_highscore() + + def update_highscore(self): + """Met à jour le meilleur score""" + if self.player.score > self.highscore: + self.highscore = self.player.score + self.save_highscore() + + def save_highscore(self): + """Sauvegarde le meilleur score""" + try: + with open(SAVE_FILE, 'w') as f: + json.dump({'highscore': self.highscore}, f) + except Exception as e: + print(f"Erreur lors de la sauvegarde du meilleur score: {e}") + + def get_game_modes(self): + """Retourne les modes de jeu disponibles""" + return self.game_modes + + def select_game_mode(self, mode_id): + """Sélectionne un mode de jeu""" + if mode_id in self.game_modes: + self.current_game_mode = self.game_modes[mode_id] + return True + return False + + def reset_game(self): + """Réinitialise le jeu""" + self.current_game_mode.initialize() + self.game_state = MENU + self.player = Player(self) # Créer un nouveau joueur (qui utilisera le personnage sélectionné) + + # Appliquer les effets des modules actifs + if hasattr(self, 'module_manager'): + self.module_manager.apply_module_effects(self.player) + + self.enemies = pygame.sprite.Group() + self.pickups = pygame.sprite.Group() + self.wave = 1 + self.enemies_killed = 0 + self.last_spawn = 0 + self.spawn_cooldown = 2000 + self.highscore = self.load_highscore() + self.module_manager.reset_temp_modules() # Réinitialisation des modules temporaires + self.save_game_state() + + def set_volume(self, volume): + """Configure le volume pour tous les sons""" + try: + if hasattr(self, 'shoot_sound') and self.shoot_sound: + self.shoot_sound.set_volume(volume) + if hasattr(self, 'enemy_shoot_sound') and self.enemy_shoot_sound: + self.enemy_shoot_sound.set_volume(volume) + if hasattr(self, 'hit_sound') and self.hit_sound: + self.hit_sound.set_volume(volume) + if hasattr(self, 'enemy_death_sound') and self.enemy_death_sound: + self.enemy_death_sound.set_volume(volume) + if hasattr(self, 'player_hurt_sound') and self.player_hurt_sound: + self.player_hurt_sound.set_volume(volume) + if hasattr(self, 'coin_sound') and self.coin_sound: + self.coin_sound.set_volume(volume) + if hasattr(self, 'achievement_sound') and self.achievement_sound: + self.achievement_sound.set_volume(volume) + if hasattr(self, 'purchase_sound') and self.purchase_sound: + self.purchase_sound.set_volume(volume) + except Exception as e: + print(f"Erreur lors du réglage du volume: {e}") + + def play_sound(self, sound_type): + """Joue un son spécifique""" + if not self.sound_muted: + try: + sound = None + if sound_type == 'shoot': + sound = self.shoot_sound + elif sound_type == 'enemy_shoot': + sound = self.enemy_shoot_sound + elif sound_type == 'hit': + sound = self.hit_sound + elif sound_type == 'enemy_death': + sound = self.enemy_death_sound + elif sound_type == 'player_hurt': + sound = self.player_hurt_sound + elif sound_type == 'coin': + sound = self.coin_sound + elif sound_type == 'achievement': + sound = self.achievement_sound + elif sound_type == 'purchase': + sound = self.purchase_sound + + if sound: + channel = pygame.mixer.find_channel(True) + if channel: + channel.set_volume(0.3) + channel.play(sound, maxtime=1000) # Limite la durée à 1 seconde + except Exception as e: + print(f"Erreur lors de la lecture du son {sound_type}: {e}") + + def load_background_music(self): + """Charge la musique de fond""" + try: + pygame.mixer.music.load(GAME_MUSIC) + pygame.mixer.music.set_volume(0.1) + except Exception as e: + print(f"Erreur lors du chargement de la musique de fond: {e}") + + def pause_background_music(self): + """Met en pause la musique de fond""" + pygame.mixer.music.pause() + + def resume_background_music(self): + """Reprend la musique de fond""" + pygame.mixer.music.unpause() + + def stop_background_music(self): + """Arrête la musique de fond""" + pygame.mixer.music.stop() + + def set_background_music_volume(self, volume): + """Règle le volume de la musique de fond""" + pygame.mixer.music.set_volume(volume) + + def get_background_music_volume(self): + """Retourne le volume courant de la musique de fond""" + return pygame.mixer.music.get_volume() + + def update_effects(self): + """Mise à jour des effets visuels""" + if TRAIL_EFFECT: + self.player.trail.append((self.player.rect.center, 255)) + # Faire disparaître progressivement la traînée + self.player.trail = [(pos, alpha-10) for pos, alpha in self.player.trail if alpha > 0] + + def draw_wave_transition(self): + """Affiche la transition entre les vagues""" + if self.wave_transition: + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + overlay.fill(BG_COLOR) + overlay.set_alpha(128) + self.screen.blit(overlay, (0, 0)) + + wave_text = self.big_font.render(f"VAGUE {self.wave}", True, CYAN) + wave_rect = wave_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) + self.screen.blit(wave_text, wave_rect) + + def draw_hud(self): + """Affiche l'interface utilisateur""" + # Affichage de la santé à gauche + health_value = round(self.player.health, 2) # Arrondir à 2 décimales + if health_value == int(health_value): + health_display = str(int(health_value)) # Afficher sans décimales si c'est un entier + else: + health_display = f"{health_value:.2f}" # Afficher avec 2 décimales + + health_text = self.font.render(f"Vie: {health_display}", True, WHITE) + health_rect = health_text.get_rect(topleft=(20, 20)) + self.screen.blit(health_text, health_rect) + + # Barre de vie + health_bar_width = 200 + health_bar_height = 20 + health_ratio = max(0, self.player.health / 100) + pygame.draw.rect(self.screen, RED, (20, 50, health_bar_width, health_bar_height), 2) + pygame.draw.rect(self.screen, RED, (20, 50, health_bar_width * health_ratio, health_bar_height)) + + # Affichage du score actuel en haut à droite + score_text = self.font.render(f"Score: {self.player.score}", True, WHITE) + score_rect = score_text.get_rect(topright=(SCREEN_WIDTH - 20, 20)) + self.screen.blit(score_text, score_rect) + + # Affichage spécifique selon le mode de jeu + if isinstance(self.current_game_mode, ClassicMode): + # Mode Classique : Temps de survie + survival_text = self.font.render( + f"Temps de survie: {self.current_game_mode.format_time(self.current_game_mode.survival_time)}", + True, WHITE + ) + survival_rect = survival_text.get_rect(midtop=(SCREEN_WIDTH // 2, 20)) + self.screen.blit(survival_text, survival_rect) + + if self.current_game_mode.best_survival_time > 0: + best_time_text = self.font.render( + f"Record: {self.current_game_mode.format_time(self.current_game_mode.best_survival_time)}", + True, GOLD_COLOR + ) + best_time_rect = best_time_text.get_rect(midtop=(SCREEN_WIDTH // 2, 50)) + self.screen.blit(best_time_text, best_time_rect) + + elif isinstance(self.current_game_mode, SurvivalMode): + # Mode Survie : Affichage de la vague actuelle et du record + wave_text = self.font.render( + f"Vague actuelle: {self.current_game_mode.wave}", + True, CYAN + ) + wave_rect = wave_text.get_rect(midtop=(SCREEN_WIDTH // 2, 20)) + self.screen.blit(wave_text, wave_rect) + + if self.current_game_mode.best_wave > 0: + best_wave_text = self.font.render( + f"Record: Vague {self.current_game_mode.best_wave}", + True, GOLD_COLOR + ) + best_wave_rect = best_wave_text.get_rect(midtop=(SCREEN_WIDTH // 2, 50)) + self.screen.blit(best_wave_text, best_wave_rect) + + elif isinstance(self.current_game_mode, BossRushMode): + # Mode Boss Rush : Compteur de boss + boss_count_text = self.font.render( + f"Boss vaincus: {self.current_game_mode.bosses_killed}", + True, WHITE + ) + boss_count_rect = boss_count_text.get_rect(midtop=(SCREEN_WIDTH // 2, 20)) + self.screen.blit(boss_count_text, boss_count_rect) + + if self.current_game_mode.best_bosses_killed > 0: + record_text = self.font.render( + f"Record: {self.current_game_mode.best_bosses_killed} boss", + True, GOLD_COLOR + ) + record_rect = record_text.get_rect(midtop=(SCREEN_WIDTH // 2, 50)) + self.screen.blit(record_text, record_rect) + + # Affichage de la vague (sauf pour le mode classique) + if not isinstance(self.current_game_mode, ClassicMode): + wave_text = self.font.render(f"Vague {self.wave}", True, CYAN) + wave_rect = wave_text.get_rect(midtop=(SCREEN_WIDTH // 2, 80)) + self.screen.blit(wave_text, wave_rect) + + # Informations sur les ennemis à droite + y_offset = 60 + spacing = 35 + + # Nombre d'ennemis actifs + enemies_text = self.font.render(f"Ennemis: {len(self.enemies)}", True, WHITE) + enemies_rect = enemies_text.get_rect(topright=(SCREEN_WIDTH - 20, y_offset)) + self.screen.blit(enemies_text, enemies_rect) + + # Ennemis restants à faire apparaître (sauf en mode classique) + if not isinstance(self.current_game_mode, ClassicMode): + remaining = self.current_game_mode.get_remaining_enemies() + remaining_text = self.font.render(f"Restants: {remaining}", True, WHITE) + remaining_rect = remaining_text.get_rect(topright=(SCREEN_WIDTH - 20, y_offset + spacing)) + self.screen.blit(remaining_text, remaining_rect) + + # Bouton son en bas à droite + button_x = SCREEN_WIDTH - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING + button_y = SCREEN_HEIGHT - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING + + # Dessiner le cercle du bouton + color = SOUND_ON_COLOR if not self.sound_muted else SOUND_OFF_COLOR + pygame.draw.circle(self.screen, color, (button_x + SOUND_BUTTON_SIZE//2, button_y + SOUND_BUTTON_SIZE//2), SOUND_BUTTON_SIZE//2) + + # Dessiner l'icône + if not self.sound_muted: + # Dessiner des ondes sonores + for i in range(3): + radius = (i + 1) * 5 + pygame.draw.arc(self.screen, WHITE, + (button_x + SOUND_BUTTON_SIZE//2 - radius, + button_y + SOUND_BUTTON_SIZE//2 - radius, + radius * 2, radius * 2), + -math.pi/4, math.pi/4, 2) + else: + # Dessiner une croix + start_x = button_x + SOUND_BUTTON_SIZE//4 + start_y = button_y + SOUND_BUTTON_SIZE//4 + end_x = button_x + SOUND_BUTTON_SIZE*3//4 + end_y = button_y + SOUND_BUTTON_SIZE*3//4 + pygame.draw.line(self.screen, WHITE, (start_x, start_y), (end_x, end_y), 2) + pygame.draw.line(self.screen, WHITE, (start_x, end_y), (end_x, start_y), 2) + + # Barre de cooldown du bouclier + shield_cooldown_width = 200 + shield_cooldown_height = 20 + current_time = pygame.time.get_ticks() + if self.player.shield_active: + shield_ratio = 1 - (current_time - self.player.last_shield_activation) / self.player.shield_duration + else: + shield_ratio = min(1, (current_time - self.player.last_shield_activation) / self.player.shield_cooldown) + + pygame.draw.rect(self.screen, RED, (20, 80, shield_cooldown_width, shield_cooldown_height), 2) + pygame.draw.rect(self.screen, CYAN, (20, 80, shield_cooldown_width * shield_ratio, shield_cooldown_height)) + + # Texte du cooldown du bouclier + shield_text = self.font.render("Bouclier", True, WHITE) + shield_rect = shield_text.get_rect(topleft=(20, 110)) + self.screen.blit(shield_text, shield_rect) + + def spawn_enemy(self): + """Spawn un ennemi""" + # Déléguer au mode de jeu actuel + if hasattr(self.current_game_mode, 'spawn_enemies'): + self.current_game_mode.spawn_enemies() + # Fallback sur la méthode originale si nécessaire + else: + self._legacy_spawn_enemy() + + def _legacy_spawn_enemy(self): + """Ancienne méthode de spawn d'ennemis (pour compatibilité)""" + current_time = pygame.time.get_ticks() + if current_time - self.last_spawn >= self.spawn_cooldown and self.current_game_mode.wave_enemies_left > 0: + health = 50 + (self.wave * 10) + speed = 2 + (self.wave * 0.5) + + if random.random() < SHOOTER_ENEMY_CHANCE: + enemy = ShootingEnemy(health * 1.5, speed * 0.8) + else: + enemy = Enemy(health, speed) + + enemy.game = self # Passer l'instance du jeu avec les sons + self.enemies.add(enemy) + self.current_game_mode.wave_enemies_left -= 1 + self.last_spawn = current_time + + def update_wave(self): + """Gestion des vagues d'ennemis""" + # On change de vague uniquement si tous les ennemis prévus sont apparus ET qu'il n'y en a plus sur le terrain + if self.wave_enemies_left <= 0 and len(self.enemies) == 0: + if not self.wave_transition: + self.wave_transition = True + self.wave_start_time = pygame.time.get_ticks() + self.wave += 1 + self.enemies_killed = 0 + # Calcul du nouveau nombre d'ennemis avec le coefficient + self.wave_enemies_left = int(ENEMIES_PER_WAVE * (WAVE_ENEMY_MULTIPLIER ** (self.wave - 1))) + self.spawn_cooldown = max(500, self.spawn_cooldown - WAVE_SPEEDUP) + print(f"Début de la transition de vague Game {self.wave}") # Log pour debug + + if self.wave_transition: + current_time = pygame.time.get_ticks() + if current_time - self.wave_start_time > WAVE_TRANSITION_TIME: + self.wave_transition = False + print(f"Fin de la transition de vague Game {self.wave}") # Log pour debug + + def update_enemies(self): + """Met à jour les ennemis""" + for enemy in self.enemies: + if isinstance(enemy, ShootingEnemy): + enemy.update((self.player.x, self.player.y)) + # Gestion des balles ennemies + for bullet in enemy.bullets: + if bullet.rect.colliderect(self.player.rect): + self.player.take_damage(ENEMY_BULLET_DAMAGE) + try: + self.player_hurt_sound.play() + except: + pass + bullet.kill() + enemy.bullets.update() + else: + enemy.move((self.player.x, self.player.y)) + + # Vérifie les collisions avec les balles + bullet_hits = pygame.sprite.spritecollide(enemy, self.player.bullets, False) + for bullet in bullet_hits: + # Utiliser la méthode handle_collision + # Récupérer le gestionnaire de particules si disponible + particle_manager = getattr(self, 'particle_manager', None) + + # Gérer la collision et déterminer si la balle doit être détruite + destroy_bullet = bullet.handle_collision(enemy, particle_manager) + + if enemy.health <= 0: + self.player.gain_score(ENEMY_SCORE) + self.enemies_killed += 1 + + # Lorsqu'un ennemi est tué, on décrémente le compteur + if hasattr(self.current_game_mode, 'wave_enemies_left'): + if self.current_game_mode.wave_enemies_left > 0: + self.current_game_mode.wave_enemies_left -= 1 + + # Chance de faire apparaître une pièce ou un coffre + if random.random() < 0.3: # 30% de chance + from game.core.pickup import Pickup + + # Calculer le bonus de pièces (à utiliser dans une future version) + coin_bonus_multiplier = self.player.coin_bonus + + if random.random() < 0.8: # 80% de chance pour une pièce, 20% pour un coffre + pickup = Pickup(enemy.rect.centerx, enemy.rect.centery, 'coin') + else: + pickup = Pickup(enemy.rect.centerx, enemy.rect.centery, 'chest') + self.pickups.add(pickup) + + if destroy_bullet: + bullet.kill() + + # Vérifie les collisions avec le joueur + if enemy.attack(self.player): + try: + self.player_hurt_sound.play() + except: + pass + if self.player.health <= 0: + self.game_over() + + def handle_game_over_events(self, events): + """Gère les événements dans l'écran de game over""" + for event in events: + print(f"Event dans game_over: {event}") + + if event.type == pygame.QUIT: + print("Événement QUIT détecté") + pygame.quit() + sys.exit() + + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + mouse_pos = event.pos + print(f"Clic à la position: {mouse_pos}") + + # Vérifier les clics sur les boutons + if self.game_over_buttons['restart'].is_clicked(mouse_pos): + print("Bouton 'Recommencer' cliqué") + self.reset_game() + self.current_game_mode.initialize() + self.game_state = PLAYING + return + + elif self.game_over_buttons['menu'].is_clicked(mouse_pos): + print("Bouton 'Menu Principal' cliqué") + self.reset_game() + self.game_state = MENU + return + + elif self.game_over_buttons['quit'].is_clicked(mouse_pos): + print("Bouton 'Quitter le jeu' cliqué") + pygame.quit() + sys.exit() + else: + print(f"Clic en dehors des boutons") + + # Conserver aussi la gestion des touches pour les utilisateurs habitués + elif event.type == pygame.KEYDOWN: + print(f"Touche pressée: {pygame.key.name(event.key)}") + if event.key == pygame.K_SPACE: + print("ESPACE pressé - lancement du jeu") + self.reset_game() + self.current_game_mode.initialize() + self.game_state = PLAYING + return + elif event.key == pygame.K_ESCAPE: + print("ÉCHAP pressé - retour au menu") + self.reset_game() + self.game_state = MENU + return + + # Mettre à jour l'état de survol des boutons même sans événement + mouse_pos = pygame.mouse.get_pos() + for button_name, button in self.game_over_buttons.items(): + old_state = button.is_hovered + button.is_hovered = button.rect.collidepoint(mouse_pos) + if old_state != button.is_hovered: + print(f"Bouton '{button_name}' état survol changé: {button.is_hovered}") + + def draw_game_over(self): + """Affiche l'écran de game over""" + # Fond semi-transparent + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + overlay.fill(BG_COLOR) + overlay.set_alpha(128) + self.screen.blit(overlay, (0, 0)) + + # Texte "GAME OVER" + game_over_text = self.big_font.render("GAME OVER", True, RED) + game_over_rect = game_over_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 3)) + self.screen.blit(game_over_text, game_over_rect) + + # Score final + score_text = self.font.render(f"Score Final: {self.player.score}", True, WHITE) + score_rect = score_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) + self.screen.blit(score_text, score_rect) + + # Affichage du meilleur score + highscore_text = self.font.render(f"Meilleur Score: {self.highscore}", True, CYAN) + highscore_rect = highscore_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 40)) + self.screen.blit(highscore_text, highscore_rect) + + # Affichage des boutons et débug de leur position + print("Position des boutons de game over:") + for name, button in self.game_over_buttons.items(): + print(f"Bouton '{name}': {button.rect}") + button.draw(self.screen) + + def save_game_state(self): + """Sauvegarde l'état du jeu""" + try: + if not os.path.exists(os.path.dirname(SAVE_FILE)): + os.makedirs(os.path.dirname(SAVE_FILE)) + + # Récupération des records pour chaque mode + records = { + 'classic': { + 'best_survival_time': self.game_modes['classic'].best_survival_time + }, + 'survival': { + 'best_wave': self.game_modes['survival'].best_wave + }, + 'boss_rush': { + 'best_bosses_killed': self.game_modes['boss_rush'].best_bosses_killed + } + } + + # Sauvegarde des modules + if hasattr(self, 'module_manager'): + self.module_manager.save_state() + + # Récupération de l'état des personnages + characters_state = {} + if hasattr(self, 'character_shop'): + characters_state = self.character_shop.save_state() + + with open(SAVE_FILE, 'w') as f: + json.dump({ + 'highscore': self.highscore, + 'wave': self.wave, + 'score': self.player.score if hasattr(self, 'player') else 0, + 'records': records, + 'characters': characters_state + }, f) + print('État du jeu sauvegardé avec succès') + except Exception as e: + print(f"Erreur lors de la sauvegarde de l'état du jeu: {e}") + + # ... [Toutes les autres méthodes de la classe Game] \ No newline at end of file diff --git a/src/game/core/pickup.py b/src/game/core/pickup.py new file mode 100644 index 0000000..654dab4 --- /dev/null +++ b/src/game/core/pickup.py @@ -0,0 +1,98 @@ +import pygame +import os +import random +from utils.constants import * + +class Pickup(pygame.sprite.Sprite): + def __init__(self, x, y, pickup_type): + super().__init__() + self.pickup_type = pickup_type + + # Chargement de l'image en fonction du type + try: + if pickup_type == 'coin': + self.image = pygame.image.load(COIN_ICON).convert_alpha() + elif pickup_type == 'chest': + self.image = pygame.image.load(os.path.join(ICONS_DIR, 'chest.png')).convert_alpha() + + # Redimensionner l'image + self.image = pygame.transform.scale(self.image, (32, 32)) + except Exception as e: + print(f'Erreur lors du chargement de l\'image du {pickup_type}: {e}') + # Image par défaut si erreur + self.image = pygame.Surface((32, 32)) + if pickup_type == 'coin': + self.image.fill(GOLD_COLOR) + else: + self.image.fill((139, 69, 19)) # Marron pour le coffre + + self.rect = self.image.get_rect() + self.rect.center = (x, y) + + # Variables pour l'animation de flottement + self.original_y = y + self.float_offset = 0 + self.float_speed = 2 + self.float_direction = 1 + self.lifetime = 10000 # Durée de vie en ms + self.spawn_time = pygame.time.get_ticks() + + # Variables pour l'attraction vers le joueur + self.is_attracted = False + self.attraction_speed = 5 + self.x = float(x) + self.y = float(y) + + def update(self, player=None): + current_time = pygame.time.get_ticks() + + # Vérifier si le pickup doit disparaître + if current_time - self.spawn_time > self.lifetime: + self.kill() + return + + # Animation de flottement si pas attiré + if not self.is_attracted: + self.float_offset += self.float_speed * self.float_direction * 0.1 + if abs(self.float_offset) > 5: + self.float_direction *= -1 + self.rect.centery = self.original_y + self.float_offset + + # Vérifier si le joueur est assez proche pour être attiré + if player and self.rect.colliderect(player.rect.inflate(100, 100)): + self.is_attracted = True + + # Attraction vers le joueur + elif player: + dx = player.rect.centerx - self.x + dy = player.rect.centery - self.y + dist = (dx * dx + dy * dy) ** 0.5 + + if dist > 0: + self.x += (dx / dist) * self.attraction_speed + self.y += (dy / dist) * self.attraction_speed + + self.rect.centerx = int(self.x) + self.rect.centery = int(self.y) + + # Collision avec le joueur + if self.rect.colliderect(player.rect): + if self.pickup_type == 'coin': + player.game.economy_manager.add_coins(random.randint(5, 15)) + if player.game.coin_sound: + player.game.coin_sound.play() + elif self.pickup_type == 'chest': + # Obtenir 3 modules temporaires aléatoires + available_modules = player.game.module_manager.get_random_temp_modules(3) + if available_modules: + # Afficher le menu de sélection des modules + player.game.module_selection.show(available_modules) + player.game.show_module_menu = True + if player.game.achievement_sound: + player.game.achievement_sound.play() + else: + # Si aucun module n'est disponible, donner des pièces + player.game.economy_manager.add_coins(random.randint(10, 25)) + if player.game.coin_sound: + player.game.coin_sound.play() + self.kill() \ No newline at end of file diff --git a/src/game/core/player.py b/src/game/core/player.py new file mode 100644 index 0000000..7440004 --- /dev/null +++ b/src/game/core/player.py @@ -0,0 +1,346 @@ +import pygame +import math +import os +import random +from utils.constants import * +from game.core.weapon import Bullet + +class Player(pygame.sprite.Sprite): + def __init__(self, game): + super().__init__() + self.game = game + + # Attributs pour les effets visuels + self.trail = [] + + # Chargement de l'image du joueur + try: + # Essayer d'abord de charger player.png + sprite_path = os.path.join(ASSETS_DIR, 'characters', 'player.png') + if os.path.exists(sprite_path): + self.original_image = pygame.image.load(sprite_path).convert_alpha() + + # Si un personnage est sélectionné, utiliser son sprite + if hasattr(game, 'character_shop') and game.character_shop.selected_character: + selected_char = game.character_shop.selected_character + # Utiliser le sprite du personnage sélectionné + self.original_image = selected_char.sprite.copy() + print(f'Utilisation du sprite du personnage {selected_char.id}') + else: + # Fallback sur default.png + sprite_path = os.path.join(ASSETS_DIR, 'characters', 'default.png') + if os.path.exists(sprite_path): + self.original_image = pygame.image.load(sprite_path).convert_alpha() + else: + # Créer une image par défaut si aucun sprite n'est trouvé + self.original_image = pygame.Surface((PLAYER_SIZE, PLAYER_SIZE), pygame.SRCALPHA) + # Dessiner un vaisseau triangulaire + points = [(PLAYER_SIZE/2, 0), (0, PLAYER_SIZE), (PLAYER_SIZE, PLAYER_SIZE)] + pygame.draw.polygon(self.original_image, PLAYER_COLOR, points) + # Ajouter des détails + pygame.draw.polygon(self.original_image, (255, 255, 255), + [(PLAYER_SIZE/2, PLAYER_SIZE/4), + (PLAYER_SIZE/3, PLAYER_SIZE/2), + (PLAYER_SIZE/2, PLAYER_SIZE/1.5), + (PLAYER_SIZE/1.5, PLAYER_SIZE/2)]) + print('Image par défaut créée pour le joueur') + except Exception as e: + print(f"Erreur lors du chargement du sprite: {e}") + self.original_image = pygame.Surface((PLAYER_SIZE, PLAYER_SIZE), pygame.SRCALPHA) + pygame.draw.polygon(self.original_image, PLAYER_COLOR, points) + + # Préparation de l'image et du rectangle + self.image = self.original_image.copy() + self.rect = self.image.get_rect() + + # Position initiale + self.x = SCREEN_WIDTH // 2 + self.y = SCREEN_HEIGHT // 2 + self.rect.center = (self.x, self.y) + + # Statistiques de base + self.base_health = 100 + self.base_speed = 5 + self.base_bullet_damage = BULLET_DAMAGE + self.base_shoot_cooldown = 250 + self.base_shield_cooldown = 5000 # Ajout de l'attribut manquant + + # Appliquer les statistiques du personnage + self.apply_character_stats() + + # Initialisation des statistiques + self.reset_stats() + + # Attributs liés au tir + self.last_shot = 0 + self.double_shot_active = False + self.triple_shot_active = False + + # Attributs pour le bouclier + self.shield_cooldown = 5000 # 5 secondes entre les activations + self.last_shield_activation = 0 + self.shield_active = False + self.shield_duration = 2000 + + # Groupe de sprites pour les balles + self.bullets = pygame.sprite.Group() + + def apply_character_stats(self): + """Applique les statistiques du personnage sélectionné""" + if hasattr(self.game, 'character_shop') and self.game.character_shop.selected_character: + selected_char = self.game.character_shop.selected_character + stats = selected_char.stats + + # Appliquer les statistiques du personnage + if 'health' in stats: + self.base_health = stats['health'] + if 'speed' in stats: + self.base_speed = 5 * stats['speed'] # Multiplier la vitesse de base par le facteur + if 'damage' in stats: + self.base_bullet_damage = BULLET_DAMAGE * stats['damage'] # Multiplier les dégâts de base + + print(f"Statistiques du personnage {selected_char.id} appliquées : Santé={self.base_health}, Vitesse={self.base_speed}, Dégâts={self.base_bullet_damage}") + else: + print("Aucun personnage sélectionné, utilisation des statistiques par défaut") + + def reset_stats(self): + """Réinitialise les statistiques du joueur""" + # S'assurer d'abord que les bonnes statistiques de base sont appliquées + self.apply_character_stats() + + # Initialiser les statistiques + self.health = self.base_health + self.max_health = self.base_health + self.speed = self.base_speed + self.bullet_damage = self.base_bullet_damage + self.score = 0 + self.shoot_cooldown = self.base_shoot_cooldown + self.shield_cooldown = self.base_shield_cooldown # Utilisation de la valeur de base + + # Autres attributs après le reset + self.coin_magnetism = 0 + self.critical_chance = 0 + self.explosion_chance = 0 + self.reflect_chance = 0 + self.slowdown_power = 0 + self.coin_bonus = 1 + self.pierce_count = 0 + self.extra_bullets = 0 + self.health_regen = 0 + + # État du joueur + self.score = 0 + self.last_shot = 0 + self.last_shield = 0 + self.last_shield_activation = 0 + self.shield_active = False + self.shield_duration = 2000 + self.extra_bullets = 0 + + # Groupe de sprites pour les balles + self.bullets = pygame.sprite.Group() + # ... autres attributs spécifiques aux modules ... + + def move(self, dx, dy): + """Déplace le joueur""" + # Normalisation du vecteur de déplacement + length = math.sqrt(dx * dx + dy * dy) + if length > 0: + dx = dx / length * self.speed + dy = dy / length * self.speed + + # Mise à jour de la position + self.x = max(16, min(SCREEN_WIDTH - 16, self.x + dx)) + self.y = max(16, min(SCREEN_HEIGHT - 16, self.y + dy)) + self.rect.center = (self.x, self.y) + + def move_up(self): + """Déplace le joueur vers le haut""" + self.move(0, -1) + + def move_down(self): + """Déplace le joueur vers le bas""" + self.move(0, 1) + + def move_left(self): + """Déplace le joueur vers la gauche""" + self.move(-1, 0) + + def move_right(self): + """Déplace le joueur vers la droite""" + self.move(1, 0) + + def shoot(self): + """Tire une balle""" + current_time = pygame.time.get_ticks() + if current_time - self.last_shot > self.shoot_cooldown: + # Calcul de l'angle vers la souris + mouse_x, mouse_y = pygame.mouse.get_pos() + angle = math.atan2(mouse_y - self.y, mouse_x - self.x) + + # Calcul de la position de départ du projectile (à l'avant du vaisseau) + offset = PLAYER_SIZE // 3 # Réduit de //2 à //3 pour s'adapter à la nouvelle taille + start_x = self.x + math.cos(angle) * offset + start_y = self.y + math.sin(angle) * offset + + # Tir multiple selon le nombre de balles supplémentaires + angles = [] + if self.extra_bullets == 0: + angles = [angle] + elif self.extra_bullets == 1: + angles = [angle - 0.1, angle + 0.1] + elif self.extra_bullets == 2: + angles = [angle - 0.2, angle, angle + 0.2] + + # Création des balles + for shoot_angle in angles: + # Déterminer si c'est un tir critique + is_critical = random.random() < self.critical_chance + # Déterminer si c'est une balle explosive + is_explosive = random.random() < self.explosion_chance + + # Calculer les dégâts (doublés pour les critiques) + damage = self.bullet_damage * (2 if is_critical else 1) + + # Créer la balle avec les attributs spéciaux + bullet = Bullet( + start_x, start_y, shoot_angle, damage, + is_enemy=False, + is_critical=is_critical, + is_explosive=is_explosive, + pierce_count=self.pierce_count, + slowdown_power=self.slowdown_power + ) + self.bullets.add(bullet) + + # Effet visuel de tir + self.last_shot_effect = current_time + + # Son de tir + if hasattr(self.game, 'shoot_sound') and self.game.shoot_sound and not self.game.sound_muted: + self.game.shoot_sound.play() + + self.last_shot = current_time + + def activate_shield(self): + """Active le bouclier""" + current_time = pygame.time.get_ticks() + if current_time - self.last_shield > self.shield_cooldown: + self.shield_active = True + self.last_shield = current_time + self.last_shield_activation = current_time + + def update(self): + """Met à jour l'état du joueur""" + # Mise à jour de l'angle de rotation en fonction de la position de la souris + mouse_x, mouse_y = pygame.mouse.get_pos() + dx = mouse_x - self.x + dy = mouse_y - self.y + self.angle = -math.degrees(math.atan2(dy, dx)) + 90 # +90 au lieu de -90 pour pivoter de 180 degrés + + # Mise à jour du bouclier + current_time = pygame.time.get_ticks() + if self.shield_active and current_time - self.last_shield > self.shield_duration: + self.shield_active = False + + # Régénération de santé + if self.health_regen > 0 and self.health < self.max_health: + self.health = min(self.max_health, self.health + self.health_regen / 60) # 60 FPS + + # Mise à jour des balles + self.bullets.update() + + # Suppression des balles hors écran + for bullet in self.bullets: + if not bullet.is_on_screen(): + bullet.kill() + + def draw(self, screen): + """Dessine le joueur et ses balles""" + # Dessin des balles + self.bullets.draw(screen) + + # Rotation de l'image du joueur + rotated_image = pygame.transform.rotate(self.original_image, self.angle) + rotated_rect = rotated_image.get_rect(center=self.rect.center) + + # Effet de tir (flash à l'avant du vaisseau) + current_time = pygame.time.get_ticks() + if current_time - self.last_shot_effect < 100: # Effet pendant 100ms + # Calculer la position du flash + flash_angle_rad = math.radians(self.angle - 90) # Convertir en radians et ajuster + flash_offset = PLAYER_SIZE // 3 # Réduit de //2 à //3 + flash_x = self.rect.centerx + math.cos(flash_angle_rad) * flash_offset + flash_y = self.rect.centery + math.sin(flash_angle_rad) * flash_offset + + # Dessiner le flash + flash_radius = 3 + (current_time - self.last_shot_effect) // 20 # Réduit de 5 à 3 + flash_alpha = 255 - (current_time - self.last_shot_effect) * 2.5 + + # Créer une surface pour le flash avec transparence + flash_surface = pygame.Surface((flash_radius * 2, flash_radius * 2), pygame.SRCALPHA) + pygame.draw.circle(flash_surface, (255, 200, 100, flash_alpha), (flash_radius, flash_radius), flash_radius) + + # Dessiner le flash + flash_rect = flash_surface.get_rect(center=(flash_x, flash_y)) + screen.blit(flash_surface, flash_rect) + + # Dessin du joueur + screen.blit(rotated_image, rotated_rect) + + # Dessin du bouclier si actif + if self.shield_active: + shield_radius = PLAYER_SIZE + 5 # Réduit de +10 à +5 + pygame.draw.circle(screen, (0, 255, 255), self.rect.center, shield_radius, 2) + # Effet de lueur pour le bouclier + pygame.draw.circle(screen, (0, 200, 200, 50), self.rect.center, shield_radius - 2, 1) + + def take_damage(self, damage): + """Inflige des dégâts au joueur""" + if not self.shield_active: + self.health -= damage + return True + return False + + def gain_score(self, points): + """Augmente le score du joueur""" + self.score += points + + def play_sound(self, sound_type): + if hasattr(self, 'game'): + print(f"Game instance exists, sound_muted: {self.game.sound_muted}") + if not self.game.sound_muted: + if sound_type == 'shoot' and self.game.shoot_sound: + print("Playing shoot sound") + self.game.shoot_sound.play() + elif sound_type == 'hurt' and self.game.player_hurt_sound: + print("Playing hurt sound") + self.game.player_hurt_sound.play() + else: + print("No game instance found") + + def die(self): + # Logique pour la mort du joueur + if hasattr(self, 'game'): + self.game.game_over() + + def attract_coins(self, pickups): + """Attire les pièces vers le joueur si l'effet coin_magnetism est actif""" + # Si l'effet d'attraction n'est pas disponible, ne rien faire + if not hasattr(self, 'coin_magnetism') or self.coin_magnetism <= 0: + return + + # Définir la portée d'attraction basée sur la puissance de l'effet + attraction_radius = 100 + (self.coin_magnetism * 20) + + # Créer un rectangle d'attraction plus grand autour du joueur + attraction_rect = self.rect.inflate(attraction_radius, attraction_radius) + + # Parcourir tous les pickups et les attirer s'ils sont à portée + for pickup in pickups: + if hasattr(pickup, 'is_attracted') and not pickup.is_attracted: + if pickup.rect.colliderect(attraction_rect): + pickup.is_attracted = True + # Augmenter la vitesse d'attraction en fonction de l'effet + if hasattr(pickup, 'attraction_speed'): + pickup.attraction_speed = 5 + (self.coin_magnetism * 0.5) \ No newline at end of file diff --git a/src/game/core/weapon.py b/src/game/core/weapon.py new file mode 100644 index 0000000..7358579 --- /dev/null +++ b/src/game/core/weapon.py @@ -0,0 +1,150 @@ +import pygame +import math +from utils.constants import * + +class Bullet(pygame.sprite.Sprite): + def __init__(self, x, y, direction, damage=BULLET_DAMAGE, is_enemy=False, + is_critical=False, is_explosive=False, pierce_count=0, slowdown_power=0): + super().__init__() + # Convertir l'angle en vecteur de direction + self.direction_angle = direction + self.direction_x = math.cos(direction) + self.direction_y = math.sin(direction) + self.speed = BULLET_SPEED + self.damage = damage + self.is_enemy = is_enemy + + # Nouvelles propriétés pour les modules + self.is_critical = is_critical + self.is_explosive = is_explosive + self.pierce_count = pierce_count # Nombre d'ennemis que la balle peut traverser + self.pierced_enemies = 0 # Nombre d'ennemis déjà touchés + self.slowdown_power = slowdown_power # Pourcentage de ralentissement + + # Couleurs différentes selon le type de balle + if is_enemy: + self.colors = BULLET_COLORS["ENEMY"] + else: + if is_critical: + self.colors = {"CORE": (255, 50, 50), "GLOW": (255, 100, 100), "TRAIL": (255, 70, 70, 80)} + elif is_explosive: + self.colors = {"CORE": (255, 165, 0), "GLOW": (255, 200, 0), "TRAIL": (255, 180, 0, 80)} + else: + self.colors = BULLET_COLORS["PLAYER"] + + # Pré-rendu de l'effet de projectile + size_multiplier = 1.5 if is_critical else 1.0 + self.image = pygame.Surface((BULLET_SURFACE_SIZE, BULLET_SURFACE_SIZE), pygame.SRCALPHA) + center = BULLET_SURFACE_SIZE // 2 + + # Effet de lueur optimisé + pygame.draw.circle(self.image, self.colors["GLOW"], (center, center), int(BULLET_SIZE * 2 * size_multiplier)) + pygame.draw.circle(self.image, self.colors["CORE"], (center, center), int(BULLET_SIZE * size_multiplier)) + + self.rect = self.image.get_rect(center=(x, y)) + self.trail_positions = [] + self.frame_count = 0 + + # Position précise pour éviter l'accumulation d'erreurs d'arrondi + self.x = float(x) + self.y = float(y) + + def update(self): + # Mise à jour de la position avec les coordonnées précises + self.x += self.direction_x * self.speed + self.y += self.direction_y * self.speed + self.rect.x = int(self.x) + self.rect.y = int(self.y) + + # Mise à jour de la traînée moins fréquente + self.frame_count = (self.frame_count + 1) % TRAIL_UPDATE_FREQUENCY + if self.frame_count == 0: + self.trail_positions.insert(0, self.rect.center) + if len(self.trail_positions) > MAX_TRAIL_LENGTH: + self.trail_positions.pop() + + def draw(self, screen): + # Dessin optimisé de la traînée + for i, pos in enumerate(self.trail_positions): + alpha = 255 * (1 - i/MAX_TRAIL_LENGTH) + size = BULLET_SIZE * (1 - i/MAX_TRAIL_LENGTH) + if size > 1: # Évite de dessiner des cercles trop petits + pygame.draw.circle(screen, (*self.colors["TRAIL"][:3], int(alpha)), + pos, int(size)) + + screen.blit(self.image, self.rect) + + def is_on_screen(self): + """Vérifie si la balle est toujours à l'écran""" + return (0 <= self.rect.x <= SCREEN_WIDTH and + 0 <= self.rect.y <= SCREEN_HEIGHT) + + def handle_collision(self, enemy, particle_manager=None): + """Gère la collision avec un ennemi et retourne True si la balle doit être détruite""" + # Infliger des dégâts + enemy.take_damage(self.damage) + + # Appliquer un ralentissement si la balle a cet effet + if self.slowdown_power > 0: + self.apply_slowdown(enemy) + + # Créer une explosion si c'est une balle explosive + if self.is_explosive and enemy.health <= 0 and particle_manager: + self.create_explosion(enemy, particle_manager) + + # Vérifier si la balle peut traverser d'autres ennemis + if self.pierce_count > 0 and self.pierced_enemies < self.pierce_count: + self.pierced_enemies += 1 + return False # Ne pas détruire la balle + + return True # Détruire la balle + + def apply_slowdown(self, enemy): + """Ralentit un ennemi touché""" + if hasattr(enemy, 'slow_factor'): + # Appliquer le ralentissement (par exemple, réduire la vitesse de 20% pour un slowdown_power de 0.2) + enemy.slow_factor = max(enemy.slow_factor, self.slowdown_power) + enemy.slow_duration = pygame.time.get_ticks() + 3000 # Effet durant 3 secondes + + def create_explosion(self, enemy, particle_manager): + """Crée une explosion au point d'impact""" + explosion_radius = BULLET_SIZE * 5 # Rayon de l'explosion + explosion_damage = self.damage * 0.5 # Dégâts de l'explosion (moitié des dégâts directs) + + # Créer un effet visuel d'explosion + if particle_manager: + particle_manager.create_explosion( + enemy.rect.centerx, enemy.rect.centery, + color=(255, 165, 0), # Orange pour les explosions + num_particles=20, + spread=10 + ) + + # Appliquer des dégâts aux ennemis proches + if hasattr(enemy, 'game') and enemy.game: + for nearby_enemy in enemy.game.enemies: + if nearby_enemy != enemy: # Ne pas endommager à nouveau la cible principale + distance = math.sqrt( + (nearby_enemy.rect.centerx - enemy.rect.centerx) ** 2 + + (nearby_enemy.rect.centery - enemy.rect.centery) ** 2 + ) + if distance < explosion_radius: + # Dégâts inversement proportionnels à la distance + damage_factor = 1 - (distance / explosion_radius) + nearby_enemy.take_damage(explosion_damage * damage_factor) + +class Weapon: + def __init__(self): + self.bullets = pygame.sprite.Group() + self.last_shot = 0 + self.cooldown = 250 # Milliseconds + + def shoot(self, pos, target): + current_time = pygame.time.get_ticks() + if current_time - self.last_shot >= self.cooldown: + direction = pygame.math.Vector2(target[0] - pos[0], target[1] - pos[1]) + if direction.length() > 0: + direction = direction.normalize() + bullet = Bullet(pos[0], pos[1], direction) + self.bullets.add(bullet) + self.last_shot = current_time \ No newline at end of file diff --git a/src/game/modes/__init__.py b/src/game/modes/__init__.py new file mode 100644 index 0000000..08c127d --- /dev/null +++ b/src/game/modes/__init__.py @@ -0,0 +1 @@ +# Package pour les différents modes de jeu \ No newline at end of file diff --git a/src/game/modes/boss_rush_mode.py b/src/game/modes/boss_rush_mode.py new file mode 100644 index 0000000..6628b1f --- /dev/null +++ b/src/game/modes/boss_rush_mode.py @@ -0,0 +1,227 @@ +import pygame +import random +from game.modes.game_mode_base import GameModeBase +from utils.constants import * +from game.core.enemy import Enemy, ShootingEnemy + +class BossRushMode(GameModeBase): + def __init__(self, game): + super().__init__(game, + id='boss_rush', + name='Mode Boss Rush', + description='Affrontez uniquement les boss les plus puissants') + self.boss = None + self.minions_count = 0 + self.boss_health = 0 + self.boss_max_health = 0 + self.bosses_killed = 0 + self.best_bosses_killed = 0 + + def initialize(self): + """Initialise le mode de jeu boss rush""" + self.wave = 1 + self.wave_enemies_left = 0 # Pas d'ennemis normaux, seulement des boss + self.wave_transition = False + self.wave_start_time = 0 + self.boss = None + self.minions_count = 0 + self.bosses_killed = 0 + self.best_bosses_killed = 0 + self.spawn_boss() + + def update(self): + """Met à jour la logique du mode boss rush""" + # Gestion de la transition de vague en priorité + if self.wave_transition: + current_time = pygame.time.get_ticks() + if current_time - self.wave_start_time > WAVE_TRANSITION_TIME: + self.wave_transition = False + self.spawn_boss() + print(f"Fin de la transition de vague {self.wave}, nouveau boss spawné") + return # Sortir de la fonction pendant la transition + + # Gérer les interactions normales hors transition + if self.boss and self.boss in self.game.enemies: + # Spawn des sbires si le boss est présent + if self.minions_count > 0: + self.spawn_minions() + # Vérifier si le boss est mort + elif self.boss and self.boss not in self.game.enemies: + self.boss = None + self.handle_wave_completion() + + self.update_wave() + + def spawn_boss(self): + """Spawn un boss""" + # Utiliser la configuration de la vague + wave_config = self.get_wave_config(self.wave) + self.boss_health = wave_config['boss_health'] + self.boss_max_health = self.boss_health + boss_speed = wave_config['boss_speed'] + + # Utiliser un ShootingEnemy comme boss pour l'instant + self.boss = ShootingEnemy(self.boss_health, boss_speed) + self.boss.size = PLAYER_SIZE * 2 # Boss plus grand + self.boss.color = (255, 0, 0) # Rouge vif + self.boss.shoot_cooldown = 500 # Tirs plus fréquents + self.boss.game = self.game + + # Positionner le boss en haut de l'écran + self.boss.x = SCREEN_WIDTH // 2 + self.boss.y = 100 + + self.game.enemies.add(self.boss) + + # Nombre de sbires à spawner + self.minions_count = wave_config['minions'] + + def spawn_minions(self): + """Spawn des sbires pour accompagner le boss""" + # Limiter le nombre d'ennemis actifs pour des performances optimales + if len(self.game.enemies) >= MAX_ACTIVE_ENEMIES: + return + + current_time = pygame.time.get_ticks() + if current_time - self.game.last_spawn >= self.game.spawn_cooldown and self.minions_count > 0: + # Sbires plus faibles que le boss + health = 50 + (self.wave * 10) + speed = 3 + (self.wave * 0.3) + + enemy = Enemy(health, speed) + enemy.game = self.game + + # Positionner le sbire près du boss + enemy.x = self.boss.x + random.randint(-200, 200) + enemy.y = self.boss.y + random.randint(50, 150) + + self.game.enemies.add(enemy) + self.minions_count -= 1 + self.game.last_spawn = current_time + + def update_wave(self): + """Gestion des vagues de boss""" + # Vérifier si la période de transition est terminée + if self.wave_transition: + current_time = pygame.time.get_ticks() + if current_time - self.wave_start_time > WAVE_TRANSITION_TIME: + self.wave_transition = False + self.spawn_boss() + print(f"Fin de la transition de vague {self.wave}, nouveau boss spawné") # Log pour debug + + def handle_wave_completion(self): + """Gestion de la fin d'une vague (boss vaincu)""" + self.wave_transition = True + self.wave_start_time = pygame.time.get_ticks() + self.wave += 1 + self.game.enemies_killed = 0 + self.bosses_killed += 1 + + # Mise à jour du record + if self.bosses_killed > self.best_bosses_killed: + self.best_bosses_killed = self.bosses_killed + # Jouer un son de réussite pour le nouveau record + if hasattr(self.game, 'achievement_sound') and self.game.achievement_sound and not self.game.sound_muted: + self.game.achievement_sound.play() + + # Récompenses importantes pour avoir vaincu un boss + coins_earned = self.wave * COINS_PER_BOSS + self.game.economy_manager.add_coins(coins_earned) + + # Notification + self.game.notification_manager.add_notification( + f'Boss vaincu ! ({self.bosses_killed} boss tués)', + f'+{coins_earned} pièces', + COIN_ICON + ) + + # Son de récompense + if hasattr(self.game, 'coin_sound') and self.game.coin_sound and not self.game.sound_muted: + self.game.coin_sound.play() + + def handle_events(self, events): + """Gestion des événements spécifiques au mode boss rush""" + # Pas d'événements spécifiques pour le moment + pass + + def draw(self, screen): + """Affichage spécifique au mode boss rush""" + # Affichage du joueur + self.game.player.draw(screen) + + # Affichage des ennemis + for enemy in self.game.enemies: + enemy.draw(screen) + if isinstance(enemy, ShootingEnemy): + enemy.bullets.draw(screen) + + # Affichage de la barre de vie du boss + if self.boss and self.boss in self.game.enemies: + # Dimensions et position de la barre de vie + boss_health_bar_width = SCREEN_WIDTH * 0.5 + boss_health_bar_height = 25 + boss_health_bar_x = (SCREEN_WIDTH - boss_health_bar_width) // 2 + boss_health_bar_y = SCREEN_HEIGHT - 100 + + # Fond avec bordure + pygame.draw.rect(screen, (30, 0, 0), + (boss_health_bar_x - 4, boss_health_bar_y - 4, + boss_health_bar_width + 8, boss_health_bar_height + 8)) + + # Fond de la barre + pygame.draw.rect(screen, (70, 0, 0), + (boss_health_bar_x, boss_health_bar_y, + boss_health_bar_width, boss_health_bar_height)) + + # Barre de vie actuelle + health_ratio = max(0, self.boss.health / self.boss_max_health) + if health_ratio < 0.3: + bar_color = (255, 30, 30) + elif health_ratio < 0.6: + bar_color = (255, 120, 30) + else: + bar_color = (255, 180, 30) + + pygame.draw.rect(screen, bar_color, + (boss_health_bar_x, boss_health_bar_y, + boss_health_bar_width * health_ratio, boss_health_bar_height)) + + # Nom du boss + boss_font = pygame.font.Font(None, 36) + boss_name = f"BOSS ALIEN NIVEAU {self.wave}" + boss_text = boss_font.render(boss_name, True, (255, 200, 50)) + boss_text_rect = boss_text.get_rect(midbottom=(SCREEN_WIDTH // 2, boss_health_bar_y - 15)) + screen.blit(boss_text, boss_text_rect) + + # Effet de brillance + pygame.draw.rect(screen, (255, 150, 50), + (boss_health_bar_x, boss_health_bar_y, + boss_health_bar_width, 2)) + + # Affichage de la transition de vague + if self.wave_transition: + self.draw_wave_transition(screen) + + def spawn_enemies(self): + """Implémentation de la méthode abstraite spawn_enemies""" + # Dans ce mode, nous ne spawnons pas d'ennemis normaux + # mais plutôt un boss et ses sbires + if self.boss is None: + self.spawn_boss() + elif self.minions_count > 0: + self.spawn_minions() + + def get_wave_config(self, wave_number: int) -> dict: + """Retourne la configuration de la vague pour le mode boss rush""" + return { + 'boss': True, + 'boss_health': 1000 + wave_number * 500, + 'boss_speed': min(0.5 + wave_number * 0.1, 1.5), + 'minions': wave_number * 2 + } + + def get_record_text(self) -> str: + """Retourne le texte du record pour le mode boss rush""" + if self.best_bosses_killed > 0: + return f"Record: {self.best_bosses_killed} boss" + return "Pas encore de record" \ No newline at end of file diff --git a/src/game/modes/classic_mode.py b/src/game/modes/classic_mode.py new file mode 100644 index 0000000..c0b24ae --- /dev/null +++ b/src/game/modes/classic_mode.py @@ -0,0 +1,133 @@ +import pygame +import random +from game.modes.game_mode_base import GameModeBase +from utils.constants import * +from game.core.enemy import Enemy, ShootingEnemy + +class ClassicMode(GameModeBase): + def __init__(self, game): + super().__init__(game, + id='classic', + name='Mode Classique', + description='Survivez le plus longtemps possible') + self.survival_time = 0 + self.best_survival_time = 0 + self.start_time = pygame.time.get_ticks() + self.paused_time = 0 + self.is_paused = False + self.difficulty_multiplier = 1.0 + self.last_difficulty_increase = 0 + + def initialize(self): + """Initialise le mode de jeu classique""" + self.start_time = pygame.time.get_ticks() + self.survival_time = 0 + self.paused_time = 0 + self.is_paused = False + self.difficulty_multiplier = 1.0 + self.last_difficulty_increase = 0 + # Réinitialiser le spawn cooldown du jeu + self.game.spawn_cooldown = 2000 + + def update(self): + """Met à jour la logique du mode classique""" + # Mise à jour du temps de survie uniquement si le jeu est en cours + if self.game.game_state == PLAYING: + if self.is_paused: + # Ajuster le temps de départ pour compenser la pause + self.start_time = pygame.time.get_ticks() - (self.survival_time * 1000) + self.is_paused = False + + # Calculer le temps écoulé + self.survival_time = (pygame.time.get_ticks() - self.start_time) // 1000 + + # Augmenter la difficulté progressivement avec le temps + current_time = pygame.time.get_ticks() + if current_time - self.last_difficulty_increase > 30000: # Toutes les 30 secondes + self.difficulty_multiplier += 0.1 + self.game.spawn_cooldown = max(500, int(self.game.spawn_cooldown * 0.9)) # Spawn plus rapide + self.last_difficulty_increase = current_time + + # Mettre à jour le record si nécessaire + if self.survival_time > self.best_survival_time: + self.best_survival_time = self.survival_time + # Son de réussite pour le nouveau record + if hasattr(self.game, 'achievement_sound') and self.game.achievement_sound and not self.game.sound_muted: + self.game.achievement_sound.play() + + elif self.game.game_state == PAUSED and not self.is_paused: + self.is_paused = True + self.paused_time = pygame.time.get_ticks() + + def format_time(self, seconds): + """Formate le temps en minutes:secondes""" + minutes = seconds // 60 + seconds = seconds % 60 + return f"{minutes:02d}:{seconds:02d}" + + def spawn_enemies(self): + """Logique de spawn des ennemis en continu pour le mode classique""" + # Limiter le nombre d'ennemis actifs pour des performances optimales + if len(self.game.enemies) >= MAX_ACTIVE_ENEMIES: + return + + current_time = pygame.time.get_ticks() + if current_time - self.game.last_spawn >= self.game.spawn_cooldown: + # Configuration de l'ennemi basée sur le temps de survie + difficulty_level = max(1, self.survival_time // 30) # Augmente tous les 30 secondes + + base_health = 50 + (difficulty_level * 10) + base_speed = 1 + (difficulty_level * 0.1) + + # Calcul du ratio d'ennemis tireurs vs normaux (augmente avec le temps) + shooter_ratio = min(0.5, 0.1 + (difficulty_level * 0.02)) # Max 50% de tireurs + + # Appliquer le multiplicateur de difficulté + health = int(base_health * self.difficulty_multiplier) + speed = base_speed * self.difficulty_multiplier + + # Création de l'ennemi + if random.random() < shooter_ratio: + enemy = ShootingEnemy(health * 1.5, speed * 0.8) + else: + enemy = Enemy(health, speed) + + enemy.game = self.game + self.game.enemies.add(enemy) + self.game.last_spawn = current_time + + def get_remaining_enemies(self): + """Retourne le nombre d'ennemis restants (toujours 'infini' en mode classique)""" + return "∞" # Symbole infini + + def handle_wave_completion(self): + """Méthode requise par l'interface mais non utilisée en mode classique""" + # Cette méthode est requise par l'interface GameModeBase mais n'est pas utilisée + # car le mode classique n'utilise pas de vagues + pass + + def handle_events(self, events): + """Gestion des événements spécifiques au mode classique""" + # Pas d'événements spécifiques pour le moment + pass + + def draw(self, screen): + """Affichage spécifique au mode classique""" + # Affichage du joueur et des ennemis + self.game.player.draw(screen) + for enemy in self.game.enemies: + enemy.draw(screen) + if isinstance(enemy, ShootingEnemy): + enemy.bullets.draw(screen) + + # Affichage du temps de survie et du niveau de difficulté + font = pygame.font.Font(None, 30) + difficulty_text = font.render(f"Difficulté: x{self.difficulty_multiplier:.1f}", True, CYAN) + difficulty_rect = difficulty_text.get_rect(topleft=(20, 140)) + screen.blit(difficulty_text, difficulty_rect) + + def get_record_text(self) -> str: + """Retourne le texte du record pour le mode classique""" + if self.best_survival_time > 0: + return f"Record: {self.format_time(self.best_survival_time)}" + return "Pas encore de record" \ No newline at end of file diff --git a/src/game/modes/game_mode_base.py b/src/game/modes/game_mode_base.py new file mode 100644 index 0000000..8c4629d --- /dev/null +++ b/src/game/modes/game_mode_base.py @@ -0,0 +1,75 @@ +import pygame +from abc import ABC, abstractmethod + +class GameModeBase(ABC): + def __init__(self, game, id='classic', name='Mode Classique', description='Mode de jeu classique'): + self.game = game # Référence à l'instance principale du jeu + self.id = id + self.name = name + self.description = description + self.high_score = 0 + self.wave = 1 + self.wave_enemies_left = 0 + self.wave_transition = False + self.wave_start_time = 0 + + @abstractmethod + def initialize(self): + """Initialise le mode de jeu""" + pass + + @abstractmethod + def update(self): + """Met à jour la logique du mode de jeu""" + pass + + @abstractmethod + def handle_events(self, events): + """Gère les événements spécifiques au mode""" + pass + + @abstractmethod + def draw(self, screen): + """Dessine les éléments spécifiques au mode""" + pass + + @abstractmethod + def spawn_enemies(self): + """Logique de spawn des ennemis spécifique au mode""" + pass + + @abstractmethod + def handle_wave_completion(self): + """Gestion de la fin d'une vague""" + pass + + def draw_wave_transition(self, screen): + """Affiche la transition entre les vagues""" + from utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT, BG_COLOR, CYAN + + # Création d'une surface semi-transparente pour l'overlay + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + overlay.fill(BG_COLOR) + overlay.set_alpha(128) + screen.blit(overlay, (0, 0)) + + # Affichage du texte de la vague + wave_text = self.game.big_font.render(f"VAGUE {self.wave}", True, CYAN) + wave_rect = wave_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) + screen.blit(wave_text, wave_rect) + + def get_record_text(self) -> str: + """Retourne le texte du record formaté pour l'affichage dans le menu""" + return "Pas de record" # Par défaut, à surcharger dans les modes spécifiques + + def format_time(self, seconds): + """Formate le temps en minutes:secondes""" + minutes = seconds // 60 + seconds = seconds % 60 + return f"{minutes:02d}:{seconds:02d}" + + def get_remaining_enemies(self): + """Retourne le nombre d'ennemis restants à spawner""" + if hasattr(self, 'wave_enemies_left'): + return max(0, self.wave_enemies_left) + return 0 \ No newline at end of file diff --git a/src/game/modes/game_modes.py b/src/game/modes/game_modes.py new file mode 100644 index 0000000..7a76115 --- /dev/null +++ b/src/game/modes/game_modes.py @@ -0,0 +1,153 @@ +from typing import Dict, List, Tuple +import random + +class GameMode: + def __init__(self, id: str, name: str, description: str): + self.id = id + self.name = name + self.description = description + self.is_unlocked = False + self.high_score = 0 + + def get_wave_config(self, wave_number: int) -> dict: + """Retourne la configuration de la vague""" + pass + + def get_record_text(self) -> str: + """Retourne le texte du record pour ce mode""" + pass + +class ClassicMode(GameMode): + def __init__(self): + super().__init__('classic', 'Mode Classique', 'Survivez le plus longtemps possible') + self.is_unlocked = True + self.best_survival_time = 0 + + def get_wave_config(self, wave_number: int) -> dict: + return { + 'normal_enemies': wave_number * 2, + 'shooting_enemies': max(0, wave_number - 2), + 'enemy_speed': min(1 + wave_number * 0.1, 2.0), + 'enemy_health': 100 + wave_number * 10 + } + + def get_record_text(self) -> str: + if self.best_survival_time > 0: + minutes = self.best_survival_time // 60 + seconds = self.best_survival_time % 60 + return f"Record: {minutes:02d}:{seconds:02d}" + return "Pas encore de record" + +class SurvivalMode(GameMode): + def __init__(self): + super().__init__('survival', 'Mode Survie', 'Affrontez des vagues infinies d\'ennemis de plus en plus difficiles') + self.is_unlocked = False + self.best_time = 0 + + def get_wave_config(self, wave_number: int) -> dict: + return { + 'normal_enemies': wave_number * 3, + 'shooting_enemies': wave_number, + 'enemy_speed': min(1 + wave_number * 0.15, 2.5), + 'enemy_health': 120 + wave_number * 15 + } + + def get_record_text(self) -> str: + if self.best_time > 0: + minutes = self.best_time // 60 + seconds = self.best_time % 60 + return f"Record: {minutes:02d}:{seconds:02d}" + return "Pas encore de record" + +class BossRushMode(GameMode): + def __init__(self): + super().__init__('boss_rush', 'Mode Boss Rush', 'Affrontez uniquement les boss les plus puissants') + self.is_unlocked = False + self.best_bosses_killed = 0 + + def get_wave_config(self, wave_number: int) -> dict: + return { + 'boss': True, + 'boss_health': 1000 + wave_number * 500, + 'boss_speed': min(0.5 + wave_number * 0.1, 1.5), + 'minions': wave_number * 2 + } + + def get_record_text(self) -> str: + if self.best_bosses_killed > 0: + return f"Record: {self.best_bosses_killed} boss" + return "Pas encore de record" + +class GameModeManager: + def __init__(self): + self.modes: Dict[str, GameMode] = { + 'classic': ClassicMode(), + 'survival': SurvivalMode(), + 'boss_rush': BossRushMode() + } + self.selected_mode = 'classic' + + def get_mode_info(self, mode_id: str) -> dict: + """Retourne les informations d'un mode, y compris son record""" + mode = self.modes.get(mode_id) + if mode: + return { + 'id': mode.id, + 'name': mode.name, + 'description': mode.description, + 'is_unlocked': mode.is_unlocked, + 'record': mode.get_record_text() + } + return None + + def unlock_mode(self, mode_id: str) -> bool: + if mode_id in self.modes and not self.modes[mode_id].is_unlocked: + self.modes[mode_id].is_unlocked = True + return True + return False + + def select_mode(self, mode_id: str) -> bool: + if mode_id in self.modes and self.modes[mode_id].is_unlocked: + self.selected_mode = mode_id + return True + return False + + def get_selected_mode(self) -> GameMode: + return self.modes[self.selected_mode] + + def update_high_score(self, mode_id: str, score: int): + if mode_id in self.modes: + self.modes[mode_id].high_score = max(self.modes[mode_id].high_score, score) + + def save_state(self) -> dict: + return { + 'selected': self.selected_mode, + 'unlocked': [id for id, mode in self.modes.items() if mode.is_unlocked], + 'high_scores': {id: mode.high_score for id, mode in self.modes.items()}, + 'records': { + 'classic': self.modes['classic'].best_survival_time if hasattr(self.modes['classic'], 'best_survival_time') else 0, + 'survival': self.modes['survival'].best_time if hasattr(self.modes['survival'], 'best_time') else 0, + 'boss_rush': self.modes['boss_rush'].best_bosses_killed if hasattr(self.modes['boss_rush'], 'best_bosses_killed') else 0 + } + } + + def load_state(self, state: dict): + if 'selected' in state and state['selected'] in self.modes: + self.selected_mode = state['selected'] + + for mode_id in state.get('unlocked', []): + if mode_id in self.modes: + self.modes[mode_id].is_unlocked = True + + for mode_id, score in state.get('high_scores', {}).items(): + if mode_id in self.modes: + self.modes[mode_id].high_score = score + + # Charger les records spécifiques à chaque mode + records = state.get('records', {}) + if 'classic' in records and hasattr(self.modes['classic'], 'best_survival_time'): + self.modes['classic'].best_survival_time = records['classic'] + if 'survival' in records and hasattr(self.modes['survival'], 'best_time'): + self.modes['survival'].best_time = records['survival'] + if 'boss_rush' in records and hasattr(self.modes['boss_rush'], 'best_bosses_killed'): + self.modes['boss_rush'].best_bosses_killed = records['boss_rush'] \ No newline at end of file diff --git a/src/game/modes/survival_mode.py b/src/game/modes/survival_mode.py new file mode 100644 index 0000000..a9d1b98 --- /dev/null +++ b/src/game/modes/survival_mode.py @@ -0,0 +1,150 @@ +import pygame +import random +import time +from game.modes.game_mode_base import GameModeBase +from utils.constants import * +from game.core.enemy import Enemy, ShootingEnemy + +class SurvivalMode(GameModeBase): + def __init__(self, game): + super().__init__(game, + id='survival', + name='Mode Survie', + description='Affrontez des vagues infinies d\'ennemis de plus en plus difficiles') + self.wave = 1 + self.best_wave = 0 # Record du nombre de vagues survécues + self.wave_enemies_left = 15 + self.wave_transition = False + self.wave_start_time = 0 + + def initialize(self): + """Initialise le mode de jeu survie""" + self.wave = 1 + self.wave_enemies_left = 15 # Plus d'ennemis que le mode classique + self.wave_transition = False + self.wave_start_time = 0 + + def update(self): + """Met à jour la logique du mode survie""" + # Mise à jour de la transition de vague en priorité + if self.wave_transition: + current_time = pygame.time.get_ticks() + if current_time - self.wave_start_time > WAVE_TRANSITION_TIME: + self.wave_transition = False + self.wave_enemies_left = int(15 * (1.7 ** (self.wave - 1))) + print(f"Fin de la transition de vague {self.wave}, nouveaux ennemis: {self.wave_enemies_left}") + # Pas de spawn d'ennemis pendant la transition + elif not self.wave_transition: + self.spawn_enemies() + self.check_wave_completion() + + def check_wave_completion(self): + """Vérifie si la vague actuelle est terminée""" + # Vérifier que tous les ennemis prévus sont apparus ET qu'il n'y en a plus sur le terrain + if self.wave_enemies_left <= 0 and len(self.game.enemies) == 0: + if not self.wave_transition: + self.handle_wave_completion() + return True + return False + + def spawn_enemies(self): + """Logique de spawn des ennemis pour le mode survie""" + # Limiter le nombre d'ennemis actifs pour des performances optimales + if len(self.game.enemies) >= MAX_ACTIVE_ENEMIES: + return + + current_time = pygame.time.get_ticks() + if current_time - self.game.last_spawn >= self.game.spawn_cooldown and self.wave_enemies_left > 0: + # Utiliser la configuration de la vague + wave_config = self.get_wave_config(self.wave) + health = wave_config['enemy_health'] + speed = wave_config['enemy_speed'] + + # Calcul du ratio d'ennemis tireurs vs normaux + normal_enemies = wave_config['normal_enemies'] + shooting_enemies = wave_config['shooting_enemies'] + total_enemies = normal_enemies + shooting_enemies + shooter_ratio = shooting_enemies / total_enemies if total_enemies > 0 else 0 + + # Création de l'ennemi + if random.random() < shooter_ratio: + enemy = ShootingEnemy(health * 1.5, speed * 0.8) + else: + enemy = Enemy(health, speed) + + enemy.game = self.game + self.game.enemies.add(enemy) + self.wave_enemies_left -= 1 + self.game.last_spawn = current_time + + def handle_wave_completion(self): + """Gestion de la fin d'une vague""" + self.wave_transition = True + self.wave_start_time = pygame.time.get_ticks() + print(f"Début de la transition de vague {self.wave} à {self.wave_start_time}") # Log pour debug + self.wave += 1 + self.game.enemies_killed = 0 + + # Mise à jour du record si nécessaire + if self.wave > self.best_wave: + self.best_wave = self.wave + # Son de réussite pour le nouveau record + if hasattr(self.game, 'achievement_sound') and self.game.achievement_sound and not self.game.sound_muted: + self.game.achievement_sound.play() + + # Le nombre d'ennemis est mis à zéro et sera recalculé à la fin de la transition + self.wave_enemies_left = 0 + + # Spawn plus rapide + self.game.spawn_cooldown = max(300, self.game.spawn_cooldown - WAVE_SPEEDUP * 1.5) + + # Récompenses plus importantes + coins_earned = self.wave * COINS_PER_WAVE * 1.5 + self.game.economy_manager.add_coins(int(coins_earned)) + + # Notification + self.game.notification_manager.add_notification( + f'Vague {self.wave-1} terminée !', + f'+{int(coins_earned)} pièces', + COIN_ICON + ) + + # Son + if hasattr(self.game, 'coin_sound') and self.game.coin_sound and not self.game.sound_muted: + self.game.coin_sound.play() + + def handle_events(self, events): + """Gestion des événements spécifiques au mode survie""" + # Pas d'événements spécifiques pour le moment + pass + + def draw(self, screen): + """Affichage spécifique au mode survie""" + # Affichage du joueur + self.game.player.draw(screen) + + # Affichage des ennemis + for enemy in self.game.enemies: + enemy.draw(screen) + if isinstance(enemy, ShootingEnemy): + enemy.bullets.draw(screen) + + # Affichage de la transition de vague + if self.wave_transition: + self.draw_wave_transition(screen) + + def get_wave_config(self, wave_number: int) -> dict: + """Retourne la configuration de la vague pour le mode survie""" + return { + 'normal_enemies': wave_number * 3, + 'shooting_enemies': wave_number, + 'enemy_speed': min(1 + wave_number * 0.15, 2.5), + 'enemy_health': 120 + wave_number * 15, + 'boss': False # Pas de boss en mode survie + } + + def get_record_text(self) -> str: + """Retourne le texte du record pour le mode survie""" + if self.best_wave > 0: + return f"Record: Vague {self.best_wave}" + return "Pas encore de record" \ No newline at end of file diff --git a/src/game/modules/__init__.py b/src/game/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/game/modules/module_manager.py b/src/game/modules/module_manager.py new file mode 100644 index 0000000..afffb69 --- /dev/null +++ b/src/game/modules/module_manager.py @@ -0,0 +1,248 @@ +import random +import json +import os + +class Module: + def __init__(self, id, name, description, max_level, cost): + """Initialise un module""" + self.id = id + self.name = name + self.description = description + self.max_level = max_level + self.cost = cost + self.level = 0 + self.is_unlocked = False + + def get_level_description(self): + """Retourne la description du niveau actuel""" + if not self.is_unlocked: + return f'Non débloqué - Coût: {self.cost} pièces' + return f'Niveau {self.level}/{self.max_level}' + + def get_effect_description(self): + """Retourne la description de l'effet selon le niveau""" + if not self.is_unlocked: + return 'Aucun effet' + + if self.id == 'rapid_fire': + return f'Vitesse de tir +{self.level * 20}%' + elif self.id == 'shield_boost': + return f'Cooldown du bouclier -{self.level * 15}%' + elif self.id == 'damage_boost': + return f'Dégâts +{self.level * 25}%' + elif self.id == 'speed_boost': + return f'Vitesse +{self.level * 15}%' + elif self.id == 'health_regen': + return f'Régénération de {self.level} PV/s' + elif self.id == 'multi_shot': + return f'{self.level + 1} projectiles' + + return 'Effet inconnu' + +class ModuleManager: + def __init__(self, game): + """Initialise le gestionnaire de modules""" + self.game = game + self.modules = {} # Modules permanents (achetés) + self.temp_modules = {} # Modules temporaires (coffres) + self.initialize_modules() + self.load_state() # Charger l'état des modules + + def initialize_modules(self): + """Initialise la liste des modules disponibles""" + module_data = { + 'rapid_fire': { + 'name': 'Module de tir rapide', + 'description': 'Augmente la vitesse de tir', + 'max_level': 3, + 'cost': 100 + }, + 'shield_boost': { + 'name': 'Module de bouclier amélioré', + 'description': 'Réduit le temps de recharge du bouclier', + 'max_level': 3, + 'cost': 150 + }, + 'damage_boost': { + 'name': 'Module de puissance', + 'description': 'Augmente les dégâts infligés', + 'max_level': 3, + 'cost': 200 + }, + 'speed_boost': { + 'name': 'Module de vitesse', + 'description': 'Augmente la vitesse de déplacement', + 'max_level': 3, + 'cost': 100 + }, + 'health_regen': { + 'name': 'Module de régénération', + 'description': 'Régénère lentement la santé', + 'max_level': 3, + 'cost': 250 + }, + 'multi_shot': { + 'name': 'Module multi-tir', + 'description': 'Tire plusieurs projectiles à la fois', + 'max_level': 3, + 'cost': 300 + } + } + + # Création des modules permanents et temporaires + for module_id, data in module_data.items(): + # Module permanent (pour l'achat) + self.modules[module_id] = Module( + module_id, + data['name'], + data['description'], + data['max_level'], + data['cost'] + ) + + # Module temporaire (pour les coffres) + self.temp_modules[module_id] = Module( + module_id, + data['name'], + data['description'], + data['max_level'], + 0 # Pas de coût pour les modules temporaires + ) + + def reset_temp_modules(self): + """Réinitialise tous les modules temporaires""" + for module in self.temp_modules.values(): + module.is_unlocked = False + module.level = 0 + + def get_random_temp_modules(self, count=3): + """Retourne une liste aléatoire de modules temporaires disponibles""" + available_modules = [ + module for module in self.temp_modules.values() + if module.level < module.max_level + ] + return random.sample(available_modules, min(count, len(available_modules))) + + def upgrade_temp_module(self, module_id): + """Améliore un module temporaire""" + if module_id not in self.temp_modules: + return False + + module = self.temp_modules[module_id] + if module.level >= module.max_level: + return False + + if not module.is_unlocked: + module.is_unlocked = True + module.level = 1 + else: + module.level += 1 + + # Son et notification + if hasattr(self.game, 'achievement_sound') and self.game.achievement_sound and not self.game.sound_muted: + self.game.achievement_sound.play() + + self.game.notification_manager.add_notification( + 'Module amélioré !', + f'{module.name} niveau {module.level}', + None + ) + return True + + def apply_module_effects(self, player): + """Applique les effets des modules au joueur""" + # Réinitialisation des stats + player.reset_stats() + + # Application des effets des modules permanents et temporaires + for modules in [self.modules, self.temp_modules]: + for module in modules.values(): + if not module.is_unlocked: + continue + + if module.id == 'rapid_fire': + player.shoot_cooldown = int(player.base_shoot_cooldown * (1 - module.level * 0.2)) + elif module.id == 'shield_boost': + player.shield_cooldown = int(player.base_shield_cooldown * (1 - module.level * 0.15)) + elif module.id == 'damage_boost': + player.damage_multiplier = 1 + module.level * 0.25 + elif module.id == 'speed_boost': + player.speed = player.base_speed * (1 + module.level * 0.15) + elif module.id == 'health_regen': + player.health_regen = module.level + elif module.id == 'multi_shot': + player.multi_shot = module.level + 1 + + def unlock_module(self, module_id): + """Débloque un module permanent""" + if module_id not in self.modules: + return False + + module = self.modules[module_id] + if module.is_unlocked: + return False + + module.is_unlocked = True + module.level = 1 + + # Sauvegarder l'état après modification + self.save_state() + + # Notification et son déjà gérés dans ModuleMenu.handle_click + return True + + def upgrade_module(self, module_id): + """Améliore un module permanent""" + if module_id not in self.modules: + return False + + module = self.modules[module_id] + if not module.is_unlocked or module.level >= module.max_level: + return False + + module.level += 1 + + # Sauvegarder l'état après modification + self.save_state() + + # Notification et son déjà gérés dans ModuleMenu.handle_click + return True + + def save_state(self): + """Sauvegarde l'état des modules""" + state = { + module_id: { + 'is_unlocked': module.is_unlocked, + 'level': module.level + } + for module_id, module in self.modules.items() + } + + try: + # Créer le dossier de sauvegarde s'il n'existe pas + save_dir = 'src/save' + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + with open(f'{save_dir}/modules.json', 'w') as f: + json.dump(state, f) + print('Modules sauvegardés avec succès') + except Exception as e: + print(f'Erreur lors de la sauvegarde des modules: {e}') + + def load_state(self): + """Charge l'état des modules""" + try: + with open('src/save/modules.json', 'r') as f: + state = json.load(f) + + for module_id, data in state.items(): + if module_id in self.modules: + module = self.modules[module_id] + module.is_unlocked = data.get('is_unlocked', False) + module.level = data.get('level', 0) + print('Modules chargés avec succès') + except FileNotFoundError: + print('Aucun fichier de sauvegarde de modules trouvé') + except Exception as e: + print(f'Erreur lors du chargement des modules: {e}') \ No newline at end of file diff --git a/src/game/modules/module_menu.py b/src/game/modules/module_menu.py new file mode 100644 index 0000000..b29725d --- /dev/null +++ b/src/game/modules/module_menu.py @@ -0,0 +1,237 @@ +import pygame +from utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT + +class ModuleMenu: + def __init__(self, game): + """Initialise le menu des modules""" + self.game = game + self.font = pygame.font.Font(None, 36) + self.small_font = pygame.font.Font(None, 24) + + # Dimensions pour l'affichage des modules + self.module_width = 200 + self.module_height = 150 + self.padding = 20 + self.modules_per_row = 3 + + # Couleurs + self.bg_color = (50, 50, 50) + self.text_color = (255, 255, 255) + self.locked_color = (100, 100, 100) + self.hover_color = (70, 70, 70) + self.selected_color = (0, 255, 0) + + # État + self.scroll_offset = 0 + self.selected_module = None + self.hovered_module = None + + def handle_event(self, event): + """Gère les événements du menu""" + if event.type == pygame.MOUSEBUTTONDOWN: + # Gestion du scroll + if event.button == 4: # Scroll vers le haut + self.scroll_offset = max(0, self.scroll_offset - 50) + elif event.button == 5: # Scroll vers le bas + self.scroll_offset = min( + self.get_max_scroll(), + self.scroll_offset + 50 + ) + # Clic sur un module + elif event.button == 1: # Clic gauche + if self.hovered_module: + if not self.hovered_module.is_unlocked: + self.game.module_manager.unlock_module(self.hovered_module.id) + else: + self.game.module_manager.upgrade_module(self.hovered_module.id) + + # Mise à jour du module survolé + mouse_pos = pygame.mouse.get_pos() + self.update_hovered_module(mouse_pos) + + def update_hovered_module(self, mouse_pos): + """Met à jour le module survolé""" + x, y = mouse_pos + + # Position de départ pour l'affichage des modules + start_x = (SCREEN_WIDTH - (self.module_width + self.padding) * self.modules_per_row + self.padding) // 2 + start_y = 150 - self.scroll_offset + + self.hovered_module = None + + # Parcours des modules + row = 0 + col = 0 + for module in self.game.module_manager.modules.values(): + # Calcul de la position du module + module_x = start_x + col * (self.module_width + self.padding) + module_y = start_y + row * (self.module_height + self.padding) + + # Vérification si la souris est sur le module + if (module_x <= x <= module_x + self.module_width and + module_y <= y <= module_y + self.module_height): + self.hovered_module = module + break + + # Passage à la ligne suivante si nécessaire + col += 1 + if col >= self.modules_per_row: + col = 0 + row += 1 + + def get_max_scroll(self): + """Retourne le scroll maximum possible""" + num_modules = len(self.game.module_manager.modules) + num_rows = (num_modules + self.modules_per_row - 1) // self.modules_per_row + total_height = num_rows * (self.module_height + self.padding) + visible_height = SCREEN_HEIGHT - 200 # Espace disponible pour les modules + return max(0, total_height - visible_height) + + def draw(self, screen): + """Dessine le menu des modules""" + # Fond semi-transparent + s = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + s.set_alpha(128) + s.fill((0, 0, 0)) + screen.blit(s, (0, 0)) + + # Titre + title = self.font.render('MODULES XENOTECH', True, self.text_color) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 50)) + screen.blit(title, title_rect) + + # Affichage des pièces + coins_text = self.font.render(f'Pièces: {self.game.economy_manager.coins}', True, (255, 255, 0)) + coins_rect = coins_text.get_rect(center=(SCREEN_WIDTH // 2, 100)) + screen.blit(coins_text, coins_rect) + + # Position de départ pour l'affichage des modules + start_x = (SCREEN_WIDTH - (self.module_width + self.padding) * self.modules_per_row + self.padding) // 2 + start_y = 150 - self.scroll_offset + + # Affichage des modules + row = 0 + col = 0 + for module in self.game.module_manager.modules.values(): + # Calcul de la position du module + x = start_x + col * (self.module_width + self.padding) + y = start_y + row * (self.module_height + self.padding) + + # Vérification si le module est visible + if y + self.module_height > 0 and y < SCREEN_HEIGHT: + # Couleur de fond selon l'état + if module == self.hovered_module: + color = self.hover_color + elif module == self.selected_module: + color = self.selected_color + else: + color = self.locked_color if not module.is_unlocked else self.bg_color + + # Dessin du fond + pygame.draw.rect(screen, color, (x, y, self.module_width, self.module_height)) + pygame.draw.rect(screen, self.text_color, (x, y, self.module_width, self.module_height), 2) + + # Nom du module + name = self.font.render(module.name, True, self.text_color) + name_rect = name.get_rect(center=(x + self.module_width // 2, y + 30)) + screen.blit(name, name_rect) + + # Description + desc_lines = self.wrap_text(module.description, self.module_width - 20) + for i, line in enumerate(desc_lines): + desc = self.small_font.render(line, True, self.text_color) + desc_rect = desc.get_rect(center=(x + self.module_width // 2, y + 60 + i * 20)) + screen.blit(desc, desc_rect) + + # Niveau et effet + level = self.small_font.render(module.get_level_description(), True, self.text_color) + level_rect = level.get_rect(center=(x + self.module_width // 2, y + 100)) + screen.blit(level, level_rect) + + effect = self.small_font.render(module.get_effect_description(), True, self.text_color) + effect_rect = effect.get_rect(center=(x + self.module_width // 2, y + 120)) + screen.blit(effect, effect_rect) + + # Passage à la ligne suivante si nécessaire + col += 1 + if col >= self.modules_per_row: + col = 0 + row += 1 + + def wrap_text(self, text, width): + """Découpe le texte en lignes selon la largeur donnée""" + words = text.split() + lines = [] + current_line = [] + current_width = 0 + + for word in words: + word_surface = self.small_font.render(word + ' ', True, self.text_color) + word_width = word_surface.get_width() + + if current_width + word_width > width: + lines.append(' '.join(current_line)) + current_line = [word] + current_width = word_width + else: + current_line.append(word) + current_width += word_width + + if current_line: + lines.append(' '.join(current_line)) + + return lines + + def handle_click(self, mouse_pos): + """Gère les clics dans le menu des modules""" + # Met à jour le module survolé + self.update_hovered_module(mouse_pos) + + # Si un module est survolé, on tente de l'acheter ou de l'améliorer + if self.hovered_module: + if not self.hovered_module.is_unlocked: + if self.game.economy_manager.coins >= self.hovered_module.cost: + self.game.economy_manager.remove_coins(self.hovered_module.cost) + self.game.module_manager.unlock_module(self.hovered_module.id) + # Son et notification + try: + if hasattr(self.game, 'coin_sound') and self.game.coin_sound and not self.game.sound_muted: + self.game.coin_sound.play() + except Exception as e: + print(f'Erreur lors de la lecture du son: {e}') + + self.game.notification_manager.add_notification( + 'Module débloqué !', + f'{self.hovered_module.name}', + None # Icône à ajouter plus tard + ) + else: + self.game.notification_manager.add_notification( + 'Pas assez de pièces !', + f'Il vous manque {self.hovered_module.cost - self.game.economy_manager.coins} pièces', + None # Icône à ajouter plus tard + ) + elif self.hovered_module.level < self.hovered_module.max_level: + upgrade_cost = self.hovered_module.cost * (self.hovered_module.level + 1) + if self.game.economy_manager.coins >= upgrade_cost: + self.game.economy_manager.remove_coins(upgrade_cost) + self.game.module_manager.upgrade_module(self.hovered_module.id) + # Son et notification + try: + if hasattr(self.game, 'coin_sound') and self.game.coin_sound and not self.game.sound_muted: + self.game.coin_sound.play() + except Exception as e: + print(f'Erreur lors de la lecture du son: {e}') + + self.game.notification_manager.add_notification( + 'Module amélioré !', + f'{self.hovered_module.name} niveau {self.hovered_module.level}', + None # Icône à ajouter plus tard + ) + else: + self.game.notification_manager.add_notification( + 'Pas assez de pièces !', + f'Il vous manque {upgrade_cost - self.game.economy_manager.coins} pièces', + None # Icône à ajouter plus tard + ) + return True \ No newline at end of file diff --git a/src/game/modules/module_selection_menu.py b/src/game/modules/module_selection_menu.py new file mode 100644 index 0000000..f6ced31 --- /dev/null +++ b/src/game/modules/module_selection_menu.py @@ -0,0 +1,157 @@ +import pygame +from utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT, PAUSED, PLAYING + +class ModuleSelectionMenu: + def __init__(self, game): + """Initialise le menu de sélection des modules""" + self.game = game + self.font = pygame.font.Font(None, 36) + self.small_font = pygame.font.Font(None, 24) + + # Dimensions pour l'affichage des modules + self.module_width = 200 + self.module_height = 150 + self.padding = 20 + + # Couleurs + self.bg_color = (50, 50, 50) + self.text_color = (255, 255, 255) + self.hover_color = (70, 70, 70) + self.selected_color = (0, 255, 0) + + # État + self.visible = False + self.modules = [] + self.hovered_module = None + + def show(self, modules): + """Affiche le menu avec les modules donnés""" + self.visible = True + self.modules = modules + self.hovered_module = None + # On ne met en pause que si le jeu est déjà en cours + if self.game.game_state == PLAYING: + self.game.game_state = PAUSED + + def hide(self): + """Cache le menu""" + self.visible = False + self.modules = [] + self.hovered_module = None + self.game.game_state = PLAYING # Reprend le jeu + + def handle_event(self, event): + """Gère les événements du menu""" + if not self.visible: + return + + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: # Clic gauche + if self.hovered_module: + # Amélioration du module temporaire sélectionné + self.game.module_manager.upgrade_temp_module(self.hovered_module.id) + self.hide() + self.game.game_state = PLAYING # Reprendre le jeu + self.game.show_module_menu = False + + # Mise à jour du module survolé + mouse_pos = pygame.mouse.get_pos() + self.update_hovered_module(mouse_pos) + + def update_hovered_module(self, mouse_pos): + """Met à jour le module survolé""" + x, y = mouse_pos + + # Position de départ pour l'affichage des modules + start_x = (SCREEN_WIDTH - (self.module_width + self.padding) * len(self.modules) + self.padding) // 2 + start_y = (SCREEN_HEIGHT - self.module_height) // 2 + + self.hovered_module = None + + # Parcours des modules + for i, module in enumerate(self.modules): + # Calcul de la position du module + module_x = start_x + i * (self.module_width + self.padding) + + # Vérification si la souris est sur le module + if (module_x <= x <= module_x + self.module_width and + start_y <= y <= start_y + self.module_height): + self.hovered_module = module + break + + def draw(self, screen): + """Dessine le menu de sélection""" + if not self.visible: + return + + # Fond semi-transparent + s = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + s.set_alpha(128) + s.fill((0, 0, 0)) + screen.blit(s, (0, 0)) + + # Titre + title = self.font.render('CHOISISSEZ UN MODULE', True, self.text_color) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 4)) + screen.blit(title, title_rect) + + # Position de départ pour l'affichage des modules + start_x = (SCREEN_WIDTH - (self.module_width + self.padding) * len(self.modules) + self.padding) // 2 + start_y = (SCREEN_HEIGHT - self.module_height) // 2 + + # Affichage des modules + for i, module in enumerate(self.modules): + x = start_x + i * (self.module_width + self.padding) + y = start_y + + # Couleur de fond selon l'état + color = self.hover_color if module == self.hovered_module else self.bg_color + + # Dessin du fond + pygame.draw.rect(screen, color, (x, y, self.module_width, self.module_height)) + pygame.draw.rect(screen, self.text_color, (x, y, self.module_width, self.module_height), 2) + + # Nom du module + name = self.font.render(module.name, True, self.text_color) + name_rect = name.get_rect(center=(x + self.module_width // 2, y + 30)) + screen.blit(name, name_rect) + + # Description + desc_lines = self.wrap_text(module.description, self.module_width - 20) + for j, line in enumerate(desc_lines): + desc = self.small_font.render(line, True, self.text_color) + desc_rect = desc.get_rect(center=(x + self.module_width // 2, y + 60 + j * 20)) + screen.blit(desc, desc_rect) + + # Niveau et effet + level = self.small_font.render(module.get_level_description(), True, self.text_color) + level_rect = level.get_rect(center=(x + self.module_width // 2, y + 100)) + screen.blit(level, level_rect) + + effect = self.small_font.render(module.get_effect_description(), True, self.text_color) + effect_rect = effect.get_rect(center=(x + self.module_width // 2, y + 120)) + screen.blit(effect, effect_rect) + + def wrap_text(self, text, width): + """Découpe le texte en lignes selon la largeur donnée""" + words = text.split() + lines = [] + current_line = [] + current_width = 0 + + for word in words: + word_surface = self.small_font.render(word + ' ', True, self.text_color) + word_width = word_surface.get_width() + + if current_width + word_width > width: + lines.append(' '.join(current_line)) + current_line = [word] + current_width = word_width + else: + current_line.append(word) + current_width += word_width + + if current_line: + lines.append(' '.join(current_line)) + + return lines \ No newline at end of file diff --git a/src/game/modules/modules.py b/src/game/modules/modules.py new file mode 100644 index 0000000..4e67598 --- /dev/null +++ b/src/game/modules/modules.py @@ -0,0 +1,179 @@ +import json +import random +from typing import Dict, List, Optional + +class Module: + def __init__(self, id: str, name: str, description: str, max_level: int, cost: int): + self.id = id + self.name = name + self.description = description + self.max_level = max_level + self.cost = cost + self.level = 0 + self.is_unlocked = False + + def get_level_description(self) -> str: + # Retourne la description du niveau actuel du module + if not self.is_unlocked: + return f'Coût: {self.cost} pièces' + return f'Niveau {self.level}/{self.max_level}' + + def get_effect_description(self) -> str: + # Retourne la description de l'effet du module au niveau actuel + if not self.is_unlocked: + return 'Module verrouillé' + + base_effects = { + 'rapid_fire': lambda level: f'Vitesse de tir +{level * 10}%', + 'shield_boost': lambda level: f'Temps de recharge du bouclier -{level * 10}%', + 'damage_boost': lambda level: f'Dégâts +{level * 15}%', + 'speed_boost': lambda level: f'Vitesse de déplacement +{level * 10}%', + 'health_regen': lambda level: f'Régénération de vie +{level} PV/s', + 'multi_shot': lambda level: f'+{level} projectile(s) supplémentaire(s)' + } + + if self.id in base_effects: + return base_effects[self.id](self.level) + return 'Effet inconnu' + +class ModuleManager: + def __init__(self, game): + self.game = game + self.modules: Dict[str, Module] = {} + self.initialize_modules() + self.load_state() + + def initialize_modules(self): + # Initialise les modules disponibles dans le jeu + modules_data = { + 'rapid_fire': { + 'name': 'Module de tir rapide', + 'description': 'Augmente la vitesse de tir', + 'max_level': 5, + 'cost': 100 + }, + 'shield_boost': { + 'name': 'Module de bouclier amélioré', + 'description': 'Réduit le temps de recharge du bouclier', + 'max_level': 3, + 'cost': 150 + }, + 'damage_boost': { + 'name': 'Module de puissance', + 'description': 'Augmente les dégâts infligés', + 'max_level': 4, + 'cost': 200 + }, + 'speed_boost': { + 'name': 'Module de vitesse', + 'description': 'Augmente la vitesse de déplacement', + 'max_level': 3, + 'cost': 120 + }, + 'health_regen': { + 'name': 'Module de régénération', + 'description': 'Régénère lentement la vie', + 'max_level': 3, + 'cost': 250 + }, + 'multi_shot': { + 'name': 'Module multi-tir', + 'description': 'Ajoute des projectiles supplémentaires', + 'max_level': 2, + 'cost': 300 + } + } + + for module_id, data in modules_data.items(): + self.modules[module_id] = Module( + module_id, + data['name'], + data['description'], + data['max_level'], + data['cost'] + ) + + def unlock_module(self, module_id: str) -> bool: + # Déverrouille un module (à appeler après vérification du coût) + if module_id not in self.modules: + return False + + module = self.modules[module_id] + if module.is_unlocked: + return False + + module.is_unlocked = True + module.level = 1 + self.save_state() + return True + + def upgrade_module(self, module_id: str) -> bool: + # Améliore un module déjà déverrouillé + if module_id not in self.modules: + return False + + module = self.modules[module_id] + if not module.is_unlocked or module.level >= module.max_level: + return False + + module.level += 1 + self.save_state() + return True + + def get_random_modules(self, count: int = 3) -> List[Module]: + # Retourne une liste aléatoire de modules déverrouillés + unlocked_modules = [m for m in self.modules.values() if m.is_unlocked] + if not unlocked_modules: + return [] + return random.sample(unlocked_modules, min(count, len(unlocked_modules))) + + def apply_module_effects(self, player) -> None: + """Applique les effets des modules au joueur""" + for module_id, module in self.modules.items(): + if not module.is_unlocked: + continue + + if module_id == 'rapid_fire': + player.shoot_cooldown *= (1 - module.level * 0.1) + elif module_id == 'shield_boost': + player.shield_cooldown *= (1 - module.level * 0.1) + elif module_id == 'damage_boost': + player.bullet_damage *= (1 + module.level * 0.15) + elif module_id == 'speed_boost': + player.speed *= (1 + module.level * 0.1) + elif module_id == 'health_regen': + player.health_regen = module.level + elif module_id == 'multi_shot': + player.extra_bullets = module.level + + def save_state(self) -> None: + # Sauvegarde l'état des modules + state = { + module_id: { + 'is_unlocked': module.is_unlocked, + 'level': module.level + } + for module_id, module in self.modules.items() + } + + try: + with open('src/save/modules.json', 'w') as f: + json.dump(state, f) + except Exception as e: + print(f'Erreur lors de la sauvegarde des modules: {e}') + + def load_state(self) -> None: + # Charge l'état des modules + try: + with open('src/save/modules.json', 'r') as f: + state = json.load(f) + + for module_id, data in state.items(): + if module_id in self.modules: + module = self.modules[module_id] + module.is_unlocked = data['is_unlocked'] + module.level = data['level'] + except FileNotFoundError: + pass # Pas de fichier de sauvegarde, on garde l'état par défaut + except Exception as e: + print(f'Erreur lors du chargement des modules: {e}') \ No newline at end of file diff --git a/src/game/systems/__init__.py b/src/game/systems/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/game/systems/economy.py b/src/game/systems/economy.py new file mode 100644 index 0000000..496a067 --- /dev/null +++ b/src/game/systems/economy.py @@ -0,0 +1,110 @@ +import json +import os +from typing import Dict, List + +class Achievement: + def __init__(self, name: str, description: str): + self.name = name + self.description = description + self.is_unlocked = False + self.progress = 0 + self.max_progress = 100 + + def unlock(self): + """Débloque l'achievement""" + self.is_unlocked = True + self.progress = self.max_progress + + def update_progress(self, progress: int) -> bool: + """Met à jour la progression""" + self.progress = min(self.max_progress, progress) + if self.progress >= self.max_progress: + self.is_unlocked = True + return True + return False + + def is_complete(self) -> bool: + """Vérifie si l'achievement est complété""" + return self.progress >= self.max_progress + +class EconomyManager: + def __init__(self, game): + """Initialise le gestionnaire d'économie""" + self.game = game + self.coins = 0 + self.achievements = { + 'first_kill': Achievement('Premier Sang', 'Tuez votre premier ennemi'), + 'survivor': Achievement('Survivant', 'Survivez pendant 5 minutes'), + 'rich': Achievement('Riche', 'Accumulez 1000 pièces'), + 'collector': Achievement('Collectionneur', 'Débloquez tous les personnages'), + 'master': Achievement('Maître', 'Atteignez le niveau 10') + } + self.save_file = 'save_data.json' + self.load_save() + + def add_coins(self, amount: int): + """Ajoute des pièces au joueur""" + self.coins += amount + self.check_achievement_progress('rich', self.coins) + self.save_game() + + def remove_coins(self, amount: int) -> bool: + """Retire des pièces au joueur""" + if self.coins >= amount: + self.coins -= amount + self.save_game() + return True + return False + + def unlock_achievement(self, achievement_id: str) -> bool: + """Débloque un achievement""" + if achievement_id in self.achievements: + achievement = self.achievements[achievement_id] + if not achievement.is_unlocked: + achievement.unlock() + # Notification et son + if hasattr(self.game, 'achievement_sound') and not self.game.sound_muted: + self.game.achievement_sound.play() + self.game.notification_manager.add_notification( + 'Achievement débloqué !', + achievement.name, + None # Icône à ajouter plus tard + ) + return True + return False + + def check_achievement_progress(self, achievement_id: str, value: int): + if achievement_id in self.achievements: + if self.achievements[achievement_id].update_progress(value): + # Notification à implémenter + pass + + def save_game(self): + save_data = { + 'coins': self.coins, + 'achievements': { + id: { + 'unlocked': achievement.is_unlocked, + 'progress': achievement.progress + } for id, achievement in self.achievements.items() + } + } + + with open(self.save_file, 'w') as f: + json.dump(save_data, f) + + def load_save(self): + if not os.path.exists(self.save_file): + return + + try: + with open(self.save_file, 'r') as f: + save_data = json.load(f) + self.coins = save_data.get('coins', 0) + + for id, data in save_data.get('achievements', {}).items(): + if id in self.achievements: + self.achievements[id].is_unlocked = data.get('unlocked', False) + self.achievements[id].progress = data.get('progress', 0) + except Exception as e: + print(f'Erreur lors du chargement de la sauvegarde: {e}') \ No newline at end of file diff --git a/src/game/ui/__init__.py b/src/game/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/game/ui/button.py b/src/game/ui/button.py new file mode 100644 index 0000000..419322a --- /dev/null +++ b/src/game/ui/button.py @@ -0,0 +1,78 @@ +import pygame + +class Button: + def __init__(self, text, x, y, width, height): + """Initialise un bouton""" + self.text = text + self.rect = pygame.Rect(x - width // 2, y - height // 2, width, height) + self.font = pygame.font.Font(None, 36) + self.is_hovered = False + + # Couleurs améliorées + self.normal_color = (50, 50, 80) # Bleu foncé + self.hover_color = (70, 70, 120) # Bleu plus clair au survol + self.text_color = (220, 220, 255) # Blanc légèrement bleuté + self.border_color = (100, 100, 200) # Bordure bleue + + # Animation + self.pulse = 0 + self.pulse_direction = 1 + + # Pour diagnostiquer les problèmes de détection + self.last_clicked = False + + def draw(self, screen): + """Dessine le bouton""" + # Mise à jour de l'état de survol + self.update_hover_state() + + # Animation de pulsation pour le bouton survolé + if self.is_hovered: + self.pulse += 0.1 * self.pulse_direction + if self.pulse > 1: + self.pulse = 1 + self.pulse_direction = -1 + elif self.pulse < 0: + self.pulse = 0 + self.pulse_direction = 1 + + # Couleur interpolée pour l'effet de pulsation + r = int(self.normal_color[0] + (self.hover_color[0] - self.normal_color[0]) * self.pulse) + g = int(self.normal_color[1] + (self.hover_color[1] - self.normal_color[1]) * self.pulse) + b = int(self.normal_color[2] + (self.hover_color[2] - self.normal_color[2]) * self.pulse) + color = (r, g, b) + else: + color = self.normal_color + self.pulse = 0 + + # Dessin du rectangle avec coins arrondis + pygame.draw.rect(screen, color, self.rect, border_radius=10) + pygame.draw.rect(screen, self.border_color, self.rect, 2, border_radius=10) # Bordure + + # Effet de lueur pour le bouton survolé + if self.is_hovered: + glow_rect = self.rect.inflate(4, 4) + pygame.draw.rect(screen, self.border_color, glow_rect, 1, border_radius=12) + + # Dessin du texte + text_surface = self.font.render(self.text, True, self.text_color) + text_rect = text_surface.get_rect(center=self.rect.center) + + # Léger décalage du texte quand le bouton est survolé + if self.is_hovered: + text_rect.y -= 2 + + screen.blit(text_surface, text_rect) + + def update_hover_state(self): + """Mise à jour de l'état de survol du bouton""" + mouse_pos = pygame.mouse.get_pos() + self.is_hovered = self.rect.collidepoint(mouse_pos) + + def is_clicked(self, pos): + """Vérifie si le bouton est cliqué""" + was_clicked = self.rect.collidepoint(pos) + if was_clicked: + print(f"Bouton '{self.text}' cliqué - Rect: {self.rect}, Pos: {pos}") + self.last_clicked = True + return was_clicked \ No newline at end of file diff --git a/src/game/ui/character_shop.py b/src/game/ui/character_shop.py new file mode 100644 index 0000000..83fd140 --- /dev/null +++ b/src/game/ui/character_shop.py @@ -0,0 +1,308 @@ +from typing import Dict, List +import pygame +from utils.constants import * +import os + +class Character: + def __init__(self, id, name, description, cost, stats): + """Initialise un personnage""" + self.id = id + self.name = name + self.description = description + self.cost = cost + self.stats = stats + self.is_unlocked = False + self.is_selected = False + self.rect = None # Initialisation du rectangle à None + + # Définir les chemins des sprites + self.sprite_paths = { + 'default': os.path.join(CHARACTERS_DIR, 'default.png'), + 'speed': os.path.join(CHARACTERS_DIR, 'speeder.png'), + 'tank': os.path.join(CHARACTERS_DIR, 'tank.png'), + 'glass': os.path.join(CHARACTERS_DIR, 'default.png') # Utiliser default pour glass + } + + try: + # Utiliser le chemin correct pour les sprites + if id in self.sprite_paths and os.path.exists(self.sprite_paths[id]): + self.sprite = pygame.image.load(self.sprite_paths[id]).convert_alpha() + else: + # Fallback sur le sprite par défaut + default_path = os.path.join(CHARACTERS_DIR, 'default.png') + if os.path.exists(default_path): + self.sprite = pygame.image.load(default_path).convert_alpha() + else: + # Créer un sprite de base si aucun n'est trouvé + self.sprite = pygame.Surface((PLAYER_SIZE * 3, PLAYER_SIZE * 3), pygame.SRCALPHA) + pygame.draw.circle(self.sprite, PLAYER_COLOR, (PLAYER_SIZE * 1.5, PLAYER_SIZE * 1.5), PLAYER_SIZE) + print(f'Sprite par défaut créé pour {id}') + + # Redimensionner le sprite + self.sprite = pygame.transform.scale(self.sprite, (PLAYER_SIZE * 3, PLAYER_SIZE * 3)) + except Exception as e: + print(f'Erreur lors du chargement du sprite pour {id}: {e}') + self.sprite = pygame.Surface((PLAYER_SIZE * 3, PLAYER_SIZE * 3), pygame.SRCALPHA) + pygame.draw.circle(self.sprite, PLAYER_COLOR, (PLAYER_SIZE * 1.5, PLAYER_SIZE * 1.5), PLAYER_SIZE) + +class CharacterShop: + def __init__(self, game): + """Initialise la boutique de personnages""" + self.game = game + self.title_font = pygame.font.Font(None, 48) # Police plus grande pour le titre + self.font = pygame.font.Font(None, 28) # Police plus petite pour le nom + self.small_font = pygame.font.Font(None, 20) # Police encore plus petite pour les stats + + # Dimensions et positions + self.character_width = 250 # Augmentation de la largeur + self.character_height = 350 # Augmentation de la hauteur + self.padding = 30 + self.characters_per_row = 3 + + # Couleurs + self.bg_color = (30, 30, 50) # Bleu foncé + self.text_color = (220, 220, 255) # Blanc bleuté + self.locked_color = (50, 50, 70) # Gris foncé + self.hover_color = (40, 40, 80) # Bleu un peu plus clair + self.selected_color = (30, 70, 30) # Vert foncé + + # État + self.scroll_offset = 0 + self.selected_character = None + self.hovered_character = None + + # Initialisation des personnages + self.characters = { + 'default': Character( + 'default', + 'Vaisseau Standard', + 'Le vaisseau de base, équilibré et fiable', + 0, # Gratuit + {'health': 100, 'speed': 1.0, 'damage': 1.0} + ), + 'speed': Character( + 'speed', + 'Vaisseau Rapide', + 'Un vaisseau léger et agile', + 150, + {'health': 80, 'speed': 1.3, 'damage': 0.9} + ), + 'tank': Character( + 'tank', + 'Vaisseau Blindé', + 'Un vaisseau lent mais résistant', + 200, + {'health': 150, 'speed': 0.8, 'damage': 1.1} + ), + 'glass': Character( + 'glass', + 'Vaisseau Canon', + 'Fragile mais puissant', + 250, + {'health': 60, 'speed': 1.1, 'damage': 1.5} + ) + } + + # Le vaisseau par défaut est débloqué et sélectionné + self.characters['default'].is_unlocked = True + self.characters['default'].is_selected = True + self.selected_character = self.characters['default'] + + def handle_click(self, pos): + """Gère les clics dans le shop""" + # Vérification des clics sur les personnages + for character_id, character in self.characters.items(): + # Vérifier si le personnage a un rectangle et si le clic est dessus + if hasattr(character, 'rect') and character.rect and character.rect.collidepoint(pos): + print(f"Clic sur le personnage {character_id}") # Débogage + if not character.is_unlocked: + # Tentative d'achat du personnage + if self.game.economy_manager.coins >= character.cost: + self.game.economy_manager.remove_coins(character.cost) + character.is_unlocked = True + # Jouer le son si disponible + if hasattr(self.game, 'coin_sound') and self.game.coin_sound and not self.game.sound_muted: + self.game.coin_sound.play() + self.game.notification_manager.add_notification( + 'Nouveau vaisseau !', + f'{character.name} débloqué', + None + ) + # Sauvegarder l'état après l'achat + self.game.save_game_state() + else: + # Notification de manque de pièces + self.game.notification_manager.add_notification( + 'Pas assez de pièces !', + f'Il vous manque {character.cost - self.game.economy_manager.coins} pièces', + None + ) + else: + # Sélectionner le personnage + if self.select_character(character_id): + # Sauvegarder l'état après la sélection + self.game.save_game_state() + return True + return False + + def select_character(self, character_id): + """Sélectionne un personnage""" + if character_id in self.characters and self.characters[character_id].is_unlocked: + # Désélectionne le personnage actuel + if self.selected_character: + self.selected_character.is_selected = False + + # Sélectionne le nouveau personnage + self.characters[character_id].is_selected = True + self.selected_character = self.characters[character_id] + + # Son et notification + if hasattr(self.game, 'coin_sound') and not self.game.sound_muted: + self.game.coin_sound.play() + self.game.notification_manager.add_notification( + 'Personnage sélectionné !', + self.characters[character_id].name, + None # Icône à ajouter plus tard + ) + + return True + return False + + def get_selected_character(self): + """Retourne le personnage sélectionné""" + return self.selected_character + + def draw(self, screen): + """Dessine la boutique de personnages""" + # Titre + title = self.title_font.render("HANGAR SPATIAL", True, (100, 150, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 50)) + screen.blit(title, title_rect) + + # Affichage des pièces + coin_text = self.title_font.render(f"{self.game.economy_manager.coins}", True, GOLD_COLOR) + coin_rect = coin_text.get_rect(center=(SCREEN_WIDTH // 2, 100)) + pygame.draw.circle(screen, GOLD_COLOR, (coin_rect.left - 20, coin_rect.centery), 10) + screen.blit(coin_text, coin_rect) + + # Calcul de la disposition des cartes + num_characters = len(self.characters) + total_width = num_characters * (CHARACTER_BUTTON_WIDTH + CHARACTER_SPACING) - CHARACTER_SPACING + start_x = (SCREEN_WIDTH - total_width) // 2 + start_y = 150 + + # Affichage des personnages + for i, character in enumerate(self.characters.values()): + x = start_x + i * (CHARACTER_BUTTON_WIDTH + CHARACTER_SPACING) + y = start_y + + # Fond de la carte + card_color = (40, 40, 60) + if character.is_selected: + card_color = (40, 60, 40) + elif not character.is_unlocked: + card_color = (60, 40, 40) + + # Rectangle de la carte avec bordure + card_rect = pygame.Rect(x, y, CHARACTER_BUTTON_WIDTH, CHARACTER_BUTTON_HEIGHT) + pygame.draw.rect(screen, card_color, card_rect) + pygame.draw.rect(screen, (100, 100, 100), card_rect, 2) + character.rect = card_rect + + # Image du personnage + sprite_rect = character.sprite.get_rect(center=(x + CHARACTER_BUTTON_WIDTH // 2, y + CHARACTER_PREVIEW_SIZE // 2 + 20)) + screen.blit(character.sprite, sprite_rect) + + # Nom du personnage avec un fond semi-transparent + name_text = self.font.render(character.name, True, WHITE) + name_rect = name_text.get_rect(center=(x + CHARACTER_BUTTON_WIDTH // 2, y + CHARACTER_PREVIEW_SIZE + 25)) + # Fond semi-transparent pour le nom + name_bg = pygame.Surface((name_rect.width + 20, name_rect.height + 6)) + name_bg.fill((0, 0, 0)) + name_bg.set_alpha(128) + name_bg_rect = name_bg.get_rect(center=name_rect.center) + screen.blit(name_bg, name_bg_rect) + screen.blit(name_text, name_rect) + + # Stats + stats_y = y + CHARACTER_PREVIEW_SIZE + 50 + stats_spacing = 20 # Espacement réduit + stats_center_x = x + CHARACTER_BUTTON_WIDTH // 2 + + # Santé + health_text = self.small_font.render(f'VIE {character.stats["health"]}%', True, (255, 100, 100)) + health_rect = health_text.get_rect(center=(stats_center_x, stats_y)) + screen.blit(health_text, health_rect) + + # Vitesse + speed_text = self.small_font.render(f'VIT {character.stats["speed"]}x', True, (100, 255, 100)) + speed_rect = speed_text.get_rect(center=(stats_center_x, stats_y + stats_spacing)) + screen.blit(speed_text, speed_rect) + + # Dégâts + damage_text = self.small_font.render(f'ATQ {character.stats["damage"]}x', True, (100, 100, 255)) + damage_rect = damage_text.get_rect(center=(stats_center_x, stats_y + stats_spacing * 2)) + screen.blit(damage_text, damage_rect) + + # État/Prix avec fond semi-transparent + if character.is_selected: + status_text = self.small_font.render("SÉLECTIONNÉ", True, (100, 255, 100)) + status_rect = status_text.get_rect(center=(stats_center_x, stats_y + stats_spacing * 3)) + elif not character.is_unlocked: + cost_text = self.small_font.render(f'COÛT: {character.cost}', True, GOLD_COLOR) + cost_rect = cost_text.get_rect(center=(stats_center_x, stats_y + stats_spacing * 3)) + pygame.draw.circle(screen, GOLD_COLOR, (cost_rect.left - 15, cost_rect.centery), 6) + screen.blit(cost_text, cost_rect) + else: + status_text = self.small_font.render("DÉBLOQUÉ", True, WHITE) + status_rect = status_text.get_rect(center=(stats_center_x, stats_y + stats_spacing * 3)) + screen.blit(status_text, status_rect) + + # Instructions + instruction_text = self.small_font.render("Cliquez sur un vaisseau pour le sélectionner ou l'acheter", True, WHITE) + instruction_rect = instruction_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 30)) + screen.blit(instruction_text, instruction_rect) + + def wrap_text(self, text, width): + """Découpe le texte en lignes selon la largeur donnée""" + words = text.split() + lines = [] + current_line = [] + current_width = 0 + + for word in words: + word_surface = self.small_font.render(word + ' ', True, self.text_color) + word_width = word_surface.get_width() + + if current_width + word_width > width: + lines.append(' '.join(current_line)) + current_line = [word] + current_width = word_width + else: + current_line.append(word) + current_width += word_width + + if current_line: + lines.append(' '.join(current_line)) + + return lines + + def save_state(self): + """Sauvegarde l'état de la boutique""" + return { + 'characters': { + char_id: { + 'unlocked': char.is_unlocked, + 'selected': char.is_selected + } for char_id, char in self.characters.items() + } + } + + def load_state(self, state): + """Charge l'état de la boutique""" + if 'characters' in state: + for char_id, data in state['characters'].items(): + if char_id in self.characters: + self.characters[char_id].is_unlocked = data.get('unlocked', False) + if data.get('selected', False): + self.select_character(char_id) \ No newline at end of file diff --git a/src/game/ui/game_mode_menu.py b/src/game/ui/game_mode_menu.py new file mode 100644 index 0000000..c454546 --- /dev/null +++ b/src/game/ui/game_mode_menu.py @@ -0,0 +1,152 @@ +import pygame +from utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT, WHITE, CYAN, GOLD_COLOR + +class GameModeMenu: + def __init__(self, screen, game): + self.screen = screen + self.game = game + self.font = pygame.font.Font(None, 36) + self.title_font = pygame.font.Font(None, 48) + self.small_font = pygame.font.Font(None, 28) # Police plus petite pour les records + self.visible = False + + # Dimensions des boutons de mode + self.mode_width = 200 + self.mode_height = 150 + self.padding = 30 + + # Couleurs + self.bg_color = (40, 40, 60) # Bleu foncé + self.hover_color = (50, 50, 70) # Bleu plus clair + self.locked_color = (60, 40, 40) # Rouge foncé + + # État + self.hovered_mode = None + + def show(self): + """Affiche le menu""" + self.visible = True + self.hovered_mode = None + + def hide(self): + """Cache le menu""" + self.visible = False + self.hovered_mode = None + + def handle_event(self, event): + """Gère les événements du menu""" + if not self.visible: + return None + + if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + # Gestion du clic sur le bouton retour + back_rect = pygame.Rect(20, SCREEN_HEIGHT - 70, 120, 50) + if back_rect.collidepoint(event.pos): + return 'back' + + # Gestion du clic sur un mode + if self.hovered_mode: + return f'mode_{self.hovered_mode.id}' + + elif event.type == pygame.MOUSEMOTION: + mouse_pos = event.pos + + # Calcul de la disposition des modes + num_modes = len(self.game.game_modes) + total_width = num_modes * (self.mode_width + self.padding) - self.padding + start_x = (SCREEN_WIDTH - total_width) // 2 + start_y = (SCREEN_HEIGHT - self.mode_height) // 2 + + # Vérification de la collision avec chaque mode + self.hovered_mode = None + for i, mode in enumerate(self.game.game_modes.values()): + x = start_x + i * (self.mode_width + self.padding) + mode_rect = pygame.Rect(x, start_y, self.mode_width, self.mode_height) + if mode_rect.collidepoint(mouse_pos): + self.hovered_mode = mode + break + + return None + + def draw(self): + """Dessine le menu""" + if not self.visible: + return + + # Fond semi-transparent + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + overlay.fill((0, 0, 0)) + overlay.set_alpha(128) + self.screen.blit(overlay, (0, 0)) + + # Titre + title = self.title_font.render("SÉLECTION DU MODE", True, WHITE) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 100)) + self.screen.blit(title, title_rect) + + # Calcul de la disposition des modes + num_modes = len(self.game.game_modes) + total_width = num_modes * (self.mode_width + self.padding) - self.padding + start_x = (SCREEN_WIDTH - total_width) // 2 + start_y = (SCREEN_HEIGHT - self.mode_height) // 2 + + # Affichage des modes + for i, mode in enumerate(self.game.game_modes.values()): + x = start_x + i * (self.mode_width + self.padding) + mode_rect = pygame.Rect(x, start_y, self.mode_width, self.mode_height) + + # Couleur de fond selon l'état + color = self.hover_color if mode == self.hovered_mode else self.bg_color + pygame.draw.rect(self.screen, color, mode_rect) + pygame.draw.rect(self.screen, WHITE, mode_rect, 2) + + # Nom du mode + name = self.font.render(mode.name, True, WHITE) + name_rect = name.get_rect(center=(x + self.mode_width // 2, start_y + 30)) + self.screen.blit(name, name_rect) + + # Description (sur plusieurs lignes si nécessaire) + desc_lines = self.wrap_text(mode.description, self.mode_width - 20) + for j, line in enumerate(desc_lines[:2]): # Limite à 2 lignes + desc = self.font.render(line, True, CYAN) + desc_rect = desc.get_rect(center=(x + self.mode_width // 2, start_y + 70 + j * 25)) + self.screen.blit(desc, desc_rect) + + # Affichage du record (avec police plus petite) + record_text = mode.get_record_text() + if record_text: + record = self.small_font.render(record_text, True, GOLD_COLOR) + record_rect = record.get_rect(center=(x + self.mode_width // 2, start_y + self.mode_height - 30)) + self.screen.blit(record, record_rect) + + # Bouton retour + back_rect = pygame.Rect(20, SCREEN_HEIGHT - 70, 120, 50) + pygame.draw.rect(self.screen, self.bg_color, back_rect) + pygame.draw.rect(self.screen, WHITE, back_rect, 2) + back_text = self.font.render('Retour', True, WHITE) + back_rect = back_text.get_rect(center=(80, SCREEN_HEIGHT - 45)) + self.screen.blit(back_text, back_rect) + + def wrap_text(self, text, max_width): + """Découpe le texte en lignes selon la largeur donnée""" + words = text.split() + lines = [] + current_line = [] + current_width = 0 + + for word in words: + word_surface = self.font.render(word + ' ', True, WHITE) + word_width = word_surface.get_width() + + if current_width + word_width > max_width: + lines.append(' '.join(current_line)) + current_line = [word] + current_width = word_width + else: + current_line.append(word) + current_width += word_width + + if current_line: + lines.append(' '.join(current_line)) + + return lines \ No newline at end of file diff --git a/src/game/ui/menu_manager.py b/src/game/ui/menu_manager.py new file mode 100644 index 0000000..cec85be --- /dev/null +++ b/src/game/ui/menu_manager.py @@ -0,0 +1,238 @@ +from typing import Dict +import pygame +from game.ui.menus import MainMenu, ShopMenu, AchievementsMenu, GameModeMenu, PauseMenu, GameOverMenu +from game.ui.button import Button +from utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT, MENU, PLAYING, PAUSED, GAME_OVER, SOUND_BUTTON_SIZE, SOUND_BUTTON_PADDING + +class MenuManager: + def __init__(self, game): + self.game = game + self.screen = game.screen + self.character_shop = game.character_shop + self.economy_manager = game.economy_manager + self.module_menu = game.module_menu # Nouveau + + # Polices + self.font = pygame.font.Font(None, 36) + self.big_font = pygame.font.Font(None, 72) + + # État du menu + self.current_menu = 'main' # main, pause, game_over, character_shop, modules + self.game_over_score = 0 + + # Position du bouton son + self.sound_button_rect = pygame.Rect( + SCREEN_WIDTH - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING - 10, + SCREEN_HEIGHT - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING - 10, + SOUND_BUTTON_SIZE, + SOUND_BUTTON_SIZE + ) + + # Boutons du menu principal + self.main_menu_buttons = [ + Button('Jouer', SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 20, 200, 50), + Button('Personnages', SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 80, 200, 50), + Button('Modules', SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 140, 200, 50), + Button('Quitter', SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 200, 200, 50) + ] + + # Boutons du menu pause + self.pause_menu_buttons = [ + Button('Reprendre', SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 60, 200, 50), + Button('Recommencer', SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2, 200, 50), + Button('Menu Principal', SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 60, 200, 50) + ] + + # Boutons du menu game over + self.game_over_buttons = [ + Button('Recommencer', SCREEN_WIDTH // 2, SCREEN_HEIGHT * 3 // 4, 200, 50), + Button('Menu Principal', SCREEN_WIDTH // 2, SCREEN_HEIGHT * 3 // 4 + 70, 200, 50) + ] + + # Boutons du menu character shop + self.character_shop_buttons = [ + Button('Retour', SCREEN_WIDTH // 2, SCREEN_HEIGHT - 100, 200, 50) + ] + + # Boutons du menu modules + self.module_menu_buttons = [ + Button('Retour', SCREEN_WIDTH // 2, SCREEN_HEIGHT - 100, 200, 50) + ] + + def handle_event(self, event): + """Gère les événements du menu""" + if event.type == pygame.MOUSEBUTTONDOWN: + mouse_pos = pygame.mouse.get_pos() + + # Gestion du bouton son + if self.is_sound_button_clicked(mouse_pos): + self.game.toggle_sound() + return None + + # Gestion des menus + if self.current_menu == 'main': + for button in self.main_menu_buttons: + if button.rect.collidepoint(mouse_pos): + if button.text == 'Jouer': + return 'start_game' + elif button.text == 'Quitter': + return 'quit' + elif button.text == 'Personnages': + self.show_menu('character_shop') + return None + elif button.text == 'Modules': + self.show_menu('modules') + return None + + elif self.current_menu == 'pause': + for button in self.pause_menu_buttons: + if button.rect.collidepoint(mouse_pos): + if button.text == 'Reprendre': + return 'resume_game' + elif button.text == 'Recommencer': + return 'restart_game' + elif button.text == 'Menu Principal': + self.show_menu('main') + return 'go_to_main_menu' + + elif self.current_menu == 'game_over': + for button in self.game_over_buttons: + if button.rect.collidepoint(mouse_pos): + if button.text == 'Recommencer': + return 'restart_game' + elif button.text == 'Menu Principal': + self.show_menu('main') + return 'go_to_main_menu' + + elif self.current_menu == 'character_shop': + # Gestion du bouton retour + for button in self.character_shop_buttons: + if button.rect.collidepoint(mouse_pos): + self.show_menu('main') + return None + + # Gestion des clics dans le shop + if hasattr(self.game, 'character_shop'): + self.game.character_shop.handle_click(mouse_pos) + + elif self.current_menu == 'modules': + # Gestion du bouton retour + for button in self.module_menu_buttons: + if button.rect.collidepoint(mouse_pos): + self.show_menu('main') + return None + + # Gestion des clics dans le menu des modules + if hasattr(self.game, 'module_menu'): + self.game.module_menu.handle_click(mouse_pos) + + return None + + def draw(self): + """Dessine le menu actuel""" + # Fond semi-transparent + s = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + s.set_alpha(128) + s.fill((0, 0, 0)) + self.screen.blit(s, (0, 0)) + + if self.current_menu == 'main': + # Titre avec effet de lueur + title_text = 'ALIEN WAVE' + + # Effet de lueur (ombre portée) + glow_font = pygame.font.Font(None, 74) + glow_surface = glow_font.render(title_text, True, (50, 50, 200)) + glow_rect = glow_surface.get_rect(center=(SCREEN_WIDTH // 2 + 2, SCREEN_HEIGHT // 5 + 2)) + self.screen.blit(glow_surface, glow_rect) + + # Texte principal + title = self.big_font.render(title_text, True, (255, 255, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 5)) + self.screen.blit(title, title_rect) + + # Affichage des pièces avec icône + coin_icon_size = 24 + coin_icon = pygame.Surface((coin_icon_size, coin_icon_size), pygame.SRCALPHA) + pygame.draw.circle(coin_icon, (255, 215, 0), (coin_icon_size//2, coin_icon_size//2), coin_icon_size//2) + pygame.draw.circle(coin_icon, (255, 255, 150), (coin_icon_size//2, coin_icon_size//2), coin_icon_size//2 - 2) + + coins_text = self.font.render(f' {self.economy_manager.coins}', True, (255, 255, 0)) + coins_rect = coins_text.get_rect(midleft=(SCREEN_WIDTH // 2 - 10, SCREEN_HEIGHT // 5 + 50)) + coin_icon_rect = coin_icon.get_rect(midright=(coins_rect.left, coins_rect.centery)) + + self.screen.blit(coin_icon, coin_icon_rect) + self.screen.blit(coins_text, coins_rect) + + # Boutons + for button in self.main_menu_buttons: + button.draw(self.screen) + + elif self.current_menu == 'pause': + # Titre + title = self.big_font.render('PAUSE', True, (255, 255, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 4)) + self.screen.blit(title, title_rect) + + # Boutons + for button in self.pause_menu_buttons: + button.draw(self.screen) + + elif self.current_menu == 'game_over': + # Titre + title = self.big_font.render('GAME OVER', True, (255, 255, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 4)) + self.screen.blit(title, title_rect) + + # Score + score_text = self.font.render(f'Score: {self.game_over_score}', True, (255, 255, 255)) + score_rect = score_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 30)) + self.screen.blit(score_text, score_rect) + + # Meilleur score + highscore_text = self.font.render(f'Meilleur score: {self.game.highscore}', True, (255, 255, 255)) + highscore_rect = highscore_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 10)) + self.screen.blit(highscore_text, highscore_rect) + + # Boutons + for button in self.game_over_buttons: + button.draw(self.screen) + + elif self.current_menu == 'character_shop': + # Affichage de la boutique + self.character_shop.draw(self.screen) + + # Bouton retour avec effet spécial + for button in self.character_shop_buttons: + # Ajouter un effet de lueur autour du bouton + glow_rect = button.rect.inflate(8, 8) + pygame.draw.rect(self.screen, (100, 100, 200, 150), glow_rect, 2, border_radius=12) + button.draw(self.screen) + + elif self.current_menu == 'modules': # Nouveau + # Affichage du menu des modules + self.module_menu.draw(self.screen) + + # Bouton retour + for button in self.module_menu_buttons: + button.draw(self.screen) + + def show_menu(self, menu_type): + """Affiche un menu spécifique""" + self.current_menu = menu_type + + def hide(self): + """Cache le menu""" + self.current_menu = None + + def is_menu_active(self): + """Vérifie si un menu est actif""" + return self.current_menu is not None + + def set_game_over_score(self, score): + """Définit le score pour l'écran de game over""" + self.game_over_score = score + + def is_sound_button_clicked(self, mouse_pos): + """Vérifie si le bouton son a été cliqué""" + return self.sound_button_rect.collidepoint(mouse_pos) \ No newline at end of file diff --git a/src/game/ui/menus.py b/src/game/ui/menus.py new file mode 100644 index 0000000..212e672 --- /dev/null +++ b/src/game/ui/menus.py @@ -0,0 +1,365 @@ +import pygame +from typing import List, Dict, Callable +from utils.constants import * + +class Button: + def __init__(self, x: int, y: int, width: int, height: int, text: str, + color: tuple = (100, 100, 100), hover_color: tuple = (150, 150, 150)): + self.rect = pygame.Rect(x, y, width, height) + self.text = text + self.color = color + self.hover_color = hover_color + self.is_hovered = False + + def draw(self, screen: pygame.Surface): + color = self.hover_color if self.is_hovered else self.color + pygame.draw.rect(screen, color, self.rect, border_radius=10) + pygame.draw.rect(screen, (255, 255, 255), self.rect, 2, border_radius=10) + + font = pygame.font.Font(None, 36) + text_surface = font.render(self.text, True, (255, 255, 255)) + text_rect = text_surface.get_rect(center=self.rect.center) + screen.blit(text_surface, text_rect) + + def handle_event(self, event: pygame.event.Event) -> bool: + if event.type == pygame.MOUSEMOTION: + self.is_hovered = self.rect.collidepoint(event.pos) + elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + return self.rect.collidepoint(event.pos) + return False + +class Menu: + def __init__(self, screen: pygame.Surface): + self.screen = screen + self.buttons: Dict[str, Button] = {} + self.active = False + + def show(self): + self.active = True + + def hide(self): + self.active = False + + def handle_event(self, event: pygame.event.Event) -> str: + """Retourne l'action à effectuer si un bouton est cliqué""" + if not self.active: + return '' + + for action, button in self.buttons.items(): + if button.handle_event(event): + return action + return '' + + def draw(self): + if not self.active: + return + + # Fond semi-transparent + overlay = pygame.Surface(self.screen.get_size(), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 180)) + self.screen.blit(overlay, (0, 0)) + + for button in self.buttons.values(): + button.draw(self.screen) + +class MainMenu(Menu): + def __init__(self, screen: pygame.Surface): + super().__init__(screen) + width, height = screen.get_size() + button_width = 200 + button_height = 50 + spacing = 20 + start_y = height // 2 - (button_height + spacing) * 2 + + self.buttons = { + 'play': Button(width//2 - button_width//2, start_y, + button_width, button_height, 'Jouer'), + 'shop': Button(width//2 - button_width//2, start_y + button_height + spacing, + button_width, button_height, 'Boutique'), + 'achievements': Button(width//2 - button_width//2, start_y + (button_height + spacing) * 2, + button_width, button_height, 'Succès'), + 'quit': Button(width//2 - button_width//2, start_y + (button_height + spacing) * 3, + button_width, button_height, 'Quitter') + } + + def draw(self): + if not self.active: + return + + super().draw() + + # Titre + font = pygame.font.Font(None, 72) + title = font.render('Survival Shooter', True, (255, 255, 255)) + title_rect = title.get_rect(centerx=self.screen.get_width()//2, y=100) + self.screen.blit(title, title_rect) + +class ShopMenu(Menu): + def __init__(self, screen: pygame.Surface, character_shop): + super().__init__(screen) + self.character_shop = character_shop + self.selected_preview = None + width, height = screen.get_size() + + # Bouton retour + self.buttons['back'] = Button(20, height - 70, 120, 50, 'Retour') + + # Création des boutons pour chaque personnage + char_button_width = 150 + char_button_height = 200 + spacing = 30 + total_width = len(character_shop.characters) * char_button_width + (len(character_shop.characters) - 1) * spacing + start_x = (width - total_width) // 2 + + for i, (char_id, character) in enumerate(character_shop.characters.items()): + x = start_x + i * (char_button_width + spacing) + self.buttons[f'char_{char_id}'] = Button(x, height//2 - char_button_height//2, + char_button_width, char_button_height, character.name) + + def draw(self): + if not self.active: + return + + super().draw() + + # Titre + font = pygame.font.Font(None, 72) + title = font.render('Boutique', True, (255, 255, 255)) + title_rect = title.get_rect(centerx=self.screen.get_width()//2, y=50) + self.screen.blit(title, title_rect) + + # Affichage des pièces + coin_font = pygame.font.Font(None, 36) + coin_text = coin_font.render(f'Pièces: {self.character_shop.economy_manager.coins}', True, (255, 215, 0)) + self.screen.blit(coin_text, (20, 20)) + + # Affichage des personnages + for char_id, character in self.character_shop.characters.items(): + button = self.buttons[f'char_{char_id}'] + + # Sprite du personnage + sprite_rect = character.sprite.get_rect(centerx=button.rect.centerx, + centery=button.rect.centery - 30) + self.screen.blit(character.sprite, sprite_rect) + + # Prix ou statut + if character.is_unlocked: + status_text = 'Débloqué' + if char_id == self.character_shop.selected_character: + status_text = 'Sélectionné' + status_color = (0, 255, 0) + else: + status_text = f'{character.price} pièces' + status_color = (255, 215, 0) + + status_font = pygame.font.Font(None, 24) + status_surface = status_font.render(status_text, True, status_color) + status_rect = status_surface.get_rect(centerx=button.rect.centerx, + bottom=button.rect.bottom - 10) + self.screen.blit(status_surface, status_rect) + +class AchievementsMenu(Menu): + def __init__(self, screen: pygame.Surface, economy_manager): + super().__init__(screen) + self.economy_manager = economy_manager + width, height = screen.get_size() + + # Bouton retour + self.buttons['back'] = Button(20, height - 70, 120, 50, 'Retour') + + def draw(self): + if not self.active: + return + + super().draw() + + # Titre + font = pygame.font.Font(None, 72) + title = font.render('Succès', True, (255, 255, 255)) + title_rect = title.get_rect(centerx=self.screen.get_width()//2, y=50) + self.screen.blit(title, title_rect) + + # Affichage des achievements + achievement_height = 80 + spacing = 20 + start_y = 150 + + for achievement in self.economy_manager.achievements.values(): + # Fond de l'achievement + achievement_rect = pygame.Rect(100, start_y, self.screen.get_width() - 200, achievement_height) + color = (100, 100, 100) if achievement.is_unlocked else (50, 50, 50) + pygame.draw.rect(self.screen, color, achievement_rect, border_radius=10) + + # Icône + if achievement.icon_path: + try: + icon = pygame.image.load(achievement.icon_path) + icon = pygame.transform.scale(icon, (60, 60)) + self.screen.blit(icon, (achievement_rect.x + 10, achievement_rect.y + 10)) + except: + pygame.draw.rect(self.screen, (150, 150, 150), + (achievement_rect.x + 10, achievement_rect.y + 10, 60, 60)) + + # Texte + name_font = pygame.font.Font(None, 32) + desc_font = pygame.font.Font(None, 24) + + name_surface = name_font.render(achievement.name, True, (255, 255, 255)) + desc_surface = desc_font.render(achievement.description, True, (200, 200, 200)) + + self.screen.blit(name_surface, (achievement_rect.x + 80, achievement_rect.y + 10)) + self.screen.blit(desc_surface, (achievement_rect.x + 80, achievement_rect.y + 40)) + + # Barre de progression + if not achievement.is_unlocked: + progress_rect = pygame.Rect(achievement_rect.right - 110, + achievement_rect.centery - 10, 100, 20) + pygame.draw.rect(self.screen, (50, 50, 50), progress_rect, border_radius=5) + + progress_width = int(progress_rect.width * (achievement.progress / achievement.max_progress)) + if progress_width > 0: + pygame.draw.rect(self.screen, (0, 255, 0), + (progress_rect.x, progress_rect.y, progress_width, progress_rect.height), + border_radius=5) + + start_y += achievement_height + spacing + +class GameModeMenu(Menu): + def __init__(self, screen: pygame.Surface, game): + super().__init__(screen) + self.game = game + width, height = screen.get_size() + + # Bouton retour + self.buttons['back'] = Button(20, height - 70, 120, 50, 'Retour') + + # Création des boutons pour chaque mode + mode_button_width = 200 + mode_button_height = 150 + spacing = 30 + total_width = len(game.get_game_modes()) * mode_button_width + (len(game.get_game_modes()) - 1) * spacing + start_x = (width - total_width) // 2 + + for i, (mode_id, mode) in enumerate(game.get_game_modes().items()): + x = start_x + i * (mode_button_width + spacing) + self.buttons[f'mode_{mode_id}'] = Button(x, height//2 - mode_button_height//2, + mode_button_width, mode_button_height, mode.name) + + def draw(self): + if not self.active: + return + + super().draw() + + # Titre + font = pygame.font.Font(None, 72) + title = font.render('Modes de Jeu', True, (255, 255, 255)) + title_rect = title.get_rect(centerx=self.screen.get_width()//2, y=50) + self.screen.blit(title, title_rect) + + # Affichage des modes + for mode_id, mode in self.game.get_game_modes().items(): + button = self.buttons[f'mode_{mode_id}'] + + # Description + desc_font = pygame.font.Font(None, 24) + desc_lines = [mode.description[i:i+20] for i in range(0, len(mode.description), 20)] + for i, line in enumerate(desc_lines): + desc_surface = desc_font.render(line, True, (200, 200, 200)) + desc_rect = desc_surface.get_rect(centerx=button.rect.centerx, + top=button.rect.bottom + 10 + i*25) + self.screen.blit(desc_surface, desc_rect) + + # Statut + if hasattr(self.game.current_game_mode, 'id') and mode_id == self.game.current_game_mode.id: + status_text = 'Sélectionné' + status_color = (0, 255, 0) + else: + status_text = 'Disponible' + status_color = (200, 200, 200) + + status_font = pygame.font.Font(None, 24) + status_surface = status_font.render(status_text, True, status_color) + status_rect = status_surface.get_rect(centerx=button.rect.centerx, + bottom=button.rect.bottom - 10) + self.screen.blit(status_surface, status_rect) + + # High score + if mode.high_score > 0: + score_text = f'Meilleur score: {mode.high_score}' + score_surface = desc_font.render(score_text, True, (255, 215, 0)) + score_rect = score_surface.get_rect(centerx=button.rect.centerx, + top=button.rect.top - 30) + self.screen.blit(score_surface, score_rect) + +class PauseMenu(Menu): + def __init__(self, screen: pygame.Surface): + super().__init__(screen) + width, height = screen.get_size() + button_width = 200 + button_height = 50 + spacing = 20 + start_y = height // 2 - button_height + + self.buttons = { + 'resume': Button(width//2 - button_width//2, start_y, + button_width, button_height, 'Reprendre'), + 'quit': Button(width//2 - button_width//2, start_y + button_height + spacing, + button_width, button_height, 'Quitter') + } + + def draw(self): + if not self.active: + return + + super().draw() + + # Titre + font = pygame.font.Font(None, 72) + title = font.render('PAUSE', True, (255, 255, 255)) + title_rect = title.get_rect(centerx=self.screen.get_width()//2, y=100) + self.screen.blit(title, title_rect) + +class GameOverMenu(Menu): + def __init__(self, screen: pygame.Surface, game): + super().__init__(screen) + self.game = game + width, height = screen.get_size() + button_width = 200 + button_height = 50 + spacing = 20 + + self.buttons = { + 'restart': Button(width//2 - button_width//2, height * 3 // 4, + button_width, button_height, 'Recommencer'), + 'main_menu': Button(width//2 - button_width//2, height * 3 // 4 + button_height + spacing, + button_width, button_height, 'Menu Principal') + } + + self.score = 0 + + def set_score(self, score: int): + self.score = score + + def draw(self): + if not self.active: + return + + super().draw() + + # Titre + font = pygame.font.Font(None, 72) + title = font.render('GAME OVER', True, (255, 0, 0)) + title_rect = title.get_rect(centerx=self.screen.get_width()//2, y=100) + self.screen.blit(title, title_rect) + + # Score + score_font = pygame.font.Font(None, 48) + score_text = score_font.render(f'Score: {self.score}', True, (255, 255, 255)) + score_rect = score_text.get_rect(centerx=self.screen.get_width()//2, y=200) + self.screen.blit(score_text, score_rect) + + # High score + highscore_text = score_font.render(f'Meilleur score: {self.game.highscore}', True, (255, 215, 0)) + highscore_rect = highscore_text.get_rect(centerx=self.screen.get_width()//2, y=250) + self.screen.blit(highscore_text, highscore_rect) \ No newline at end of file diff --git a/src/game/ui/module_menu.py b/src/game/ui/module_menu.py new file mode 100644 index 0000000..fa98b78 --- /dev/null +++ b/src/game/ui/module_menu.py @@ -0,0 +1,227 @@ +import pygame +from typing import Optional, Tuple + +from utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT, WHITE, BLACK, GREEN, RED, YELLOW, GOLD_COLOR + +class ModuleMenu: + def __init__(self, game): + """Initialise le menu des modules""" + self.game = game + self.title_font = pygame.font.Font(None, 48) # Police plus grande pour le titre + self.font = pygame.font.Font(None, 28) # Police plus petite pour le nom + self.small_font = pygame.font.Font(None, 20) # Police encore plus petite pour les stats + + # Dimensions pour l'affichage des modules + self.module_width = 160 + self.module_height = 200 + self.padding = 20 + self.modules_per_row = 4 + + # Couleurs + self.bg_color = (40, 40, 60) # Bleu foncé + self.selected_color = (40, 60, 40) # Vert foncé + self.locked_color = (60, 40, 40) # Rouge foncé + self.hover_color = (50, 50, 70) # Bleu plus clair + + # État + self.scroll_offset = 0 + self.max_scroll = 0 + self.hovered_module = None + + def draw(self, screen): + """Dessine le menu des modules""" + # Titre + title = self.title_font.render("MODULES", True, (100, 150, 255)) + title_rect = title.get_rect(center=(SCREEN_WIDTH // 2, 50)) + screen.blit(title, title_rect) + + # Affichage des pièces + coin_text = self.title_font.render(f"{self.game.economy_manager.coins}", True, GOLD_COLOR) + coin_rect = coin_text.get_rect(center=(SCREEN_WIDTH // 2, 100)) + pygame.draw.circle(screen, GOLD_COLOR, (coin_rect.left - 20, coin_rect.centery), 10) + screen.blit(coin_text, coin_rect) + + # Calcul de la disposition des modules + num_modules = len(self.game.module_manager.modules) + total_width = min(self.modules_per_row, num_modules) * (self.module_width + self.padding) - self.padding + start_x = (SCREEN_WIDTH - total_width) // 2 + start_y = 150 - self.scroll_offset + + # Affichage des modules + row = 0 + col = 0 + for module in self.game.module_manager.modules.values(): + x = start_x + col * (self.module_width + self.padding) + y = start_y + row * (self.module_height + self.padding) + + # Fond de la carte + card_color = self.bg_color + if module == self.hovered_module: + card_color = self.hover_color + elif not module.is_unlocked: + card_color = self.locked_color + + # Rectangle de la carte avec bordure + card_rect = pygame.Rect(x, y, self.module_width, self.module_height) + pygame.draw.rect(screen, card_color, card_rect) + pygame.draw.rect(screen, (100, 100, 100), card_rect, 2) + + # Nom du module avec fond semi-transparent + name_text = self.font.render(module.name, True, WHITE) + name_rect = name_text.get_rect(center=(x + self.module_width // 2, y + 30)) + # Fond semi-transparent pour le nom + name_bg = pygame.Surface((name_rect.width + 20, name_rect.height + 6)) + name_bg.fill((0, 0, 0)) + name_bg.set_alpha(128) + name_bg_rect = name_bg.get_rect(center=name_rect.center) + screen.blit(name_bg, name_bg_rect) + screen.blit(name_text, name_rect) + + # Description + desc_lines = self.wrap_text(module.description, self.module_width - 20) + for i, line in enumerate(desc_lines[:2]): # Limite à 2 lignes + desc_text = self.small_font.render(line, True, WHITE) + desc_rect = desc_text.get_rect(center=(x + self.module_width // 2, y + 60 + i * 20)) + screen.blit(desc_text, desc_rect) + + # Stats + stats_y = y + 110 + stats_center_x = x + self.module_width // 2 + + # Niveau actuel + if module.is_unlocked: + level_text = self.small_font.render(f'NIV {module.level}/{module.max_level}', True, (100, 255, 100)) + else: + level_text = self.small_font.render('VERROUILLÉ', True, (255, 100, 100)) + level_rect = level_text.get_rect(center=(stats_center_x, stats_y)) + screen.blit(level_text, level_rect) + + # Effet actuel + if module.is_unlocked: + effect_text = self.small_font.render(module.get_effect_description(), True, (100, 100, 255)) + effect_rect = effect_text.get_rect(center=(stats_center_x, stats_y + 25)) + screen.blit(effect_text, effect_rect) + + # Prix/État + if not module.is_unlocked: + cost_text = self.small_font.render(f'COÛT: {module.cost}', True, GOLD_COLOR) + cost_rect = cost_text.get_rect(center=(stats_center_x, y + self.module_height - 30)) + pygame.draw.circle(screen, GOLD_COLOR, (cost_rect.left - 15, cost_rect.centery), 6) + screen.blit(cost_text, cost_rect) + elif module.level < module.max_level: + next_cost = module.cost * (module.level + 1) + upgrade_text = self.small_font.render(f'AMÉL: {next_cost}', True, GOLD_COLOR) + upgrade_rect = upgrade_text.get_rect(center=(stats_center_x, y + self.module_height - 30)) + pygame.draw.circle(screen, GOLD_COLOR, (upgrade_rect.left - 15, upgrade_rect.centery), 6) + screen.blit(upgrade_text, upgrade_rect) + else: + max_text = self.small_font.render('NIVEAU MAX', True, (100, 255, 100)) + max_rect = max_text.get_rect(center=(stats_center_x, y + self.module_height - 30)) + screen.blit(max_text, max_rect) + + # Passage à la colonne/ligne suivante + col += 1 + if col >= self.modules_per_row: + col = 0 + row += 1 + + # Instructions + instruction_text = self.small_font.render("Cliquez sur un module pour le débloquer ou l'améliorer", True, WHITE) + instruction_rect = instruction_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT - 30)) + screen.blit(instruction_text, instruction_rect) + + def wrap_text(self, text, max_width): + """Découpe le texte en lignes selon la largeur donnée""" + words = text.split() + lines = [] + current_line = [] + current_width = 0 + + for word in words: + word_surface = self.small_font.render(word + ' ', True, WHITE) + word_width = word_surface.get_width() + + if current_width + word_width > max_width: + lines.append(' '.join(current_line)) + current_line = [word] + current_width = word_width + else: + current_line.append(word) + current_width += word_width + + if current_line: + lines.append(' '.join(current_line)) + + return lines + + def handle_event(self, event): + """Gère les événements du menu""" + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button in (4, 5): # Molette de la souris + self.scroll_offset = max(0, min(self.max_scroll, + self.scroll_offset + (30 if event.button == 5 else -30))) + elif event.button == 1: # Clic gauche + self.handle_click(event.pos) + elif event.type == pygame.MOUSEMOTION: + self.update_hovered_module(event.pos) + + def update_hovered_module(self, pos): + """Met à jour le module survolé""" + mouse_x, mouse_y = pos + mouse_y += self.scroll_offset # Ajuster pour le scroll + + # Calcul de la disposition des modules + num_modules = len(self.game.module_manager.modules) + total_width = min(self.modules_per_row, num_modules) * (self.module_width + self.padding) - self.padding + start_x = (SCREEN_WIDTH - total_width) // 2 + start_y = 150 + + self.hovered_module = None + row = 0 + col = 0 + for module in self.game.module_manager.modules.values(): + x = start_x + col * (self.module_width + self.padding) + y = start_y + row * (self.module_height + self.padding) + + if (x <= mouse_x <= x + self.module_width and + y <= mouse_y <= y + self.module_height): + self.hovered_module = module + break + + col += 1 + if col >= self.modules_per_row: + col = 0 + row += 1 + + def handle_click(self, pos): + """Gère les clics dans le menu""" + if not self.hovered_module: + return + + if not self.hovered_module.is_unlocked: + # Tente de débloquer le module + if self.game.economy_manager.coins >= self.hovered_module.cost: + if self.game.module_manager.unlock_module(self.hovered_module.id): + self.game.economy_manager.remove_coins(self.hovered_module.cost) + if hasattr(self.game, 'coin_sound') and not self.game.sound_muted: + self.game.coin_sound.play() + else: + self.game.notification_manager.add_notification( + 'Pas assez de pièces !', + f'Il vous manque {self.hovered_module.cost - self.game.economy_manager.coins} pièces', + None + ) + elif self.hovered_module.level < self.hovered_module.max_level: + # Tente d'améliorer le module + next_cost = self.hovered_module.cost * (self.hovered_module.level + 1) + if self.game.economy_manager.coins >= next_cost: + if self.game.module_manager.upgrade_module(self.hovered_module.id): + self.game.economy_manager.remove_coins(next_cost) + if hasattr(self.game, 'coin_sound') and not self.game.sound_muted: + self.game.coin_sound.play() + else: + self.game.notification_manager.add_notification( + 'Pas assez de pièces !', + f'Il vous manque {next_cost - self.game.economy_manager.coins} pièces', + None + ) \ No newline at end of file diff --git a/src/game/ui/module_selection.py b/src/game/ui/module_selection.py new file mode 100644 index 0000000..7aa78d0 --- /dev/null +++ b/src/game/ui/module_selection.py @@ -0,0 +1,135 @@ +import pygame +from typing import List, Optional + +from utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT, WHITE, BLACK, GREEN, YELLOW +from game.modules import Module + +class ModuleSelectionMenu: + def __init__(self, game): + self.game = game + self.font = pygame.font.Font(None, 32) + self.small_font = pygame.font.Font(None, 24) + + # Dimensions pour l'affichage des modules + self.module_width = 200 + self.module_height = 120 + self.padding = 20 + + # Couleurs + self.bg_color = (0, 0, 0, 128) # Noir semi-transparent + self.module_bg = (30, 30, 30) + self.module_hover = (50, 50, 50) + + # État + self.is_visible = False + self.modules: List[Module] = [] + + def show(self, modules: List[Module]) -> None: + self.is_visible = True + self.modules = modules + self.game.state = 'PAUSED' + + def hide(self) -> None: + self.is_visible = False + self.game.state = 'PLAYING' + + def draw(self, surface: pygame.Surface) -> None: + if not self.is_visible: + return + + # Fond semi-transparent + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) + overlay.fill(self.bg_color) + surface.blit(overlay, (0, 0)) + + # Titre + title = self.font.render('Choisissez un module à améliorer', True, WHITE) + surface.blit(title, (SCREEN_WIDTH // 2 - title.get_width() // 2, 20)) + + # Calcul des positions des modules + total_width = len(self.modules) * (self.module_width + self.padding) - self.padding + start_x = (SCREEN_WIDTH - total_width) // 2 + start_y = (SCREEN_HEIGHT - self.module_height) // 2 + + # Affichage des modules + x = start_x + for module in self.modules: + # Rectangle du module + module_rect = pygame.Rect(x, start_y, self.module_width, self.module_height) + mouse_pos = pygame.mouse.get_pos() + is_hovered = module_rect.collidepoint(mouse_pos) + color = self.module_hover if is_hovered else self.module_bg + pygame.draw.rect(surface, color, module_rect) + pygame.draw.rect(surface, WHITE, module_rect, 2) + + # Nom du module + name = self.font.render(module.name, True, WHITE) + surface.blit(name, (x + 10, start_y + 10)) + + # Description + description_lines = self.wrap_text(module.description, self.module_width - 20, self.small_font) + for i, line in enumerate(description_lines): + text = self.small_font.render(line, True, WHITE) + surface.blit(text, (x + 10, start_y + 45 + i * 20)) + + # Niveau actuel + level_text = self.small_font.render(module.get_level_description(), True, GREEN) + surface.blit(level_text, (x + 10, start_y + self.module_height - 30)) + + # Effet actuel + effect_text = self.small_font.render(module.get_effect_description(), True, YELLOW) + surface.blit(effect_text, (x + 10, start_y + self.module_height - 50)) + + x += self.module_width + self.padding + + def handle_event(self, event: pygame.event.Event) -> None: + if not self.is_visible: + return + + if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + # Calcul des positions des modules + total_width = len(self.modules) * (self.module_width + self.padding) - self.padding + start_x = (SCREEN_WIDTH - total_width) // 2 + start_y = (SCREEN_HEIGHT - self.module_height) // 2 + + # Vérifier chaque module + x = start_x + for module in self.modules: + module_rect = pygame.Rect(x, start_y, self.module_width, self.module_height) + + if module_rect.collidepoint(event.pos): + if self.game.module_manager.upgrade_module(module.id): + self.game.notification_manager.add_notification( + f'Module {module.name} amélioré !', + GREEN + ) + self.hide() + break + + x += self.module_width + self.padding + + def wrap_text(self, text: str, max_width: int, font: pygame.font.Font) -> list[str]: + words = text.split() + lines = [] + current_line = [] + current_width = 0 + + for word in words: + word_surface = font.render(word + ' ', True, WHITE) + word_width = word_surface.get_width() + + if current_width + word_width > max_width: + if current_line: + lines.append(' '.join(current_line)) + current_line = [word] + current_width = word_width + else: + lines.append(word) + else: + current_line.append(word) + current_width += word_width + + if current_line: + lines.append(' '.join(current_line)) + + return lines[:2] # Limiter à 2 lignes pour éviter le débordement \ No newline at end of file diff --git a/src/game/ui/notification_manager.py b/src/game/ui/notification_manager.py new file mode 100644 index 0000000..4a72643 --- /dev/null +++ b/src/game/ui/notification_manager.py @@ -0,0 +1,143 @@ +import pygame +from typing import List, Tuple +import time +from utils.constants import SCREEN_WIDTH, SCREEN_HEIGHT + +class Notification: + def __init__(self, title: str, description: str, icon_path: str = None): + self.title = title + self.description = description + self.icon_path = icon_path + self.creation_time = time.time() + self.duration = 3.0 # Durée d'affichage en secondes + self.alpha = 255 # Pour l'effet de fade out + self.height = 80 + self.width = 300 + self.y_offset = 0 # Pour l'animation de slide + + # Chargement de l'icône si présente + self.icon = None + if icon_path: + try: + self.icon = pygame.image.load(icon_path).convert_alpha() + self.icon = pygame.transform.scale(self.icon, (50, 50)) + except Exception as e: + print(f'Erreur lors du chargement de l\'icône {icon_path}: {e}') + + def should_remove(self) -> bool: + return time.time() - self.creation_time > self.duration + + def update(self): + # Calcul de l'alpha pour le fade out + remaining_time = self.duration - (time.time() - self.creation_time) + if remaining_time < 0.5: # Fade out sur la dernière demi-seconde + self.alpha = int(255 * (remaining_time / 0.5)) + + # Animation de slide + target_y = 20 + self.y_offset + current_y = -self.height + (target_y + self.height) * min(1, (time.time() - self.creation_time) * 2) + self.y_offset = current_y + + def draw(self, screen: pygame.Surface): + # Création de la surface de notification avec transparence + notification_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA) + + # Fond semi-transparent + pygame.draw.rect(notification_surface, (0, 0, 0, min(180, self.alpha)), + (0, 0, self.width, self.height), border_radius=10) + + # Titre + font_title = pygame.font.Font(None, 32) + title_surface = font_title.render(self.title, True, (255, 255, 255)) + title_surface.set_alpha(self.alpha) + + # Description + font_desc = pygame.font.Font(None, 24) + desc_surface = font_desc.render(self.description, True, (200, 200, 200)) + desc_surface.set_alpha(self.alpha) + + # Position des éléments + icon_padding = 60 if self.icon else 10 + notification_surface.blit(title_surface, (icon_padding, 10)) + notification_surface.blit(desc_surface, (icon_padding, 40)) + + if self.icon: + icon_copy = self.icon.copy() + icon_copy.set_alpha(self.alpha) + notification_surface.blit(icon_copy, (5, 15)) + + # Affichage de la notification + screen.blit(notification_surface, (screen.get_width() - self.width - 20, self.y_offset)) + +class NotificationManager: + def __init__(self, game): + self.game = game + self.notifications: List[Notification] = [] + self.spacing = 10 # Espacement entre les notifications + self.font = pygame.font.Font(None, 36) + self.small_font = pygame.font.Font(None, 24) + + # Style des notifications + self.notification_width = 300 + self.notification_height = 80 + self.padding = 10 + self.spacing = 10 + + # Couleurs + self.bg_color = (50, 50, 50) + self.title_color = (255, 255, 255) + self.message_color = (200, 200, 200) + + def add_notification(self, title: str, description: str, icon_path: str = None): + notification = Notification(title, description, icon_path) + # Calcul du décalage vertical en fonction des notifications existantes + notification.y_offset = len(self.notifications) * (notification.height + self.spacing) + self.notifications.append(notification) + + def update(self): + # Mise à jour et suppression des notifications expirées + self.notifications = [n for n in self.notifications if not n.should_remove()] + + # Mise à jour des positions + for i, notification in enumerate(self.notifications): + notification.y_offset = i * (notification.height + self.spacing) + notification.update() + + def draw(self, screen: pygame.Surface): + # Position de départ (coin supérieur droit) + x = SCREEN_WIDTH - self.notification_width - self.padding + y = self.padding + + for notification in self.notifications: + # Calcul de l'opacité + alpha = notification.alpha + + # Surface semi-transparente pour le fond + bg = pygame.Surface((self.notification_width, self.notification_height)) + bg.fill(self.bg_color) + bg.set_alpha(int(alpha * 0.8)) # Fond légèrement transparent + screen.blit(bg, (x, y)) + + # Titre + title = self.font.render(notification.title, True, self.title_color) + title.set_alpha(alpha) + title_rect = title.get_rect(topleft=(x + self.padding, y + self.padding)) + screen.blit(title, title_rect) + + # Description + message = self.small_font.render(notification.description, True, self.message_color) + message.set_alpha(alpha) + message_rect = message.get_rect(topleft=(x + self.padding, y + title_rect.height + self.padding)) + screen.blit(message, message_rect) + + # Icône (si présente) + if notification.icon: + try: + icon = notification.icon.copy() + icon.set_alpha(alpha) + screen.blit(icon, (x + self.notification_width - 42, y + self.padding)) + except Exception as e: + print(f"Erreur lors du chargement de l'icône: {e}") + + # Mise à jour de la position pour la prochaine notification + y += self.notification_height + self.spacing \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..7c61329 --- /dev/null +++ b/src/main.py @@ -0,0 +1,42 @@ +from game.core.game import Game +from utils.sound_generator import SoundGenerator +from utils.constants import SOUND_DIR + +def check_assets(): + """Vérifie la présence des assets nécessaires""" + import os + from utils.constants import BASE_DIR + print("Vérification des assets:") + print(f"Dossier de travail actuel: {os.getcwd()}") + print(f"Le fichier existe: {os.path.exists(BASE_DIR)}") + print(f"Chemin complet: {BASE_DIR}") + +def check_sounds(): + """Vérifie la présence des fichiers sons""" + import os + from utils.constants import ( + SHOOT_SOUND, ENEMY_SHOOT_SOUND, HIT_SOUND, + ENEMY_DEATH_SOUND, PLAYER_HURT_SOUND, GAME_MUSIC + ) + print("Vérification des fichiers sons:") + sound_files = [ + SHOOT_SOUND, + ENEMY_SHOOT_SOUND, + HIT_SOUND, + ENEMY_DEATH_SOUND, + PLAYER_HURT_SOUND, + GAME_MUSIC + ] + for sound_file in sound_files: + print(f"Fichier {sound_file} existe: {os.path.exists(sound_file)}") + +if __name__ == '__main__': + # Vérifications initiales + check_assets() + check_sounds() + sound_generator = SoundGenerator() + sound_generator.generate_all_sounds(SOUND_DIR) + + # Lancement du jeu + game = Game() + game.run() \ No newline at end of file diff --git a/src/save/modules.json b/src/save/modules.json new file mode 100644 index 0000000..c885fd4 --- /dev/null +++ b/src/save/modules.json @@ -0,0 +1 @@ +{"rapid_fire": {"is_unlocked": true, "level": 3}, "shield_boost": {"is_unlocked": true, "level": 3}, "damage_boost": {"is_unlocked": true, "level": 3}, "speed_boost": {"is_unlocked": true, "level": 3}, "health_regen": {"is_unlocked": true, "level": 3}, "multi_shot": {"is_unlocked": true, "level": 3}} \ No newline at end of file diff --git a/src/save/modules_state.json b/src/save/modules_state.json new file mode 100644 index 0000000..2c17615 --- /dev/null +++ b/src/save/modules_state.json @@ -0,0 +1 @@ +{"rapid_fire": {"is_unlocked": false, "level": 0}, "shield_boost": {"is_unlocked": false, "level": 0}, "damage_boost": {"is_unlocked": false, "level": 0}, "speed_boost": {"is_unlocked": false, "level": 0}, "health_regen": {"is_unlocked": false, "level": 0}, "multi_shot": {"is_unlocked": true, "level": 1}} \ No newline at end of file diff --git a/survival-shooter/src/utils/__init__.py b/src/utils/__init__.py similarity index 100% rename from survival-shooter/src/utils/__init__.py rename to src/utils/__init__.py diff --git a/survival-shooter/src/utils/constants.py b/src/utils/constants.py similarity index 70% rename from survival-shooter/src/utils/constants.py rename to src/utils/constants.py index 006680e..d07113c 100644 --- a/survival-shooter/src/utils/constants.py +++ b/src/utils/constants.py @@ -16,6 +16,7 @@ BULLET_COLOR = (160, 50, 255) # Violet clair pour les projectiles HEALTH_COLOR = (220, 50, 50) # Rouge vif pour la santé XP_COLOR = (80, 220, 255) # Bleu néon pour l'XP +GOLD_COLOR = (255, 215, 0) # Couleur or pour les pièces # Player settings PLAYER_SPEED = 5 @@ -52,16 +53,19 @@ # Wave settings ENEMIES_PER_WAVE = 10 -WAVE_TRANSITION_TIME = 1000 -WAVE_SPEEDUP = 100 -WAVE_ENEMY_MULTIPLIER = 1.2 # Coefficient multiplicateur d'ennemis par vague +WAVE_ENEMY_MULTIPLIER = 1.2 +WAVE_SPEEDUP = 50 +WAVE_TRANSITION_TIME = 2000 # 2 secondes en millisecondes SHOOTER_ENEMY_CHANCE = 0.3 # 30% de chance d'avoir un ennemi qui tire ENEMY_BULLET_DAMAGE = 15 # Base paths -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Dossier src +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ASSETS_DIR = os.path.join(BASE_DIR, "assets") EFFECTS_DIR = os.path.join(ASSETS_DIR, "effects") +CHARACTERS_DIR = os.path.join(ASSETS_DIR, "characters") +ACHIEVEMENTS_DIR = os.path.join(ASSETS_DIR, "achievements") +ICONS_DIR = os.path.join(ASSETS_DIR, "icons") # Assets paths PLAYER_SPRITE = os.path.join(ASSETS_DIR,'sprites', "player.png") @@ -71,6 +75,20 @@ ENEMY_BULLET_SPRITE = os.path.join(ASSETS_DIR, "enemy_bullet.png") GAME_LOGO = os.path.join(ASSETS_DIR, "logo.ico") +# Character sprites +DEFAULT_CHARACTER_SPRITE = os.path.join(CHARACTERS_DIR, "default.png") +SPEEDER_CHARACTER_SPRITE = os.path.join(CHARACTERS_DIR, "speeder.png") +TANK_CHARACTER_SPRITE = os.path.join(CHARACTERS_DIR, "tank.png") + +# Achievement icons +FIRST_KILL_ICON = os.path.join(ACHIEVEMENTS_DIR, "first_kill.png") +RICH_ICON = os.path.join(ACHIEVEMENTS_DIR, "rich.png") +SURVIVOR_ICON = os.path.join(ACHIEVEMENTS_DIR, "survivor.png") + +# UI icons +COIN_ICON = os.path.join(ICONS_DIR, "coin.png") +SHIELD_ICON = os.path.join(ICONS_DIR, "shield.png") + # Sound paths SOUND_DIR = os.path.join(ASSETS_DIR, "sounds") SHOOT_SOUND = os.path.join(SOUND_DIR, "shoot.wav") @@ -79,6 +97,9 @@ ENEMY_DEATH_SOUND = os.path.join(SOUND_DIR, "enemy_death.wav") PLAYER_HURT_SOUND = os.path.join(SOUND_DIR, "player_hurt.wav") GAME_MUSIC = os.path.join(SOUND_DIR, "game_music.mp3") +COIN_SOUND = os.path.join(SOUND_DIR, "coin.wav") +ACHIEVEMENT_SOUND = os.path.join(SOUND_DIR, "achievement.wav") +PURCHASE_SOUND = os.path.join(SOUND_DIR, "purchase.wav") # Fireball settings FIREBALL_SPRITE = os.path.join(EFFECTS_DIR, "Part 3 Free.gif") @@ -138,9 +159,44 @@ # Score settings SCORE_FILE = os.path.join(BASE_DIR, "data", "highscore.json") +SAVE_FILE = os.path.join(BASE_DIR, "data", "game_save.json") # Vérification de l'existence du logo if not os.path.exists(GAME_LOGO): print(f"ATTENTION: Le fichier {GAME_LOGO} n'existe pas!") print(f"Chemin complet attendu: {os.path.abspath(GAME_LOGO)}") +# Achievements settings +ACHIEVEMENT_NOTIFICATION_DURATION = 3.0 +ACHIEVEMENT_NOTIFICATION_WIDTH = 300 +ACHIEVEMENT_NOTIFICATION_HEIGHT = 80 + +# Shop settings +CHARACTER_PREVIEW_SIZE = 100 +CHARACTER_BUTTON_WIDTH = 160 +CHARACTER_BUTTON_HEIGHT = 200 +CHARACTER_SPACING = 20 + +# Game modes settings +MODE_BUTTON_WIDTH = 200 +MODE_BUTTON_HEIGHT = 150 +MODE_SPACING = 30 + +# Rewards settings +COINS_PER_WAVE = 100 +COINS_PER_NORMAL_ENEMY = 10 +COINS_PER_SHOOTING_ENEMY = 15 +COINS_PER_BOSS = 500 + +# Création des dossiers s'ils n'existent pas +os.makedirs(CHARACTERS_DIR, exist_ok=True) +os.makedirs(ACHIEVEMENTS_DIR, exist_ok=True) +os.makedirs(ICONS_DIR, exist_ok=True) +os.makedirs(os.path.join(BASE_DIR, "data"), exist_ok=True) + +# Debug +DEBUG_MODE = True # Mettre à False en production + +# Limites de performance +MAX_ACTIVE_ENEMIES = 15 # Nombre maximum d'ennemis actifs à l'écran + diff --git a/survival-shooter/src/utils/sound_generator.py b/src/utils/sound_generator.py similarity index 67% rename from survival-shooter/src/utils/sound_generator.py rename to src/utils/sound_generator.py index 7b6b015..f663ccc 100644 --- a/survival-shooter/src/utils/sound_generator.py +++ b/src/utils/sound_generator.py @@ -7,7 +7,20 @@ class SoundGenerator: def __init__(self, sample_rate=44100): self.sample_rate = sample_rate + self.check_dependencies() + def check_dependencies(self): + """Vérifie si les dépendances nécessaires sont installées""" + try: + import numpy + import wave + print("Dépendances pour la génération de sons vérifiées avec succès.") + except ImportError as e: + print(f"ERREUR: Dépendance manquante pour la génération de sons: {e}") + print("Veuillez installer les dépendances avec: pip install numpy") + return False + return True + def _create_sound_buffer(self, data): # Amplification du son data = data * 1.5 # Augmentation de l'amplitude @@ -81,9 +94,29 @@ def save_sound(self, waveform, filename): except Exception as e: print(f"Erreur lors de la sauvegarde du son {filename}: {e}") + def generate_test_sound_wave(self): + """Génère un son de test simple""" + duration = 0.1 + t = np.linspace(0, duration, int(self.sample_rate * duration)) + frequency = 440 # La4 standard + waveform = np.sin(2 * np.pi * frequency * t) + waveform *= np.exp(-t * 10) # Ajout d'une enveloppe pour adoucir le son + return waveform + def generate_all_sounds(self, sound_dir): """Génère et sauvegarde tous les sons du jeu""" - os.makedirs(sound_dir, exist_ok=True) + print(f"Génération des sons dans le dossier: {sound_dir}") + + # Créer le dossier des sons s'il n'existe pas + try: + os.makedirs(sound_dir, exist_ok=True) + print(f"Dossier des sons créé/vérifié: {sound_dir}") + except Exception as e: + print(f"ERREUR lors de la création du dossier des sons: {e}") + return False + + # Son de test pour tous les sons manquants + test_sound = self.generate_test_sound_wave() # Génération des formes d'onde sounds = { @@ -91,11 +124,21 @@ def generate_all_sounds(self, sound_dir): "enemy_shoot.wav": self.generate_enemy_shoot_sound_wave(), "hit.wav": self.generate_hit_sound_wave(), "enemy_death.wav": self.generate_enemy_death_sound_wave(), - "player_hurt.wav": self.generate_player_hurt_sound_wave() + "player_hurt.wav": self.generate_player_hurt_sound_wave(), + "coin.wav": test_sound, + "achievement.wav": test_sound, + "purchase.wav": test_sound } # Sauvegarde des sons + success_count = 0 for filename, waveform in sounds.items(): - filepath = os.path.join(sound_dir, filename) - if not os.path.exists(filepath): - self.save_sound(waveform, filepath) \ No newline at end of file + full_path = os.path.join(sound_dir, filename) + try: + self.save_sound(waveform, full_path) + success_count += 1 + except Exception as e: + print(f"ERREUR lors de la sauvegarde du son {filename}: {e}") + + print(f"Génération des sons terminée: {success_count}/{len(sounds)} sons générés avec succès.") + return success_count == len(sounds) \ No newline at end of file diff --git a/survival-shooter/src/assets/sounds/enemy_death.wav b/survival-shooter/src/assets/sounds/enemy_death.wav deleted file mode 100644 index 0b6fbee..0000000 Binary files a/survival-shooter/src/assets/sounds/enemy_death.wav and /dev/null differ diff --git a/survival-shooter/src/assets/sounds/hit.wav b/survival-shooter/src/assets/sounds/hit.wav deleted file mode 100644 index c10b2ea..0000000 Binary files a/survival-shooter/src/assets/sounds/hit.wav and /dev/null differ diff --git a/survival-shooter/src/game/__pycache__/__init__.cpython-312.pyc b/survival-shooter/src/game/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 4a7cd62..0000000 Binary files a/survival-shooter/src/game/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/survival-shooter/src/game/__pycache__/enemy.cpython-312.pyc b/survival-shooter/src/game/__pycache__/enemy.cpython-312.pyc deleted file mode 100644 index 2c50444..0000000 Binary files a/survival-shooter/src/game/__pycache__/enemy.cpython-312.pyc and /dev/null differ diff --git a/survival-shooter/src/game/__pycache__/player.cpython-312.pyc b/survival-shooter/src/game/__pycache__/player.cpython-312.pyc deleted file mode 100644 index ea23ad8..0000000 Binary files a/survival-shooter/src/game/__pycache__/player.cpython-312.pyc and /dev/null differ diff --git a/survival-shooter/src/game/__pycache__/weapon.cpython-312.pyc b/survival-shooter/src/game/__pycache__/weapon.cpython-312.pyc deleted file mode 100644 index fa95123..0000000 Binary files a/survival-shooter/src/game/__pycache__/weapon.cpython-312.pyc and /dev/null differ diff --git a/survival-shooter/src/game/player.py b/survival-shooter/src/game/player.py deleted file mode 100644 index 7615e23..0000000 --- a/survival-shooter/src/game/player.py +++ /dev/null @@ -1,153 +0,0 @@ -import pygame -import math -from utils.constants import * -from game.weapon import Bullet - -class Player: - def __init__(self, x, y): - super().__init__() - self.x = x - self.y = y - self.health = 100 - self.score = 0 - - # Chargement et configuration du sprite du joueur - try: - self.original_image = pygame.image.load(PLAYER_SPRITE).convert_alpha() - # Augmentation de la taille à 96x96 (PLAYER_SIZE * 3) - self.original_image = pygame.transform.scale(self.original_image, (PLAYER_SIZE * 3, PLAYER_SIZE * 3)) - # Rotation initiale de 270 degrés + retournement du sprite - self.original_image = pygame.transform.rotate(self.original_image, 270) - # Retournement horizontal du sprite - self.original_image = pygame.transform.flip(self.original_image, True, False) - self.image = self.original_image - except Exception as e: - print(f"Erreur lors du chargement de l'image du joueur: {e}") - # Fallback au dessin par défaut avec la nouvelle taille - self.original_image = pygame.Surface((PLAYER_SIZE * 3, PLAYER_SIZE * 3), pygame.SRCALPHA) - center = PLAYER_SIZE * 1.5 - pygame.draw.circle(self.original_image, (*PLAYER_COLOR, 128), (center, center), PLAYER_SIZE * 1.5) - pygame.draw.circle(self.original_image, PLAYER_COLOR, (center, center), PLAYER_SIZE) - self.image = self.original_image - - self.rect = self.image.get_rect(center=(x, y)) - self.bullets = pygame.sprite.Group() - self.last_shot = 0 - self.trail = [] - self.angle = 0 # Pour suivre l'angle de rotation actuel - self.shield_active = False - self.shield_duration = 5000 # Durée du bouclier en millisecondes - self.shield_cooldown = 10000 # Temps de recharge du bouclier - self.last_shield_activation = 0 - - def update(self): - keys = pygame.key.get_pressed() - if keys[pygame.K_z] or keys[pygame.K_w]: # Haut - self.move(0, -PLAYER_SPEED) - if keys[pygame.K_s]: # Bas - self.move(0, PLAYER_SPEED) - if keys[pygame.K_q] or keys[pygame.K_a]: # Gauche - self.move(-PLAYER_SPEED, 0) - if keys[pygame.K_d]: # Droite - self.move(PLAYER_SPEED, 0) - - # Rotation en fonction de la position de la souris - mouse_x, mouse_y = pygame.mouse.get_pos() - dx = mouse_x - self.rect.centerx - dy = mouse_y - self.rect.centery - self.angle = math.degrees(math.atan2(-dy, dx)) - - # Rotation du sprite - self.image = pygame.transform.rotate(self.original_image, self.angle) - self.rect = self.image.get_rect(center=self.rect.center) - - # Gestion du tir avec le clic gauche - mouse_buttons = pygame.mouse.get_pressed() - if mouse_buttons[0]: # Clic gauche - self.shoot() - - # Mise à jour des balles - self.bullets.update() - - # Supprime les balles qui sortent de l'écran - for bullet in self.bullets: - if (bullet.rect.x < 0 or bullet.rect.x > SCREEN_WIDTH or - bullet.rect.y < 0 or bullet.rect.y > SCREEN_HEIGHT): - bullet.kill() - - if self.shield_active: - current_time = pygame.time.get_ticks() - if current_time - self.last_shield_activation >= self.shield_duration: - self.shield_active = False - - def move(self, dx, dy): - self.x += dx - self.y += dy - # Empêche le joueur de sortir de l'écran - self.x = max(0, min(self.x, SCREEN_WIDTH)) - self.y = max(0, min(self.y, SCREEN_HEIGHT)) - self.rect.center = (self.x, self.y) - - def play_sound(self, sound_type): - if hasattr(self, 'game'): - print(f"Game instance exists, sound_muted: {self.game.sound_muted}") - if not self.game.sound_muted: - if sound_type == 'shoot' and self.game.shoot_sound: - print("Playing shoot sound") - self.game.shoot_sound.play() - elif sound_type == 'hurt' and self.game.player_hurt_sound: - print("Playing hurt sound") - self.game.player_hurt_sound.play() - else: - print("No game instance found") - - def shoot(self): - current_time = pygame.time.get_ticks() - if current_time - self.last_shot >= SHOOT_COOLDOWN: - mouse_x, mouse_y = pygame.mouse.get_pos() - dx = mouse_x - self.x - dy = mouse_y - self.y - distance = math.sqrt(dx**2 + dy**2) - - if distance != 0: - direction = (dx/distance, dy/distance) - bullet = Bullet(self.x, self.y, direction) - self.bullets.add(bullet) - if hasattr(self, 'game') and self.game.shoot_sound and not self.game.sound_muted: - pygame.mixer.find_channel(True).play(self.game.shoot_sound) - self.last_shot = current_time - - def draw(self, screen): - # Dessin des projectiles - for bullet in self.bullets: - bullet.draw(screen) - - # Dessin du joueur (toujours visible) - screen.blit(self.image, self.rect) - - # Dessin du bouclier par-dessus le joueur - if self.shield_active: - # Effet de bouclier plus visible avec double cercle - pygame.draw.circle(screen, (0, 255, 255), self.rect.center, PLAYER_SIZE * 1.5, 3) - pygame.draw.circle(screen, (0, 200, 255), self.rect.center, PLAYER_SIZE * 1.2, 2) - - def take_damage(self, amount): - if not self.shield_active: # Ne prend des dégâts que si le bouclier n'est pas actif - self.health = max(0, self.health - amount) # Empêche la santé de devenir négative - self.play_sound('hurt') - if self.health <= 0: - self.die() - - def die(self): - # Logique pour la mort du joueur - if hasattr(self, 'game'): - self.game.game_over() - - def gain_score(self, points): - self.score += points - - def activate_shield(self): - current_time = pygame.time.get_ticks() - if not self.shield_active and (current_time - self.last_shield_activation >= self.shield_cooldown): - self.shield_active = True - self.last_shield_activation = current_time \ No newline at end of file diff --git a/survival-shooter/src/game/weapon.py b/survival-shooter/src/game/weapon.py deleted file mode 100644 index 9561091..0000000 --- a/survival-shooter/src/game/weapon.py +++ /dev/null @@ -1,61 +0,0 @@ -import pygame -import math -from utils.constants import * - -class Bullet(pygame.sprite.Sprite): - def __init__(self, x, y, direction, is_enemy=False): - super().__init__() - self.direction = direction - self.speed = BULLET_SPEED - self.is_enemy = is_enemy - self.colors = BULLET_COLORS["ENEMY"] if is_enemy else BULLET_COLORS["PLAYER"] - - # Pré-rendu de l'effet de projectile - self.image = pygame.Surface((BULLET_SURFACE_SIZE, BULLET_SURFACE_SIZE), pygame.SRCALPHA) - center = BULLET_SURFACE_SIZE // 2 - - # Effet de lueur optimisé - pygame.draw.circle(self.image, self.colors["GLOW"], (center, center), BULLET_SIZE * 2) - pygame.draw.circle(self.image, self.colors["CORE"], (center, center), BULLET_SIZE) - - self.rect = self.image.get_rect(center=(x, y)) - self.trail_positions = [] - self.frame_count = 0 - - def update(self): - self.rect.x += self.direction[0] * self.speed - self.rect.y += self.direction[1] * self.speed - - # Mise à jour de la traînée moins fréquente - self.frame_count = (self.frame_count + 1) % TRAIL_UPDATE_FREQUENCY - if self.frame_count == 0: - self.trail_positions.insert(0, self.rect.center) - if len(self.trail_positions) > MAX_TRAIL_LENGTH: - self.trail_positions.pop() - - def draw(self, screen): - # Dessin optimisé de la traînée - for i, pos in enumerate(self.trail_positions): - alpha = 255 * (1 - i/MAX_TRAIL_LENGTH) - size = BULLET_SIZE * (1 - i/MAX_TRAIL_LENGTH) - if size > 1: # Évite de dessiner des cercles trop petits - pygame.draw.circle(screen, (*self.colors["TRAIL"][:3], int(alpha)), - pos, int(size)) - - screen.blit(self.image, self.rect) - -class Weapon: - def __init__(self): - self.bullets = pygame.sprite.Group() - self.last_shot = 0 - self.cooldown = 250 # Milliseconds - - def shoot(self, pos, target): - current_time = pygame.time.get_ticks() - if current_time - self.last_shot >= self.cooldown: - direction = pygame.math.Vector2(target[0] - pos[0], target[1] - pos[1]) - if direction.length() > 0: - direction = direction.normalize() - bullet = Bullet(pos[0], pos[1], direction) - self.bullets.add(bullet) - self.last_shot = current_time \ No newline at end of file diff --git a/survival-shooter/src/main.py b/survival-shooter/src/main.py deleted file mode 100644 index 8823f83..0000000 --- a/survival-shooter/src/main.py +++ /dev/null @@ -1,616 +0,0 @@ -import pygame -import sys -import random -import os -import math -import json -from utils.constants import * -from game.player import Player -from game.enemy import Enemy, ShootingEnemy -from utils.sound_generator import SoundGenerator -from ui.menu_manager import MenuManager - -def check_assets(): - print("Vérification des assets:") - print(f"Dossier de travail actuel: {os.getcwd()}") - print(f"Le fichier existe: {os.path.exists(FIREBALL_SPRITE)}") - print(f"Chemin complet: {os.path.abspath(FIREBALL_SPRITE)}") - -def check_sounds(): - print("Vérification des fichiers sons:") - sound_files = [ - SHOOT_SOUND, - ENEMY_SHOOT_SOUND, - HIT_SOUND, - ENEMY_DEATH_SOUND, - PLAYER_HURT_SOUND, - GAME_MUSIC - ] - for sound_file in sound_files: - print(f"Fichier {sound_file} existe: {os.path.exists(sound_file)}") - -check_assets() -check_sounds() - -class Game: - def __init__(self): - # Initialisation de Pygame et du mixer avec plus de canaux - pygame.init() - pygame.mixer.quit() # Réinitialiser le mixer - pygame.mixer.init(44100, -16, 2, 1024) # Augmentation du buffer - pygame.mixer.set_num_channels(64) # Réduction du nombre de canaux - - # Configuration de l'écran - self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) - pygame.display.set_caption("ALIEN WAVE") - - # Chargement du logo avec gestion d'erreur - try: - self.icon = pygame.image.load(GAME_LOGO) - pygame.display.set_icon(self.icon) # Ajout de cette ligne pour définir l'icône - except Exception as e: - print(f"Erreur lors du chargement du logo: {e}") - - self.clock = pygame.time.Clock() - - # Initialisation des polices - self.font = pygame.font.Font(None, 36) - self.big_font = pygame.font.Font(None, 72) - - # État du jeu - self.game_state = MENU # État initial sur le menu - - self.wave_enemies_left = ENEMIES_PER_WAVE - self.enemies_killed = 0 - self.wave_transition = False - self.wave_start_time = 0 - - # Génération des sons s'ils n'existent pas - sound_gen = SoundGenerator() - sound_gen.generate_all_sounds(SOUND_DIR) - - # Chargement des sons - try: - self.shoot_sound = pygame.mixer.Sound(SHOOT_SOUND) - self.enemy_shoot_sound = pygame.mixer.Sound(ENEMY_SHOOT_SOUND) - self.hit_sound = pygame.mixer.Sound(HIT_SOUND) - self.enemy_death_sound = pygame.mixer.Sound(ENEMY_DEATH_SOUND) - self.player_hurt_sound = pygame.mixer.Sound(PLAYER_HURT_SOUND) - - # Configuration du volume et durée des sons - for sound in [self.shoot_sound, self.enemy_shoot_sound, - self.hit_sound, self.enemy_death_sound, - self.player_hurt_sound]: - sound.set_volume(0.1) - # Réduire la durée de rétention du canal - sound.fadeout(100) # Fade out après 100ms - - # Musique de fond - self.load_background_music() - self.play_background_music() - except Exception as e: - print(f"Erreur lors du chargement des sons: {e}") - self.shoot_sound = None - self.enemy_shoot_sound = None - self.hit_sound = None - self.enemy_death_sound = None - self.player_hurt_sound = None - - self.sound_muted = False - self.reset_game() - - # Configuration des étoiles - self.stars = [] - self.num_stars = 150 # Augmentation du nombre d'étoiles - self.star_speeds = [0.3, 0.5, 0.8] # Vitesses plus douces - self.generate_stars() - - # Après l'initialisation existante - self.highscore = self.load_highscore() - - self.menu_manager = MenuManager(self) - - def set_volume(self, volume): - """Configure le volume pour tous les sons""" - try: - if hasattr(self, 'shoot_sound') and self.shoot_sound: - self.shoot_sound.set_volume(volume) - print(f"Volume shoot_sound réglé à {volume}") - if hasattr(self, 'enemy_shoot_sound') and self.enemy_shoot_sound: - self.enemy_shoot_sound.set_volume(volume) - if hasattr(self, 'hit_sound') and self.hit_sound: - self.hit_sound.set_volume(volume) - if hasattr(self, 'enemy_death_sound') and self.enemy_death_sound: - self.enemy_death_sound.set_volume(volume) - if hasattr(self, 'player_hurt_sound') and self.player_hurt_sound: - self.player_hurt_sound.set_volume(volume) - except Exception as e: - print(f"Erreur lors du réglage du volume: {e}") - - def reset_game(self): - self.player = Player(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2) - self.player.game = self # Passer l'instance du jeu - self.enemies = pygame.sprite.Group() - self.wave = 1 - self.wave_enemies_left = ENEMIES_PER_WAVE - self.enemies_killed = 0 - self.wave_transition = False - self.wave_transition_start = 0 - self.last_spawn = 0 - self.spawn_cooldown = 2000 - - def spawn_enemy(self): - current_time = pygame.time.get_ticks() - if current_time - self.last_spawn >= self.spawn_cooldown and self.wave_enemies_left > 0: - health = 50 + (self.wave * 10) - speed = 2 + (self.wave * 0.5) - - if random.random() < SHOOTER_ENEMY_CHANCE: - enemy = ShootingEnemy(health * 1.5, speed * 0.8) - else: - enemy = Enemy(health, speed) - - enemy.game = self # Passer l'instance du jeu avec les sons - self.enemies.add(enemy) - self.wave_enemies_left -= 1 - self.last_spawn = current_time - - def update_wave(self): - # On change de vague uniquement si tous les ennemis prévus sont apparus ET qu'il n'y en a plus sur le terrain - if self.wave_enemies_left <= 0 and len(self.enemies) == 0: - if not self.wave_transition: - self.wave_transition = True - self.wave_start_time = pygame.time.get_ticks() - self.wave += 1 - self.enemies_killed = 0 - # Calcul du nouveau nombre d'ennemis avec le coefficient - self.wave_enemies_left = int(ENEMIES_PER_WAVE * (WAVE_ENEMY_MULTIPLIER ** (self.wave - 1))) - self.spawn_cooldown = max(500, self.spawn_cooldown - WAVE_SPEEDUP) - - if self.wave_transition: - if pygame.time.get_ticks() - self.wave_start_time > WAVE_TRANSITION_TIME: - self.wave_transition = False - - def update_enemies(self): - for enemy in self.enemies: - if isinstance(enemy, ShootingEnemy): - enemy.update((self.player.x, self.player.y)) - # Gestion des balles ennemies - for bullet in enemy.bullets: - if bullet.rect.colliderect(self.player.rect): - self.player.take_damage(ENEMY_BULLET_DAMAGE) - try: - self.player_hurt_sound.play() - except: - pass - bullet.kill() - enemy.bullets.update() - else: - enemy.move((self.player.x, self.player.y)) - - # Vérifie les collisions avec les balles - bullet_hits = pygame.sprite.spritecollide(enemy, self.player.bullets, True) - for bullet in bullet_hits: - if enemy.take_damage(BULLET_DAMAGE): - self.player.gain_score(ENEMY_SCORE) - self.enemies_killed += 1 # Compte les ennemis tués - - # Vérifie les collisions avec le joueur - if enemy.attack(self.player): - try: - self.player_hurt_sound.play() - except: - pass - if self.player.health <= 0: - self.game_over() - - def draw_game_over(self): - # Fond semi-transparent - overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) - overlay.fill(BG_COLOR) - overlay.set_alpha(128) - self.screen.blit(overlay, (0, 0)) - - # Texte "GAME OVER" - game_over_text = self.big_font.render("GAME OVER", True, RED) - game_over_rect = game_over_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 3)) - self.screen.blit(game_over_text, game_over_rect) - - # Score final - score_text = self.font.render(f"Score Final: {self.player.score}", True, WHITE) - score_rect = score_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) - self.screen.blit(score_text, score_rect) - - # Affichage du meilleur score - highscore_text = self.font.render(f"Meilleur Score: {self.highscore}", True, CYAN) - highscore_rect = highscore_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 40)) - self.screen.blit(highscore_text, highscore_rect) - - # Bouton Restart - restart_text = self.font.render("Appuyez sur ESPACE pour recommencer", True, CYAN) - restart_rect = restart_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT * 2 // 3)) - self.screen.blit(restart_text, restart_rect) - - # Bouton Quitter - quit_text = self.font.render("Appuyez sur ÉCHAP pour quitter", True, WHITE) - quit_rect = quit_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT * 3 // 4)) - self.screen.blit(quit_text, quit_rect) - - def game_over(self): - self.game_state = GAME_OVER - self.update_highscore() - - def handle_game_over_input(self): - for event in pygame.event.get(): - if event.type == pygame.KEYDOWN: - if event.key == pygame.K_SPACE: - self.reset_game() - self.game_state = PLAYING # Important : définir l'état à PLAYING - elif event.key == pygame.K_ESCAPE: - pygame.quit() - sys.exit() - - def draw_hud(self): - # Affichage de la santé à gauche - health_text = self.font.render(f"Vie: {self.player.health}", True, WHITE) - health_rect = health_text.get_rect(topleft=(20, 20)) - self.screen.blit(health_text, health_rect) - - # Barre de vie - health_bar_width = 200 - health_bar_height = 20 - health_ratio = max(0, self.player.health / 100) - pygame.draw.rect(self.screen, RED, (20, 50, health_bar_width, health_bar_height), 2) - pygame.draw.rect(self.screen, RED, (20, 50, health_bar_width * health_ratio, health_bar_height)) - - # Affichage du score actuel en haut à droite - score_text = self.font.render(f"Score: {self.player.score}", True, WHITE) - score_rect = score_text.get_rect(topright=(SCREEN_WIDTH - 20, 20)) - self.screen.blit(score_text, score_rect) - - # Affichage de la vague au centre - wave_text = self.font.render(f"Vague {self.wave}", True, CYAN) - wave_rect = wave_text.get_rect(midtop=(SCREEN_WIDTH // 2, 20)) - self.screen.blit(wave_text, wave_rect) - - # Affichage du meilleur score sous la vague - highscore_text = self.font.render(f"Meilleur Score : {self.highscore}", True, CYAN) - highscore_rect = highscore_text.get_rect(midtop=(SCREEN_WIDTH // 2, 60)) - self.screen.blit(highscore_text, highscore_rect) - - # Informations sur les ennemis à droite (réorganisées) - y_offset = 60 # Commence plus haut pour combler l'espace - spacing = 35 # Espacement réduit entre les lignes - - # Nombre d'ennemis actifs - enemies_text = self.font.render(f"Ennemis: {len(self.enemies)}", True, WHITE) - enemies_rect = enemies_text.get_rect(topright=(SCREEN_WIDTH - 20, y_offset)) - self.screen.blit(enemies_text, enemies_rect) - - # Ennemis restants à faire apparaître - remaining_text = self.font.render(f"Restants: {self.wave_enemies_left}", True, WHITE) - remaining_rect = remaining_text.get_rect(topright=(SCREEN_WIDTH - 20, y_offset + spacing)) - self.screen.blit(remaining_text, remaining_rect) - - # Bouton son en bas à droite - button_x = SCREEN_WIDTH - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING - button_y = SCREEN_HEIGHT - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING - - # Dessiner le cercle du bouton - color = SOUND_ON_COLOR if not self.sound_muted else SOUND_OFF_COLOR - pygame.draw.circle(self.screen, color, (button_x + SOUND_BUTTON_SIZE//2, button_y + SOUND_BUTTON_SIZE//2), SOUND_BUTTON_SIZE//2) - - # Dessiner l'icône - if not self.sound_muted: - # Dessiner des ondes sonores - for i in range(3): - radius = (i + 1) * 5 - pygame.draw.arc(self.screen, WHITE, - (button_x + SOUND_BUTTON_SIZE//2 - radius, - button_y + SOUND_BUTTON_SIZE//2 - radius, - radius * 2, radius * 2), - -math.pi/4, math.pi/4, 2) - else: - # Dessiner une croix - start_x = button_x + SOUND_BUTTON_SIZE//4 - start_y = button_y + SOUND_BUTTON_SIZE//4 - end_x = button_x + SOUND_BUTTON_SIZE*3//4 - end_y = button_y + SOUND_BUTTON_SIZE*3//4 - pygame.draw.line(self.screen, WHITE, (start_x, start_y), (end_x, end_y), 2) - pygame.draw.line(self.screen, WHITE, (start_x, end_y), (end_x, start_y), 2) - - # Barre de cooldown du bouclier - shield_cooldown_width = 200 - shield_cooldown_height = 20 - current_time = pygame.time.get_ticks() - if self.player.shield_active: - shield_ratio = 1 - (current_time - self.player.last_shield_activation) / self.player.shield_duration - else: - shield_ratio = min(1, (current_time - self.player.last_shield_activation) / self.player.shield_cooldown) - - pygame.draw.rect(self.screen, RED, (20, 80, shield_cooldown_width, shield_cooldown_height), 2) - pygame.draw.rect(self.screen, CYAN, (20, 80, shield_cooldown_width * shield_ratio, shield_cooldown_height)) - - # Texte du cooldown du bouclier - shield_text = self.font.render("Bouclier", True, WHITE) - shield_rect = shield_text.get_rect(topleft=(20, 110)) - self.screen.blit(shield_text, shield_rect) - - def update_effects(self): - # Mise à jour des effets visuels - if TRAIL_EFFECT: - self.player.trail.append((self.player.rect.center, 255)) - # Faire disparaître progressivement la traînée - self.player.trail = [(pos, alpha-10) for pos, alpha in self.player.trail if alpha > 0] - - def draw_wave_transition(self): - if self.wave_transition: - overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) - overlay.fill(BG_COLOR) - overlay.set_alpha(128) - self.screen.blit(overlay, (0, 0)) - - wave_text = self.big_font.render(f"VAGUE {self.wave}", True, CYAN) - wave_rect = wave_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) - self.screen.blit(wave_text, wave_rect) - - def toggle_sound(self): - self.sound_muted = not self.sound_muted - - # Gestion des effets sonores - volume = 0 if self.sound_muted else 0.3 - for sound in [self.shoot_sound, self.enemy_shoot_sound, - self.hit_sound, self.enemy_death_sound, - self.player_hurt_sound]: - if sound: - sound.set_volume(volume) - - # Gestion de la musique de fond - if self.sound_muted: - pygame.mixer.music.pause() - else: - pygame.mixer.music.unpause() - if not pygame.mixer.music.get_busy(): - self.play_background_music() - - def play_sound(self, sound_type): - if not self.sound_muted: - try: - sound = None - if sound_type == 'shoot': - sound = self.shoot_sound - elif sound_type == 'enemy_shoot': - sound = self.enemy_shoot_sound - elif sound_type == 'hit': - sound = self.hit_sound - elif sound_type == 'enemy_death': - sound = self.enemy_death_sound - elif sound_type == 'player_hurt': - sound = self.player_hurt_sound - - if sound: - channel = pygame.mixer.find_channel(True) - if channel: - channel.set_volume(0.3) - channel.play(sound, maxtime=1000) # Limite la durée à 1 seconde - print(f"Son joué: {sound_type}") - except Exception as e: - print(f"Erreur lors de la lecture du son {sound_type}: {e}") - - def generate_stars(self): - """Génère les étoiles initiales""" - for _ in range(self.num_stars): - x = random.randint(0, SCREEN_WIDTH) - y = random.randint(0, SCREEN_HEIGHT) - speed = random.choice(self.star_speeds) - brightness = random.randint(100, 255) - size = random.randint(1, 3) - angle = random.uniform(0, 2 * math.pi) # Angle aléatoire pour chaque étoile - self.stars.append({ - 'pos': [x, y], - 'speed': speed, - 'brightness': brightness, - 'size': size, - 'angle': angle # Nouvelle propriété pour la direction - }) - - def update_stars(self): - """Met à jour la position des étoiles avec un mouvement constant""" - for star in self.stars: - # Mouvement constant vers le bas avec une légère dérive - star['pos'][1] += star['speed'] - star['pos'][0] += math.sin(star['angle']) * 0.3 - - # Fait réapparaître les étoiles en haut quand elles sortent de l'écran - if star['pos'][1] > SCREEN_HEIGHT: - star['pos'][1] = 0 - star['pos'][0] = random.randint(0, SCREEN_WIDTH) - star['brightness'] = random.randint(100, 255) # Nouvelle luminosité - - # Garde les étoiles dans les limites horizontales - if star['pos'][0] < 0: - star['pos'][0] = SCREEN_WIDTH - elif star['pos'][0] > SCREEN_WIDTH: - star['pos'][0] = 0 - - def draw_stars(self): - """Dessine les étoiles""" - for star in self.stars: - color = (star['brightness'], star['brightness'], star['brightness']) - pygame.draw.circle(self.screen, color, - (int(star['pos'][0]), int(star['pos'][1])), - star['size']) - - def load_background_music(self): - """Charge la musique de fond""" - try: - pygame.mixer.music.load(GAME_MUSIC) - pygame.mixer.music.set_volume(0.1) - except Exception as e: - print(f"Erreur lors du chargement de la musique de fond: {e}") - - def play_background_music(self): - """Joue la musique de fond en boucle""" - try: - pygame.mixer.music.play(-1) # -1 pour jouer en boucle - except Exception as e: - print(f"Erreur lors de la lecture de la musique de fond: {e}") - - def pause_background_music(self): - """Met en pause la musique de fond""" - pygame.mixer.music.pause() - - def resume_background_music(self): - """Reprend la musique de fond""" - pygame.mixer.music.unpause() - - def stop_background_music(self): - """Arrête la musique de fond""" - pygame.mixer.music.stop() - - def set_background_music_volume(self, volume): - """Règle le volume de la musique de fond""" - pygame.mixer.music.set_volume(volume) - - def get_background_music_volume(self): - """Retourne le volume courant de la musique de fond""" - return pygame.mixer.music.get_volume() - - def load_highscore(self): - try: - if not os.path.exists(os.path.dirname(SCORE_FILE)): - os.makedirs(os.path.dirname(SCORE_FILE)) - if os.path.exists(SCORE_FILE): - with open(SCORE_FILE, 'r') as f: - data = json.load(f) - return data.get('highscore', 0) - return 0 - except Exception as e: - print(f"Erreur lors du chargement du meilleur score: {e}") - return 0 - - def save_highscore(self): - try: - with open(SCORE_FILE, 'w') as f: - json.dump({'highscore': self.highscore}, f) - except Exception as e: - print(f"Erreur lors de la sauvegarde du meilleur score: {e}") - - def update_highscore(self): - if self.player.score > self.highscore: - self.highscore = self.player.score - self.save_highscore() - - def draw_menu(self): - # Fond sombre semi-transparent - overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) - overlay.fill((0, 0, 0, 180)) - self.screen.blit(overlay, (0, 0)) - - # Titre du jeu - title_text = self.big_font.render("SHOOTER SURVIVAL", True, CYAN) - title_rect = title_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 3)) - self.screen.blit(title_text, title_rect) - - # Instructions - start_text = self.font.render("Appuyez sur ESPACE pour commencer", True, WHITE) - start_rect = start_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) - self.screen.blit(start_text, start_rect) - - # Meilleur score - highscore_text = self.font.render(f"Meilleur score : {self.highscore}", True, WHITE) - highscore_rect = highscore_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT * 2 // 3)) - self.screen.blit(highscore_text, highscore_rect) - - def draw_pause(self): - # Fond sombre semi-transparent - overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) - overlay.fill((0, 0, 0, 180)) - self.screen.blit(overlay, (0, 0)) - - # Texte de pause - pause_text = self.big_font.render("PAUSE", True, WHITE) - pause_rect = pause_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) - self.screen.blit(pause_text, pause_rect) - - # Instructions - resume_text = self.font.render("Appuyez sur ECHAP pour reprendre", True, WHITE) - resume_rect = resume_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT * 2 // 3)) - self.screen.blit(resume_text, resume_rect) - - def run(self): - while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.quit() - sys.exit() - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_m: - self.toggle_sound() - elif event.key == pygame.K_ESCAPE: - if self.game_state == PLAYING: - self.game_state = PAUSED - elif self.game_state == PAUSED: - self.game_state = PLAYING - elif event.key == pygame.K_SPACE: - if self.game_state == MENU: - self.reset_game() - self.game_state = PLAYING - elif self.game_state == GAME_OVER: - self.reset_game() - self.game_state = PLAYING - elif event.type == pygame.MOUSEBUTTONDOWN: - if event.button == 1: # Clic gauche - mouse_x, mouse_y = event.pos - button_x = SCREEN_WIDTH - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING - button_y = SCREEN_HEIGHT - SOUND_BUTTON_SIZE - SOUND_BUTTON_PADDING - if (button_x <= mouse_x <= button_x + SOUND_BUTTON_SIZE and - button_y <= mouse_y <= button_y + SOUND_BUTTON_SIZE): - self.toggle_sound() - elif event.button == 3: # Clic droit - if self.game_state == PLAYING: - self.player.activate_shield() - - # Fond étoilé toujours actif - self.screen.fill((5, 5, 15)) - self.update_stars() - self.draw_stars() - - if self.game_state == MENU: - self.menu_manager.draw_main_menu() - elif self.game_state == PLAYING: - self.player.update() - if not self.wave_transition: - self.spawn_enemy() - self.update_enemies() - self.update_effects() - self.update_wave() - - self.player.draw(self.screen) - for enemy in self.enemies: - enemy.draw(self.screen) - for enemy in self.enemies: - if isinstance(enemy, ShootingEnemy): - enemy.bullets.draw(self.screen) - self.draw_hud() - self.draw_wave_transition() - elif self.game_state == PAUSED: - # Afficher d'abord le jeu en arrière-plan - self.player.draw(self.screen) - for enemy in self.enemies: - enemy.draw(self.screen) - for enemy in self.enemies: - if isinstance(enemy, ShootingEnemy): - enemy.bullets.draw(self.screen) - self.draw_hud() - # Puis afficher le menu de pause par-dessus - self.menu_manager.draw_pause_menu() - elif self.game_state == GAME_OVER: - self.menu_manager.draw_game_over_menu() - - pygame.display.flip() - self.clock.tick(FPS) - -if __name__ == '__main__': - game = Game() - game.run() \ No newline at end of file diff --git a/survival-shooter/src/ui/__pycache__/menu_manager.cpython-312.pyc b/survival-shooter/src/ui/__pycache__/menu_manager.cpython-312.pyc deleted file mode 100644 index d348eee..0000000 Binary files a/survival-shooter/src/ui/__pycache__/menu_manager.cpython-312.pyc and /dev/null differ diff --git a/survival-shooter/src/ui/menu_manager.py b/survival-shooter/src/ui/menu_manager.py deleted file mode 100644 index 60368b7..0000000 --- a/survival-shooter/src/ui/menu_manager.py +++ /dev/null @@ -1,55 +0,0 @@ -from utils.constants import * -import pygame - -class MenuManager: - def __init__(self, game): - self.game = game - self.screen = game.screen - self.font = game.font - self.big_font = game.big_font - - def draw_main_menu(self): - self._draw_overlay() - self._draw_title("ALIEN WAVE", CYAN) - self._draw_text("Appuyez sur ESPACE pour commencer", WHITE, 0.5) - self._draw_text(f"Meilleur score : {self.game.highscore}", WHITE, 0.66) - - def draw_pause_menu(self): - self._draw_overlay() - self._draw_title("PAUSE", WHITE) - self._draw_text("Appuyez sur ECHAP pour reprendre", WHITE, 0.66) - - def draw_settings_menu(self): - self._draw_overlay() - self._draw_title("PARAMÈTRES", WHITE) - self._draw_resolution_buttons() - self._draw_back_button() - - def draw_game_over_menu(self): - self._draw_overlay() - self._draw_title("GAME OVER", RED) - self._draw_text(f"Score Final: {self.game.player.score}", WHITE, 0.5) - self._draw_text(f"Meilleur Score: {self.game.highscore}", CYAN, 0.58) - self._draw_text("Appuyez sur ESPACE pour recommencer", CYAN, 0.66) - self._draw_text("Appuyez sur ÉCHAP pour quitter", WHITE, 0.75) - - def _draw_overlay(self): - overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) - overlay.fill((0, 0, 0, 180)) - self.screen.blit(overlay, (0, 0)) - - def _draw_title(self, text, color): - title_text = self.big_font.render(text, True, color) - title_rect = title_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 3)) - self.screen.blit(title_text, title_rect) - - def _draw_text(self, text, color, y_ratio): - text_surface = self.font.render(text, True, color) - text_rect = text_surface.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT * y_ratio)) - self.screen.blit(text_surface, text_rect) - - def _draw_settings_button(self): - settings_text = self.font.render("Paramètres", True, WHITE) - self.settings_rect = settings_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT * 0.75)) - pygame.draw.rect(self.screen, WHITE, self.settings_rect.inflate(20, 10), 1) - self.screen.blit(settings_text, self.settings_rect) \ No newline at end of file diff --git a/survival-shooter/src/utils/__pycache__/__init__.cpython-312.pyc b/survival-shooter/src/utils/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 0bcf907..0000000 Binary files a/survival-shooter/src/utils/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/survival-shooter/src/utils/__pycache__/constants.cpython-312.pyc b/survival-shooter/src/utils/__pycache__/constants.cpython-312.pyc deleted file mode 100644 index dc42864..0000000 Binary files a/survival-shooter/src/utils/__pycache__/constants.cpython-312.pyc and /dev/null differ diff --git a/survival-shooter/src/utils/__pycache__/sound_generator.cpython-312.pyc b/survival-shooter/src/utils/__pycache__/sound_generator.cpython-312.pyc deleted file mode 100644 index cad2862..0000000 Binary files a/survival-shooter/src/utils/__pycache__/sound_generator.cpython-312.pyc and /dev/null differ diff --git a/survival-shooter/src/utils/__pycache__/spritesheet.cpython-312.pyc b/survival-shooter/src/utils/__pycache__/spritesheet.cpython-312.pyc deleted file mode 100644 index 7b99660..0000000 Binary files a/survival-shooter/src/utils/__pycache__/spritesheet.cpython-312.pyc and /dev/null differ