From 183594bd6e80f111bdddb7534b2facc66ff24212 Mon Sep 17 00:00:00 2001 From: tarik elouattassi Date: Wed, 25 Feb 2026 20:54:20 +0000 Subject: [PATCH] Ajout de SECONDGAME V2 --- SECONDGAME/debug.py | 10 + SECONDGAME/drop.py | 55 +++ SECONDGAME/enemy.py | 299 ++++++++++++++ SECONDGAME/graphics/player/player.png | Bin 0 -> 65220 bytes SECONDGAME/level.py | 555 ++++++++++++++++++++++++++ SECONDGAME/magic.py | 163 ++++++++ SECONDGAME/main.py | 175 ++++++++ SECONDGAME/map/boss_level.txt | 9 + SECONDGAME/map/level1.txt | 15 + SECONDGAME/map/level2.txt | 14 + SECONDGAME/map/level3.txt | 20 + SECONDGAME/map/level4.txt | 16 + SECONDGAME/map/level6.txt | 11 + SECONDGAME/player.py | 286 +++++++++++++ SECONDGAME/sav1.txt | 337 ++++++++++++++++ SECONDGAME/sav2.txt | 134 +++++++ SECONDGAME/sav3.txt | 194 +++++++++ SECONDGAME/save.txt | 171 ++++++++ SECONDGAME/save_data.json | 19 + SECONDGAME/save_manager.py | 50 +++ SECONDGAME/settings.py | 59 +++ SECONDGAME/support.py | 46 +++ SECONDGAME/tile.py | 153 +++++++ SECONDGAME/ui.py | 187 +++++++++ SECONDGAME/weapon.py | 325 +++++++++++++++ 25 files changed, 3303 insertions(+) create mode 100644 SECONDGAME/debug.py create mode 100644 SECONDGAME/drop.py create mode 100644 SECONDGAME/enemy.py create mode 100644 SECONDGAME/graphics/player/player.png create mode 100644 SECONDGAME/level.py create mode 100644 SECONDGAME/magic.py create mode 100644 SECONDGAME/main.py create mode 100644 SECONDGAME/map/boss_level.txt create mode 100644 SECONDGAME/map/level1.txt create mode 100644 SECONDGAME/map/level2.txt create mode 100644 SECONDGAME/map/level3.txt create mode 100644 SECONDGAME/map/level4.txt create mode 100644 SECONDGAME/map/level6.txt create mode 100644 SECONDGAME/player.py create mode 100644 SECONDGAME/sav1.txt create mode 100644 SECONDGAME/sav2.txt create mode 100644 SECONDGAME/sav3.txt create mode 100644 SECONDGAME/save.txt create mode 100644 SECONDGAME/save_data.json create mode 100644 SECONDGAME/save_manager.py create mode 100644 SECONDGAME/settings.py create mode 100644 SECONDGAME/support.py create mode 100644 SECONDGAME/tile.py create mode 100644 SECONDGAME/ui.py create mode 100644 SECONDGAME/weapon.py diff --git a/SECONDGAME/debug.py b/SECONDGAME/debug.py new file mode 100644 index 0000000..598ea6e --- /dev/null +++ b/SECONDGAME/debug.py @@ -0,0 +1,10 @@ +import pygame +pygame.init() +font = pygame.font.Font(None, 30) + +def debug(info, y=10, x=10): + display_surface = pygame.display.get_surface() + debug_surf = font.render(str(info), True, 'White') + debug_rect = debug_surf.get_rect(topleft=(x, y)) + pygame.draw.rect(display_surface, 'Black', debug_rect) + display_surface.blit(debug_surf, debug_rect) \ No newline at end of file diff --git a/SECONDGAME/drop.py b/SECONDGAME/drop.py new file mode 100644 index 0000000..9e1b943 --- /dev/null +++ b/SECONDGAME/drop.py @@ -0,0 +1,55 @@ +import pygame +import random +import math +from settings import * + +class Drop(pygame.sprite.Sprite): + def __init__(self, pos, drop_type, groups): + super().__init__(groups) + self.sprite_type = 'drop' + self.drop_type = drop_type + + # Créer l'image selon le type (taille un peu plus grande pour plus de lisibilité) + self.image = pygame.Surface((24, 24), pygame.SRCALPHA) + self.draw_drop() + self.rect = self.image.get_rect(center=pos) + self.hitbox = self.rect.inflate(-4, -4) + + # Animation de flottement + self.float_offset = 0 + self.float_speed = 2 + self.float_amplitude = 3 + + def draw_drop(self): + self.image.fill((0, 0, 0, 0)) # Transparent + + if self.drop_type == 'health_potion': + # Potion rouge de soin + pygame.draw.rect(self.image, '#8B0000', (6, 10, 12, 10), border_radius=3) # Corps + pygame.draw.rect(self.image, '#FF0000', (8, 8, 8, 8)) # Liquide + pygame.draw.circle(self.image, '#FFFFFF', (12, 6), 3) # Bulle + + elif self.drop_type == 'money_bag': + # Sac d'argent doré + pygame.draw.ellipse(self.image, '#FFD700', (4, 8, 16, 10)) # Sac + pygame.draw.line(self.image, '#000000', (12, 8), (12, 3), 2) # Corde + pygame.draw.circle(self.image, '#FFFF00', (10, 12), 2) # Pièce + pygame.draw.circle(self.image, '#FFFF00', (14, 12), 2) # Pièce + + def update(self): + # Animation de flottement + self.float_offset += self.float_speed + self.rect.y += self.float_amplitude * math.sin(self.float_offset * 0.1) * 0.1 + + def collect(self, player): + """Méthode appelée quand le joueur ramasse le drop""" + if self.drop_type == 'health_potion': + heal_amount = min(50, player.stats['health'] - player.health) # Soigne jusqu'à 50 HP + player.health += heal_amount + if player.health > player.stats['health']: + player.health = player.stats['health'] + elif self.drop_type == 'money_bag': + money_amount = random.randint(50, 150) + player.rubies += money_amount + + self.kill() # Supprimer le drop après collecte \ No newline at end of file diff --git a/SECONDGAME/enemy.py b/SECONDGAME/enemy.py new file mode 100644 index 0000000..1cab505 --- /dev/null +++ b/SECONDGAME/enemy.py @@ -0,0 +1,299 @@ +import pygame +import random +import math +from settings import * +from drop import Drop + +class Enemy(pygame.sprite.Sprite): + def __init__(self, monster_name, pos, groups, obstacle_sprites, damage_player, trigger_death_particles, create_magic=None, trigger_shake=None): + super().__init__(groups) + self.sprite_type = 'enemy' + self.monster_name = monster_name + self.create_magic = create_magic + self.trigger_shake = trigger_shake + self.groups_list = groups # Sauvegarder les groupes pour créer les drops + self.attack_cooldown = 1000 + + # --- STATS ET CONFIGURATION --- + if monster_name == 'squid': + self.health = 40 + self.speed = 3 + self.attack_damage = 10 + self.image_size = (64, 64) + elif monster_name == 'raccoon': + self.health = 60 + self.speed = 2.5 + self.attack_damage = 15 + self.image_size = (64, 64) + elif monster_name == 'demon': + self.health = 80 + self.speed = 3.5 + self.attack_damage = 20 + self.image_size = (64, 64) + elif monster_name == 'boss': + self.health = 200 + self.speed = 2 + self.attack_damage = 40 + self.image_size = (96, 96) + # Logique Boss + self.enraged = False + self.current_pattern = 'follow' + self.last_pattern_change = pygame.time.get_ticks() + self.pattern_duration = 3000 + self.attack_cooldown = 1500 + + elif monster_name == 'elite_enemy': + self.health = 150 + self.speed = 4.5 # Très rapide + self.attack_damage = 25 + self.image_size = (64, 64) + self.exp = 300 # Plus d'EXP + + # --- VISUEL ET PHYSIQUE --- + self.image = pygame.Surface(self.image_size, pygame.SRCALPHA) + self.draw_monster_shape() + self.rect = self.image.get_rect(topleft=pos) + self.hitbox = self.rect.inflate(-10, -10) + self.obstacle_sprites = obstacle_sprites + + self.direction = pygame.math.Vector2() + self.knockback_vector = pygame.math.Vector2() + self.exp = 2000 if monster_name == 'boss' else 100 + + # --- TIMERS ET ÉTATS --- + self.damage_player = damage_player + self.trigger_death_particles = trigger_death_particles + self.can_attack = True + self.attack_time = None + self.vulnerable = True + self.hit_time = None + self.invincibility_duration = 400 + + def draw_monster_shape(self): + self.image.fill((0, 0, 0, 0)) + + if self.monster_name == 'squid': + pygame.draw.ellipse(self.image, '#FF4444', (12, 10, 40, 45)) + pygame.draw.circle(self.image, 'white', (24, 25), 6) + pygame.draw.circle(self.image, 'white', (40, 25), 6) + pygame.draw.circle(self.image, 'black', (24, 25), 2) + pygame.draw.circle(self.image, 'black', (40, 25), 2) + for i in range(4): + pygame.draw.rect(self.image, '#CC0000', (16 + (i*8), 48, 6, 12), border_radius=2) + + elif self.monster_name in ['raccoon', 'demon']: + color = '#885522' if self.monster_name == 'raccoon' else '#333333' + pygame.draw.circle(self.image, color, (32, 35), 25) + pygame.draw.rect(self.image, '#222222', (15, 28, 34, 12), border_radius=5) + eye_color = 'yellow' if self.monster_name == 'raccoon' else 'red' + pygame.draw.circle(self.image, eye_color, (24, 34), 3) + pygame.draw.circle(self.image, eye_color, (40, 34), 3) + + elif self.monster_name == 'boss': + body_color = '#4B0082' if not getattr(self, 'enraged', False) else '#AA0000' + aura_color = 'purple' + if hasattr(self, 'current_pattern'): + if self.current_pattern == 'circle': aura_color = 'red' + if self.current_pattern == 'burst': aura_color = 'orange' + if self.current_pattern == 'berserk': aura_color = 'white' + + pygame.draw.rect(self.image, body_color, (10, 20, 76, 66), border_radius=15) + pygame.draw.polygon(self.image, 'gold', [(20, 20), (35, 0), (48, 20), (61, 0), (76, 20)]) + pygame.draw.circle(self.image, 'white', (48, 50), 18) + pygame.draw.circle(self.image, 'red', (48, 50), 10) + pygame.draw.circle(self.image, 'black', (48, 50), 5) + pygame.draw.rect(self.image, aura_color, (10, 20, 76, 66), 6, border_radius=15) + + elif self.monster_name == 'elite_enemy': + # Ennemi d'élite avec un design spécial (rouge et noir) + body_color = '#8B0000' # Rouge foncé + highlight_color = '#FF0000' # Rouge clair + + # Corps principal + pygame.draw.ellipse(self.image, body_color, (10, 15, 44, 34)) + + # Marques spéciales (comme des scarifications ou armure) + pygame.draw.line(self.image, '#000000', (20, 20), (44, 20), 3) + pygame.draw.line(self.image, '#000000', (20, 25), (44, 25), 3) + pygame.draw.line(self.image, '#000000', (20, 30), (44, 30), 3) + + # Yeux rougeoyants + pygame.draw.circle(self.image, highlight_color, (24, 22), 4) + pygame.draw.circle(self.image, highlight_color, (40, 22), 4) + pygame.draw.circle(self.image, '#FFFFFF', (24, 22), 2) + pygame.draw.circle(self.image, '#FFFFFF', (40, 22), 2) + + # Aura rouge + pygame.draw.ellipse(self.image, '#FF4444', (10, 15, 44, 34), 2) + + def get_player_distance_direction(self, player): + enemy_vec = pygame.math.Vector2(self.rect.center) + player_vec = pygame.math.Vector2(player.rect.center) + distance = (player_vec - enemy_vec).magnitude() + direction = (player_vec - enemy_vec).normalize() if distance > 0 else pygame.math.Vector2() + return (distance, direction) + + def boss_logic(self, player): + current_time = pygame.time.get_ticks() + if current_time - self.last_pattern_change >= self.pattern_duration: + patterns = ['follow', 'circle', 'burst'] + if getattr(self, 'enraged', False): patterns.append('berserk') + self.current_pattern = random.choice(patterns) + self.last_pattern_change = current_time + self.draw_monster_shape() + + dist, dir = self.get_player_distance_direction(player) + + if self.current_pattern == 'follow': + self.direction = dir + elif self.current_pattern == 'circle': + self.direction = pygame.math.Vector2(0,0) + if self.can_attack: + self.shoot_circle() + self.can_attack = False + self.attack_time = current_time + elif self.current_pattern == 'burst': + self.direction = dir * 0.5 + if self.can_attack: + self.create_magic('fireball', 20, 0, self.rect.center, dir) + self.can_attack = False + self.attack_time = current_time + elif self.current_pattern == 'berserk': + self.direction = dir + self.move_berserk(dir) + + def move_berserk(self, dir): + speed_boost = 6 if self.enraged else 4 + self.hitbox.x += dir.x * speed_boost + self.collision('horizontal') + self.hitbox.y += dir.y * speed_boost + self.collision('vertical') + + def shoot_circle(self): + for angle in range(0, 360, 45): + rad = math.radians(angle) + direction = pygame.math.Vector2(math.cos(rad), math.sin(rad)) + self.create_magic('fireball', 20, 0, self.rect.center, direction) + + def actions(self, player): + if self.monster_name == 'boss': + self.boss_logic(player) + else: + dist, dir = self.get_player_distance_direction(player) + if dist <= 50 and self.can_attack: + self.attack_time = pygame.time.get_ticks() + self.damage_player(self.attack_damage, 'physical') + self.can_attack = False + elif dist <= 400: self.direction = dir + else: self.direction = pygame.math.Vector2() + + def move(self, speed): + if not self.vulnerable: + # --- RECUL SÉCURISÉ (Empêche de traverser les murs) --- + self.hitbox.x += self.knockback_vector.x + self.collision('horizontal') + self.hitbox.y += self.knockback_vector.y + self.collision('vertical') + # Friction : s'arrête plus vite (0.7 au lieu de 0.9) + self.knockback_vector *= 0.7 + else: + # --- MOUVEMENT NORMAL --- + self.hitbox.x += self.direction.x * speed + self.collision('horizontal') + self.hitbox.y += self.direction.y * speed + self.collision('vertical') + + self.rect.center = self.hitbox.center + + def enemy_collision(self, direction): + """Gestion des collisions entre ennemis pour éviter l'entassement""" + # Obtenir tous les autres ennemis + enemy_sprites = [sprite for sprite in self.groups()[0] if hasattr(sprite, 'sprite_type') and sprite.sprite_type == 'enemy' and sprite != self] + + for enemy in enemy_sprites: + if enemy.hitbox.colliderect(self.hitbox): + if direction == 'horizontal': + if self.direction.x > 0: + self.hitbox.right = enemy.hitbox.left + if self.direction.x < 0: + self.hitbox.left = enemy.hitbox.right + if direction == 'vertical': + if self.direction.y > 0: + self.hitbox.bottom = enemy.hitbox.top + if self.direction.y < 0: + self.hitbox.top = enemy.hitbox.bottom + + def collision(self, direction): + # Collisions avec les murs + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if direction == 'horizontal': + # Détection selon la direction du mouvement OU du knockback + if self.direction.x > 0 or self.knockback_vector.x > 0: self.hitbox.right = sprite.hitbox.left + if self.direction.x < 0 or self.knockback_vector.x < 0: self.hitbox.left = sprite.hitbox.right + if direction == 'vertical': + if self.direction.y > 0 or self.knockback_vector.y > 0: self.hitbox.bottom = sprite.hitbox.top + if self.direction.y < 0 or self.knockback_vector.y < 0: self.hitbox.top = sprite.hitbox.bottom + + # Collisions avec les autres ennemis + self.enemy_collision(direction) + + def get_damage(self, player, attack_type): + if self.vulnerable: + # Calcul dégâts + damage = player.stats['attack'] * (1 + (player.sword_level * 0.5)) if attack_type == 'weapon' else 30 + self.health -= damage + + # Déclenchement Phase 2 Boss + if self.monster_name == 'boss' and not self.enraged and self.health <= 100: + self.enraged = True + self.speed *= 1.5 + self.attack_cooldown /= 2 + self.pattern_duration = 1500 + if self.trigger_shake: self.trigger_shake(15, 40) + self.draw_monster_shape() + + if self.health > 0: + player_pos = pygame.math.Vector2(player.rect.center) + enemy_pos = pygame.math.Vector2(self.rect.center) + + # Appliquer Knockback (Sauf pour le Boss pour éviter qu'il sorte de la petite map) + if (enemy_pos - player_pos).magnitude() != 0: + # Force réduite pour éviter l'effet de "téléportation" lors des collisions + force = 0 if self.monster_name == 'boss' else 3 + self.knockback_vector = (enemy_pos - player_pos).normalize() * force + + self.vulnerable = False + self.hit_time = pygame.time.get_ticks() + self.image.fill((255, 0, 0, 150), special_flags=pygame.BLEND_RGBA_MULT) + + if self.health <= 0: + was_boss = self.monster_name == 'boss' + + # Chance de drop (15% pour les ennemis normaux, 100% pour le boss) + drop_chance = 100 if was_boss else 15 + if random.randint(1, 100) <= drop_chance: + # Créer un drop + drop_type = 'money_bag' if random.randint(1, 2) == 1 else 'health_potion' + Drop(self.rect.center, drop_type, self.groups_list) + + self.kill() + return (self.exp, was_boss) + return 0 + + def cooldowns(self): + current_time = pygame.time.get_ticks() + if not self.can_attack: + if current_time - self.attack_time >= self.attack_cooldown: + self.can_attack = True + if not self.vulnerable: + if current_time - self.hit_time >= self.invincibility_duration: + self.vulnerable = True + self.draw_monster_shape() + + def update(self): + self.move(self.speed) + self.cooldowns() + + def enemy_update(self, player): + self.actions(player) \ No newline at end of file diff --git a/SECONDGAME/graphics/player/player.png b/SECONDGAME/graphics/player/player.png new file mode 100644 index 0000000000000000000000000000000000000000..273847e3d32304b21de70e8da36d80105afed65d GIT binary patch literal 65220 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

D-627e#}r46| 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 literal 0 HcmV?d00001 diff --git a/SECONDGAME/level.py b/SECONDGAME/level.py new file mode 100644 index 0000000..6f13fd0 --- /dev/null +++ b/SECONDGAME/level.py @@ -0,0 +1,555 @@ +import pygame +from settings import * +from tile import Tile +from player import Player +from support import * +from weapon import Weapon, NewWeapon +from enemy import Enemy +from ui import UI +from magic import MagicPlayer, Projectile +from drop import Drop +import random +import math + +class Mission: + def __init__(self, mission_type, pos=None): + self.type = mission_type + self.active = True + self.start_time = pygame.time.get_ticks() + self.duration = 30000 # 30 secondes pour la mission de vitesse + self.enemies_killed = 0 + self.target_kills = 10 + self.reward_money = random.randint(200, 400) + self.reward_heart = random.choice([True, False]) + + if mission_type == 'elite_hunt': + self.description = "Tuez l'ennemi d'élite rapide !" + self.enemy_pos = pos + elif mission_type == 'speed_kill': + self.description = "Tuez 10 ennemis en 30 secondes !" + + def update(self, enemies_killed, current_time): + if self.type == 'speed_kill': + self.enemies_killed = enemies_killed + if current_time - self.start_time >= self.duration: + self.active = False + return False # Mission échouée + elif self.enemies_killed >= self.target_kills: + self.active = False + return True # Mission réussie + return None # Mission en cours + + def get_time_left(self): + return max(0, self.duration - (pygame.time.get_ticks() - self.start_time)) + +class Level: + def __init__(self, save_data, change_level): + # Configuration initiale + self.display_surface = pygame.display.get_surface() + + # Variables d'état + self.show_talents = False + self.show_help = False + self.flash_alpha = 0 + self.current_attack = None + + # Groupes de sprites + self.visible_sprites = YSortCameraGroup() + self.obstacle_sprites = pygame.sprite.Group() + self.attack_sprites = pygame.sprite.Group() + self.attackable_sprites = pygame.sprite.Group() + self.drop_sprites = pygame.sprite.Group() + + # Données de progression + self.save_data = save_data + if 'life_hearts' not in self.save_data: self.save_data['life_hearts'] = 0 + # Système simplifié : une seule arme (épée) et une seule magie (boule de feu) + self.level_number = save_data['current_level'] + self.change_level = change_level + + # Système de missions + self.active_mission = None + self.mission_timer = 0 + self.mission_start_time = 0 + self.enemies_killed_in_mission = 0 + self.mission_enemy = None # Pour l'ennemi d'élite de mission + + # Effets visuels + self.flash_surface = pygame.Surface((WIDTH, HEIGHT)) + self.flash_surface.fill((255, 255, 255)) + + # Systèmes de jeu + self.ui = UI() + self.magic_player = MagicPlayer(None) + + # Création du niveau + self.create_map() + # --- LOGIQUE DE PROBABILITÉ UNIQUE --- + # On décide au début du niveau s'il y a une mission + is_boss_level = self.level_number % 5 == 0 # On évite les missions aux boss + + if not is_boss_level: + if random.randint(1, 12) == 1: + self.try_spawn_mission() + + def create_map(self): + self.layout = import_csv_layout(LEVEL_MAPS[self.level_number]) + layout = self.layout + + for row_index, row in enumerate(layout): + for col_index, col in enumerate(row): + x = col_index * TILESIZE + y = row_index * TILESIZE + + if col == 'x': + Tile((x,y), [self.visible_sprites, self.obstacle_sprites], 'wall') + elif col == 's': + self.exit_sprite = Tile((x,y), [self.visible_sprites], 'exit') + self.exit_sprite.image.set_alpha(0) + elif col == 'r': + self.secret_reward = Tile((x,y), [self.visible_sprites], 'reward') + elif col == 'p': + # Toujours l'épée comme arme unique + self.player = Player( + (x,y), [self.visible_sprites], + self.obstacle_sprites, + self.create_attack, + self.destroy_attack, + self.create_magic, + self.save_data, + 'sword') + elif col in ['e','d']: + monster = 'demon' if col == 'd' else ('raccoon' if self.level_number > 2 else 'squid') + Enemy(monster, (x,y), [self.visible_sprites, self.attackable_sprites, self.drop_sprites], + self.obstacle_sprites, self.damage_player, self.trigger_death_particles) + elif col == 'B': + Enemy('boss', (x,y), [self.visible_sprites, self.attackable_sprites, self.drop_sprites], + self.obstacle_sprites, self.damage_player, self.trigger_death_particles, + self.spawn_boss_projectile, self.trigger_boss_effects) + + # Sécurité : si aucune case 'p' n'est trouvée dans la carte, créer un joueur par défaut + if not hasattr(self, 'player'): + # Position par défaut : centre de l'écran + default_x = WIDTH // 2 + default_y = HEIGHT // 2 + + self.player = Player( + (default_x, default_y), [self.visible_sprites], + self.obstacle_sprites, + self.create_attack, + self.destroy_attack, + self.create_magic, + self.save_data, + 'sword') + + # --- EFFETS ET COMBAT --- + def trigger_boss_effects(self, amount, duration): + self.visible_sprites.shake(amount, duration) + self.flash_alpha = 255 + + def spawn_boss_projectile(self, style, strength, cost, pos, direction): + Projectile(style, pos, direction, [self.visible_sprites, self.attack_sprites], + self.obstacle_sprites, [self.player], self.player) + + def trigger_dungeon_reward(self): + self.save_data['life_hearts'] += 1 + self.player.life_hearts = self.save_data['life_hearts'] # Sync avec le player + self.player.exp += 500 + self.flash_alpha = 150 + + def sync_player_to_save(self): + """Synchronise les valeurs persistantes du joueur avec la sauvegarde.""" + if hasattr(self, 'player'): + self.save_data['exp'] = self.player.exp + self.save_data['rubies'] = self.player.rubies + self.save_data['life_hearts'] = self.player.life_hearts + self.save_data['stats'] = self.player.stats + self.save_data['sword_level'] = self.player.sword_level + + def create_attack(self): + # Système simplifié : toujours l'épée + self.current_attack = NewWeapon(self.player, [self.visible_sprites, self.attack_sprites], + 'sword', self.obstacle_sprites, self.attackable_sprites) + + # Logique Épée Laser (si 3 cœurs ou plus) conservée + if self.player.life_hearts >= 3: + dir_vec = self.player.get_direction_vector() + Projectile('sword_beam', self.player.rect.center, dir_vec, + [self.visible_sprites, self.attack_sprites], + self.obstacle_sprites, self.attackable_sprites, self.player, self.victory) + + def create_magic(self, style, strength, cost, element=None, level=None): + """Système simplifié : seule la boule de feu de base est disponible.""" + if style == 'fireball': + self.magic_player.fireball(self.player, cost, [self.visible_sprites, self.attack_sprites], + self.obstacle_sprites, self.attackable_sprites, self.victory) + + def destroy_attack(self): + if self.current_attack: self.current_attack.kill() + self.current_attack = None + + def player_attack_logic(self): + if self.attack_sprites: + # Créer une copie de la liste pour éviter les modifications pendant l'itération + attack_sprites_copy = self.attack_sprites.sprites().copy() + for attack_sprite in attack_sprites_copy: + # Vérifier que le sprite existe encore + if attack_sprite not in self.attack_sprites: + continue + + collision_sprites = pygame.sprite.spritecollide(attack_sprite, self.attackable_sprites, False) + if collision_sprites: + for target_sprite in collision_sprites: + if target_sprite.sprite_type == 'enemy': + # --- RESET TIMER TÉLÉPORTATION --- + target_sprite.last_hit_time = pygame.time.get_ticks() + + damage_result = target_sprite.get_damage(self.player, 'weapon') + if isinstance(damage_result, tuple): + exp_gagne, was_boss_killed = damage_result + if exp_gagne > 0: + self.player.exp += exp_gagne + # Compter pour les missions (sauf boss et ennemis d'élite) + if not was_boss_killed and target_sprite.monster_name != 'elite_enemy': + self.on_enemy_killed_for_mission() + if was_boss_killed: + self.victory() + else: + exp_gagne = damage_result + if exp_gagne > 0: + self.player.exp += exp_gagne + # Compter pour les missions + if target_sprite.monster_name not in ['boss', 'elite_enemy']: + self.on_enemy_killed_for_mission() + if target_sprite.monster_name == 'boss' and not target_sprite.alive(): + self.victory() + + def damage_player(self, amount, attack_type): + if not self.player.invisible: + self.player.get_damage(amount) + # Annuler l'effet des 3 cœurs si le joueur se fait toucher + if self.player.life_hearts >= 3: + self.player.life_hearts = 0 + self.save_data['life_hearts'] = 0 + # Mise à jour de la save_data pour l'UI après perte du bonus + self.save_data['life_hearts'] = self.player.life_hearts + if self.player.health <= 0: self.player.kill() + + def enemy_teleportation_logic(self): + """Téléporte les ennemis s'ils n'ont pas été touchés depuis 90 secondes""" + current_time = pygame.time.get_ticks() + for enemy in self.attackable_sprites: + if enemy.sprite_type == 'enemy': + # Initialisation du timer si inexistant + if not hasattr(enemy, 'last_hit_time'): + enemy.last_hit_time = current_time + + # Si 90 secondes sans dégâts + if current_time - enemy.last_hit_time >= 90000: + angle = random.uniform(0, 2 * math.pi) + dist = random.randint(150, 250) + new_x = self.player.rect.centerx + math.cos(angle) * dist + new_y = self.player.rect.centery + math.sin(angle) * dist + + # Vérification collision murs + temp_rect = enemy.hitbox.copy() + temp_rect.center = (new_x, new_y) + can_teleport = True + for wall in self.obstacle_sprites: + if wall.hitbox.colliderect(temp_rect): + can_teleport = False + break + + if can_teleport: + enemy.hitbox.center = (new_x, new_y) + enemy.rect.center = enemy.hitbox.center + enemy.last_hit_time = current_time # Reset timer après TP + + def trigger_death_particles(self, pos, particle_type): + pass + + # --- GESTION DE LA SORTIE --- + def check_exit_collision(self): + if hasattr(self, 'exit_sprite'): + if self.exit_sprite.image.get_alpha() > 0: + detection_zone = self.exit_sprite.rect.inflate(30, 30) + if self.player.hitbox.colliderect(detection_zone): + self.next_level() + + def check_drop_collection(self): + """Vérifie si le joueur ramasse des drops""" + for drop in self.drop_sprites: + # Vérifier que c'est bien un objet Drop (pas un Enemy) + if hasattr(drop, 'collect') and drop.hitbox.colliderect(self.player.hitbox): + drop.collect(self.player) + + def try_spawn_mission(self): + """Tente de faire apparaître une mission (chance rare)""" + if self.active_mission is None and random.randint(1, 100) <= 20: # 20% de chance + mission_type = random.choice(['elite_hunt', 'speed_kill']) + self.active_mission = Mission(mission_type) + + if mission_type == 'elite_hunt': + # Faire apparaître l'ennemi d'élite près du joueur + max_attempts = 20 # Éviter une boucle infinie + valid_position = None + + for _ in range(max_attempts): + angle = random.uniform(0, 2 * math.pi) + dist = random.randint(200, 300) + x = self.player.rect.centerx + math.cos(angle) * dist + y = self.player.rect.centery + math.sin(angle) * dist + + # Vérifier que la position est dans les limites de la map + map_width = len(self.layout[0]) * TILESIZE if self.layout else WIDTH + map_height = len(self.layout) * TILESIZE if self.layout else HEIGHT + + if 0 <= x < map_width and 0 <= y < map_height: + # Vérifier qu'il n'y a pas d'obstacle à cette position + temp_rect = pygame.Rect(x - 16, y - 16, 32, 32) # Taille approximative d'un ennemi + collision = False + for obstacle in self.obstacle_sprites: + if obstacle.hitbox.colliderect(temp_rect): + collision = True + break + + if not collision: + valid_position = (x, y) + break + + # Créer l'ennemi d'élite seulement si on a trouvé une position valide + if valid_position: + self.mission_enemy = Enemy('elite_enemy', valid_position, [self.visible_sprites, self.attackable_sprites, self.drop_sprites], + self.obstacle_sprites, self.damage_player, self.trigger_death_particles) + + elif mission_type == 'speed_kill': + # Pour la mission speed_kill, faire apparaître quelques ennemis supplémentaires + self.spawn_mission_enemies(3) # Faire apparaître 3 ennemis supplémentaires + + def spawn_mission_enemies(self, count): + """Fait apparaître des ennemis supplémentaires pour les missions""" + map_width = len(self.layout[0]) * TILESIZE if self.layout else WIDTH + map_height = len(self.layout) * TILESIZE if self.layout else HEIGHT + + spawned = 0 + max_attempts = 50 # Éviter une boucle infinie + + for _ in range(max_attempts): + if spawned >= count: + break + + # Choisir une position aléatoire dans la map (éviter les bords) + margin = 100 + x = random.randint(margin, map_width - margin) + y = random.randint(margin, map_height - margin) + + # Vérifier que la position n'est pas sur un obstacle + temp_rect = pygame.Rect(x - 16, y - 16, 32, 32) + collision = False + for obstacle in self.obstacle_sprites: + if obstacle.hitbox.colliderect(temp_rect): + collision = True + break + + # Vérifier qu'il n'y a pas d'ennemi déjà présent + for enemy in self.attackable_sprites: + if enemy.hitbox.colliderect(temp_rect): + collision = True + break + + # Vérifier la distance avec le joueur (pas trop près) + player_dist = pygame.math.Vector2(x, y).distance_to(self.player.rect.center) + if player_dist < 150: + collision = True + + if not collision: + # Créer un ennemi (type aléatoire selon le niveau) + monster = 'demon' if self.level_number > 2 else 'squid' + Enemy(monster, (x, y), [self.visible_sprites, self.attackable_sprites], + self.obstacle_sprites, self.damage_player, self.trigger_death_particles) + spawned += 1 + + def update_missions(self): + """Met à jour les missions actives""" + # Chance de déclencher une mission + # Mettre à jour la mission active + if self.active_mission: + current_time = pygame.time.get_ticks() + result = self.active_mission.update(self.enemies_killed_in_mission, current_time) + + if result is True: # Mission réussie + self.complete_mission() + elif result is False: # Mission échouée + self.fail_mission() + + def complete_mission(self): + """Termine une mission avec succès""" + if self.active_mission: + # Récompenses + self.player.rubies += self.active_mission.reward_money + if self.active_mission.reward_heart: + if self.save_data['life_hearts'] < 3: + self.save_data['life_hearts'] += 1 + self.player.life_hearts = self.save_data['life_hearts'] + + # Supprimer l'ennemi de mission s'il existe + if self.mission_enemy and self.mission_enemy.alive(): + self.mission_enemy.kill() + + self.active_mission = None + self.mission_enemy = None + self.enemies_killed_in_mission = 0 + + def fail_mission(self): + """Échoue une mission""" + if self.active_mission: + # Supprimer l'ennemi de mission s'il existe + if self.mission_enemy and self.mission_enemy.alive(): + self.mission_enemy.kill() + + self.active_mission = None + self.mission_enemy = None + self.enemies_killed_in_mission = 0 + + def on_enemy_killed_for_mission(self): + """Incrémenter le compteur d'ennemis tués pour la mission de vitesse""" + if self.active_mission and self.active_mission.type == 'speed_kill': + self.enemies_killed_in_mission += 1 + + def next_level(self): + self.save_data['current_level'] += 1 + # Si on vient de finir le boss level (niveau 5), passer au niveau 6 (salle vide) + # mais seulement après avoir collecté la récompense du boss + if self.level_number == 5: # Boss level + # Ajouter une récompense spéciale pour le boss + self.player.rubies += 700 # Beaucoup d'argent du boss + self.player.exp += 1000 # EXP bonus + self.change_level(self.save_data) + + def victory(self): + # Après avoir tué le boss, faire apparaître l'escalier de sortie au lieu de retourner au niveau 1 + if hasattr(self, 'exit_sprite'): + self.exit_sprite.image.set_alpha(255) # Faire apparaître l'escalier + # Ne pas changer de niveau immédiatement - le joueur doit marcher vers l'escalier + + def input_talents(self): + keys = pygame.key.get_pressed() + if self.player.exp >= 500: + if keys[pygame.K_1]: + self.player.stats['health'] += 20 + self.player.exp -= 500 + elif keys[pygame.K_2]: + self.player.stats['attack'] += 5 + self.player.exp -= 500 + elif keys[pygame.K_3]: + self.player.speed += 0.5 + self.player.exp -= 500 + + # Menus d'armes et de magie supprimés : les méthodes associées sont désormais vides + def input_weapon_menu(self): + pass + + def input_magic_menu(self): + pass + + def update_selected_magic(self): + pass + + def run(self): + # 1. Rendu et UI + self.visible_sprites.custom_draw(self.player) + self.ui.display(self.player, self.save_data) + + # 2. Décompte des ennemis (excluant les ennemis de mission) + enemies = [s for s in self.attackable_sprites if s.sprite_type == 'enemy' and s.monster_name != 'elite_enemy'] + enemy_count = len(enemies) + self.ui.show_enemy_count(enemy_count) + + # Synchronisation du HUD avec les coeurs du joueur + self.save_data['life_hearts'] = self.player.life_hearts + if self.save_data['life_hearts'] > 0: + self.ui.show_special_hearts(self.save_data['life_hearts']) + + # Affichage de la mission active + if self.active_mission: + self.ui.display_mission(self.active_mission) + + # 3. Flash blanc + if self.flash_alpha > 0: + self.flash_alpha -= 4 + self.flash_surface.set_alpha(self.flash_alpha) + self.display_surface.blit(self.flash_surface, (0,0)) + + # 4. Menus et Logique + if self.show_help: + self.ui.display_help() + elif self.show_talents: + self.ui.display_talent_menu(self.player) + self.input_talents() + else: + self.visible_sprites.update() + self.visible_sprites.enemy_update(self.player) + self.player_attack_logic() + self.enemy_teleportation_logic() + + # Récompense donjon + if hasattr(self, 'secret_reward') and self.secret_reward.alive(): + if self.player.hitbox.colliderect(self.secret_reward.rect): + self.trigger_dungeon_reward() + self.secret_reward.kill() + + # L'escalier devient visible si enemy_count == 0 + if hasattr(self, 'exit_sprite'): + if enemy_count == 0: + self.exit_sprite.image.set_alpha(255) + self.check_exit_collision() + else: + self.exit_sprite.image.set_alpha(0) + + # Vérifier la collecte des drops + self.check_drop_collection() + + # Système de missions + self.update_missions() + + # Gestion des menus + + if self.show_talents: + self.ui.display_talent_menu(self.player) + self.input_talents() + + # Synchroniser chaque frame les données persistantes (XP, rubis, upgrades, etc.) + self.sync_player_to_save() + +# --- GROUPE CAMERA --- +class YSortCameraGroup(pygame.sprite.Group): + def __init__(self): + super().__init__() + self.display_surface = pygame.display.get_surface() + self.half_width = self.display_surface.get_size()[0] // 2 + self.half_height = self.display_surface.get_size()[1] // 2 + self.offset = pygame.math.Vector2() + self.shake_amount = 0 + self.shake_duration = 0 + + def shake(self, amount, duration): + self.shake_amount = amount + self.shake_duration = duration + + def custom_draw(self, player): + self.offset.x = player.rect.centerx - self.half_width + self.offset.y = player.rect.centery - self.half_height + + if self.shake_duration > 0: + self.offset.x += random.randint(-self.shake_amount, self.shake_amount) + self.offset.y += random.randint(-self.shake_amount, self.shake_amount) + self.shake_duration -= 1 + + for sprite in sorted(self.sprites(), key=lambda sprite: sprite.rect.centery): + offset_pos = sprite.rect.topleft - self.offset + self.display_surface.blit(sprite.image, offset_pos) + + def enemy_update(self, player): + enemy_sprites = [s for s in self.sprites() if hasattr(s, 'sprite_type') and s.sprite_type == 'enemy'] + for enemy in enemy_sprites: + enemy.enemy_update(player) \ No newline at end of file diff --git a/SECONDGAME/magic.py b/SECONDGAME/magic.py new file mode 100644 index 0000000..e4dcad3 --- /dev/null +++ b/SECONDGAME/magic.py @@ -0,0 +1,163 @@ +import pygame +from settings import * +from random import randint + +class MagicPlayer: + def __init__(self, animation_player): + self.animation_player = animation_player + + def fireball(self, player, cost, groups, obstacle_sprites, attackable_sprites, boss_victory_callback=None): + """Magie unique : boule de feu de base.""" + if player.energy >= cost: + player.energy -= cost + + # Déterminer la direction du projectile selon le regard du joueur + direction = pygame.math.Vector2(0,0) + if 'right' in player.status: direction = pygame.math.Vector2(1,0) + elif 'left' in player.status: direction = pygame.math.Vector2(-1,0) + elif 'up' in player.status: direction = pygame.math.Vector2(0,-1) + else: direction = pygame.math.Vector2(0,1) + + # Créer le projectile + Projectile('fireball', player.rect.center, direction, groups, obstacle_sprites, attackable_sprites, player, boss_victory_callback) + + def cast_ice_spell(self, player, level, spell_data, attackable_sprites): + print(f"Lancement glace niveau {level}") # Debug + if level == 1: + # Gel - ralentit avec effet visuel + affected_count = 0 + for sprite in attackable_sprites: + dist = (pygame.math.Vector2(sprite.rect.center) - pygame.math.Vector2(player.rect.center)).magnitude() + if dist < 150 and hasattr(sprite, 'speed'): + sprite.speed *= 0.3 # Ralentissement à 30% + sprite.frozen_timer = pygame.time.get_ticks() + 3000 # 3 secondes + affected_count += 1 + + # Effet visuel : cercle de gel + if affected_count > 0: + freeze_effect = IceFreezeEffect(player.rect.center, 150) + # On ne peut pas ajouter directement aux groupes ici, mais l'effet sera visible temporairement + elif level == 6: + print("Explosion glacée niveau 6") # Debug + # Explosion glacée + IceExplosion(player.rect.center, attackable_sprites, spell_data['damage']) + elif level == 10: + print("Gel instantané niveau 10") # Debug + # Gel instantané + killed_count = 0 + for sprite in attackable_sprites: + dist = (pygame.math.Vector2(sprite.rect.center) - pygame.math.Vector2(player.rect.center)).magnitude() + if dist < 100 and hasattr(sprite, 'health'): + sprite.health = 0 # Mort instantanée + killed_count += 1 + print(f"Gel instantané: {killed_count} ennemis tués") # Debug + + def cast_water_spell(self, player, level, spell_data, groups, attackable_sprites, player_ref): + direction = pygame.math.Vector2(0, 0) + if 'right' in player.status: direction = pygame.math.Vector2(1, 0) + elif 'left' in player.status: direction = pygame.math.Vector2(-1, 0) + elif 'up' in player.status: direction = pygame.math.Vector2(0, -1) + else: direction = pygame.math.Vector2(0, 1) + + if level == 1: + print("Jet d'eau niveau 1: repousse les ennemis") # Debug + # Jet d'eau - repousse avec effet visuel + WaterJetProjectile(player_ref.rect.center, direction, groups, attackable_sprites, spell_data['damage']) + # Effet visuel autour du joueur + push_effect = WaterPushEffect(player_ref.rect.center, 100) + groups[0].add(push_effect) + elif level == 6: + print("Prison d'eau niveau 6") # Debug + # Prison d'eau + WaterPrisonProjectile(player_ref.rect.center, direction, groups, attackable_sprites, spell_data['damage']) + elif level == 10: + print("Eau vitalisante niveau 10") # Debug + # Eau vitalisante + HealingWaterProjectile(player_ref.rect.center, direction, groups, attackable_sprites, player_ref, spell_data['damage']) + + def cast_wind_spell(self, player, level, spell_data, groups, attackable_sprites, player_ref): + print(f"Lancement vent niveau {level}") # Debug + if level == 1: + print("Dash de vent niveau 1") # Debug + # Dash avec effet visuel + WindDash(player_ref) + # Effet visuel temporaire autour du joueur + dash_effect = WindDashEffect(player_ref) + groups[0].add(dash_effect) # Ajouter au groupe visible + elif level == 6: + print("Tourbillon de vent niveau 6") # Debug + # Tourbillon + WindVortex(player_ref.rect.center, groups, attackable_sprites, spell_data['damage']) + elif level == 10: + print("Lames d'air niveau 10") # Debug + # Lames d'air + WindBlades(player_ref.rect.center, groups, attackable_sprites, spell_data['damage']) + + # Les autres magies (glace, eau, vent, invisibilité, etc.) ont été supprimées + +class Projectile(pygame.sprite.Sprite): + def __init__(self, type, pos, direction, groups, obstacle_sprites, attackable_sprites, player, boss_victory_callback=None): + super().__init__(groups) + self.player = player # On garde une référence au joueur + self.boss_victory_callback = boss_victory_callback + self.sprite_type = 'projectile' + self.image = pygame.Surface((32, 32)) + if type == 'fireball': + self.image.fill('orange') + elif type == 'sword_beam': + self.image.fill('red') + else: + self.image.fill('blue') + self.rect = self.image.get_rect(center = pos) + + self.direction = direction + self.speed = 10 + self.obstacle_sprites = obstacle_sprites + self.attackable_sprites = attackable_sprites + + self.spawn_time = pygame.time.get_ticks() + self.duration = 1500 + self.pierces_walls = (type == 'sword_beam') # Sword beams pierce walls + + def update(self): + self.rect.x += self.direction.x * self.speed + self.rect.y += self.direction.y * self.speed + + # Collision murs (sauf si perce les murs) + if not self.pierces_walls: + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.rect): + self.kill() + break + + # Collision Ennemis avec récompenses + attackable_sprites_copy = self.attackable_sprites.sprites().copy() + for sprite in attackable_sprites_copy: + # Vérifier que le sprite existe encore + if sprite not in self.attackable_sprites or not hasattr(sprite, 'hitbox'): + continue + + if sprite.hitbox.colliderect(self.rect): + if hasattr(sprite, 'get_damage'): + # On passe self.player au lieu de None pour l'EXP + damage_result = sprite.get_damage(self.player, 'fireball') + if isinstance(damage_result, tuple): + exp_gagne, was_boss_killed = damage_result + if exp_gagne and exp_gagne > 0: + self.player.exp += exp_gagne + self.player.rubies += randint(5, 15) + if was_boss_killed and self.boss_victory_callback: + self.boss_victory_callback() + else: + exp_gagne = damage_result + if exp_gagne and exp_gagne > 0: + self.player.exp += exp_gagne + self.player.rubies += randint(5, 15) + self.kill() + break # Sortir de la boucle après avoir touché un ennemi + + if pygame.time.get_ticks() - self.spawn_time >= self.duration: + self.kill() + + # Les classes avancées de projectiles/effets (explosions, glace, eau, vent...) + # ont été supprimées pour simplifier le système de magie. \ No newline at end of file diff --git a/SECONDGAME/main.py b/SECONDGAME/main.py new file mode 100644 index 0000000..525e92f --- /dev/null +++ b/SECONDGAME/main.py @@ -0,0 +1,175 @@ +import pygame, sys +from settings import * +from level import Level +from save_manager import SaveManager +import os + +class Game: + def __init__(self): + # Configuration initiale de Pygame + pygame.init() + self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) + pygame.display.set_caption('Zelda RPG - New Game+ Edition') + self.clock = pygame.time.Clock() + + # 1. Gestionnaire de sauvegarde + self.save_manager = SaveManager() + self.data = self.save_manager.load() +# SUPPRIMEZ OU COMMENTEZ CETTE LIGNE : + # self.generate_world_maps() + + # Initialisation des données de sauvegarde + # Dans main.py, à l'intérieur de class Game, méthode __init__ + self.data = { + 'current_level': 1, + 'health': 200, + 'exp': 0, + 'rubies': 0, + 'life_hearts': 0 , + 'sword_level': 1, + # AJOUTEZ CE BLOC 'stats' ICI : + 'stats': { + 'health': 100, + 'energy': 60, + 'attack': 10, + 'magic': 4, + 'speed': 5 + } + } + # Lancer le premier niveau + self.level = Level(self.data, self.update_save_data) + + # 3. États du jeu + self.game_state = 'MENU' + self.level = None + self.last_escape_time = 0 # Pour le double Échap + self.font = pygame.font.Font(UI_FONT, 50) + + + def start_game(self, is_new=True): + if is_new: + self.data = self.save_manager.default_data + else: + self.data = self.save_manager.load() + + self.level = Level(self.data, self.update_save_data) + self.game_state = 'PLAYING' + + def update_save_data(self, new_data): + """Callback utilisé par Level pour sauvegarder la progression.""" + self.data = new_data + self.save_manager.save(self.data) + # On recharge le niveau avec les nouvelles données (ex: passage au niveau suivant) + self.level = Level(self.data, self.update_save_data) + + def draw_menu(self): + # 1. Arrière-plan stylisé (Dégradé ou Couleur sombre profonde) + self.screen.fill('#0a0a0c') + + # Petit effet de "vignettage" (bordures sombres) + for i in range(10): + alpha = 150 - (i * 15) + s = pygame.Surface((WIDTH, HEIGHT)) + s.set_alpha(alpha) + s.fill('#000000') + pygame.draw.rect(s, (0,0,0,0), (i*20, i*20, WIDTH-(i*40), HEIGHT-(i*40))) + self.screen.blit(s, (0,0)) + + # 2. Titre avec Ombre portée + title_text = "ZELDA ADVENTURE" + # Ombre + shadow_surf = self.font.render(title_text, True, '#3d2b00') + shadow_rect = shadow_surf.get_rect(center=(WIDTH//2 + 4, 154)) + self.screen.blit(shadow_surf, shadow_rect) + # Texte principal + title_surf = self.font.render(title_text, True, 'gold') + title_rect = title_surf.get_rect(center=(WIDTH//2, 150)) + self.screen.blit(title_surf, title_rect) + + # 3. Gestion des boutons et des interactions + mouse_pos = pygame.mouse.get_pos() + btn_new = pygame.Rect(WIDTH//2 - 150, 300, 300, 60) + btn_cont = pygame.Rect(WIDTH//2 - 150, 400, 300, 60) + + for btn, text in [(btn_new, "NOUVEAU JEU"), (btn_cont, "CONTINUER")]: + # Effet Hover (si la souris est sur le bouton) + is_hovered = btn.collidepoint(mouse_pos) + bg_color = '#444444' if is_hovered else '#222222' + border_color = 'gold' if is_hovered else '#666666' + text_color = 'white' if not is_hovered else 'gold' + + # Dessin du bouton (Fond + Bordure) + pygame.draw.rect(self.screen, bg_color, btn, border_radius=12) + pygame.draw.rect(self.screen, border_color, btn, 3, border_radius=12) + + # Texte du bouton centré + text_surf = self.font.render(text, True, text_color) + text_rect = text_surf.get_rect(center=btn.center) + self.screen.blit(text_surf, text_rect) + + return btn_new, btn_cont + + def draw_game_over(self): + self.screen.fill('black') + go_surf = self.font.render("GAME OVER", True, 'red') + self.screen.blit(go_surf, (WIDTH//2 - 120, HEIGHT//2 - 50)) + retry_surf = pygame.font.Font(None, 30).render("Appuyez sur R pour le Menu", True, 'white') + self.screen.blit(retry_surf, (WIDTH//2 - 110, HEIGHT//2 + 50)) + + def run(self): + while True: + # --- GESTION DES ÉVÉNEMENTS --- + for event in pygame.event.get(): + if event.type == pygame.QUIT: + if self.level: self.update_save_data(self.level.save_data) + pygame.quit() + sys.exit() + + if event.type == pygame.KEYDOWN: + # Double Échap pour quitter + if event.key == pygame.K_ESCAPE: + current_time = pygame.time.get_ticks() + if current_time - self.last_escape_time < 500: + if self.level: self.update_save_data(self.level.save_data) + pygame.quit() + sys.exit() + self.last_escape_time = current_time + + # Touche T pour le Menu des Talents + if self.game_state == 'PLAYING' and event.key == pygame.K_t: + self.level.show_talents = not self.level.show_talents + + # Menus d'armes et de magie supprimés : M et K ne font plus rien + + # Touche R pour recommencer après Game Over + if self.game_state == 'GAMEOVER' and event.key == pygame.K_r: + self.game_state = 'MENU' + + if self.game_state == 'MENU' and event.type == pygame.MOUSEBUTTONDOWN: + btn_new, btn_cont = self.draw_menu() + if btn_new.collidepoint(event.pos): + self.start_game(is_new=True) + elif btn_cont.collidepoint(event.pos): + self.start_game(is_new=False) + + # --- MISE À JOUR ET DESSIN --- + if self.game_state == 'MENU': + self.draw_menu() + + elif self.game_state == 'PLAYING': + self.screen.fill('black') + self.level.run() + + # Vérification de la mort du joueur + if self.level.player.health <= 0: + self.game_state = 'GAMEOVER' + + elif self.game_state == 'GAMEOVER': + self.draw_game_over() + + pygame.display.update() + self.clock.tick(FPS) + +if __name__ == '__main__': + game = Game() + game.run() \ No newline at end of file diff --git a/SECONDGAME/map/boss_level.txt b/SECONDGAME/map/boss_level.txt new file mode 100644 index 0000000..32685ba --- /dev/null +++ b/SECONDGAME/map/boss_level.txt @@ -0,0 +1,9 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +x x +x xxxx xxxx x +x x p x x +x B x +x x x x +x xxx xxxx x +x x +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/SECONDGAME/map/level1.txt b/SECONDGAME/map/level1.txt new file mode 100644 index 0000000..f094849 --- /dev/null +++ b/SECONDGAME/map/level1.txt @@ -0,0 +1,15 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +x p e x +x x xxxxxxxxxx x +x x x d x x +x e x x r x x +x x x d d x x +x xxxx xxxx x +x x +x xxxxxxxxx xxxxxxxxx +x x e x +x e x x +x x e s x +x x +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + diff --git a/SECONDGAME/map/level2.txt b/SECONDGAME/map/level2.txt new file mode 100644 index 0000000..836e443 --- /dev/null +++ b/SECONDGAME/map/level2.txt @@ -0,0 +1,14 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +x p x x x e x +x x x x x x +x x e x x e x x +x x x x x x +x e x x x x x +x xxxxxxxxxxx x +x x d d d x e e x +x x d d d x x +x xxxxx xxxx x +x x x x x +x e x x x s x +x x e x x x +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/SECONDGAME/map/level3.txt b/SECONDGAME/map/level3.txt new file mode 100644 index 0000000..a25ddf0 --- /dev/null +++ b/SECONDGAME/map/level3.txt @@ -0,0 +1,20 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +x p x e x +x xxxxxx x +x x xxxxxxxxxxxxxxxx x +x x e x d d d x x +x x x r x x +x x x d d d d x x +x xxxxxx xxxxxxx xxxxxxx x +x x x +x x xxxxxxxxxxxxxxxxxxxxx x +x x x e x x +x x s x xxxxxxxxxxxxxxxx x +x x x x x +x xxxxxxxx x e x x +x x x x +x xxxxxxxxxxxxx x x +x e e x +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/SECONDGAME/map/level4.txt b/SECONDGAME/map/level4.txt new file mode 100644 index 0000000..8a10f27 --- /dev/null +++ b/SECONDGAME/map/level4.txt @@ -0,0 +1,16 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +x p x x dd dd x x +x x s x x +x e x x dd dd x x +x xxxxxx xxx xxx x +x xxxxxxxxxxxxxx x +x d x e x +x xxxxxxxxxxxxxxxxxxx x +x x e x x +x x xxxxxxxxxxxx x +x x x dd dd d x +x x e x d r x e x +x x x dddd dd x x +x x xxxxx xxxxx x +x e x +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/SECONDGAME/map/level6.txt b/SECONDGAME/map/level6.txt new file mode 100644 index 0000000..f0b4f59 --- /dev/null +++ b/SECONDGAME/map/level6.txt @@ -0,0 +1,11 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +x x +x x +x x +x x +x x +x s x +x x +x x +x x +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ No newline at end of file diff --git a/SECONDGAME/player.py b/SECONDGAME/player.py new file mode 100644 index 0000000..7a2a1d1 --- /dev/null +++ b/SECONDGAME/player.py @@ -0,0 +1,286 @@ +import pygame +import math +from settings import * + +class Player(pygame.sprite.Sprite): + def __init__(self, pos, groups, obstacle_sprites, create_attack, destroy_attack, create_magic, save_data, weapon_type='sword'): + super().__init__(groups) + + # 1. Configuration de base de l'image (Transparence) + self.image = pygame.Surface((48, 48), pygame.SRCALPHA) + self.rect = self.image.get_rect(topleft=pos) + self.hitbox = self.rect.inflate(-6, -26) + + # 2. STATS (Chargées depuis la sauvegarde) + self.stats = save_data['stats'] + self.health = self.stats['health'] + self.energy = self.stats['magic'] + self.exp = save_data['exp'] + self.speed = self.stats['speed'] + self.rubies = save_data['rubies'] + self.sword_level = save_data['sword_level'] + + # 3. ÉTAT ET MOUVEMENT + self.status = 'down' + self.direction = pygame.math.Vector2() + self.attacking = False + self.attack_cooldown = 400 + self.attack_time = None + self.obstacle_sprites = obstacle_sprites + + # --- LOGIQUE BONUS 3 COEURS --- + self.life_hearts = save_data.get('life_hearts', 0) + self.vulnerable = True + self.hurt_time = None + self.invincibility_duration = 500 + + # 4. DESSIN INITIAL + self.draw_player_shape() + + # 5. MÉTHODES ET MAGIE + self.create_attack = create_attack + self.destroy_attack = destroy_attack + self.create_magic = create_magic + self.weapon_type = weapon_type if weapon_type else 'sword' + + # Système de magie simplifié (une seule magie : boule de feu) + self.can_cast = True + self.magic_time = None + self.magic_cooldown = 800 # Plus long pour éviter le spam + + # Invisibilité + self.invisible = False + + def get_direction_vector(self): + """Retourne un vecteur basé sur le statut actuel (Corrige l'AttributeError)""" + direction = self.status.split('_')[0] + if direction == 'right': return pygame.math.Vector2(1,0) + if direction == 'left': return pygame.math.Vector2(-1,0) + if direction == 'up': return pygame.math.Vector2(0,-1) + return pygame.math.Vector2(0,1) # down + + def draw_player_shape(self, weapon_type=None): + """Dessine un personnage RPG détaillé avec style Zelda-like.""" + if weapon_type: + self.weapon_type = weapon_type + self.image.fill((0, 0, 0, 0)) + + # Couleurs améliorées + CLOTHES = '#4169E1' # Royal Blue + SKIN = '#F5DEB3' # Wheat (warm skin tone) + HAIR = '#8B4513' # Saddle Brown + SHIRT = '#228B22' # Forest Green + BELT = '#8B4513' # Saddle Brown + BOOTS = '#654321' # Dark Brown + HIGHLIGHT = '#FFFFFF' # White for highlights + + # Effet visuel du bonus (clignotement doré) + if self.life_hearts >= 3: + if math.sin(pygame.time.get_ticks() / 50) >= 0: + CLOTHES = '#FFD700' # Gold + HIGHLIGHT = '#FFFF00' # Yellow highlight + + # Position des yeux selon la direction + eye_y = 11 + if 'down' in self.status: + left_eye_x, right_eye_x = 20, 28 + elif 'up' in self.status: + left_eye_x, right_eye_x = 20, 28 # Même position pour up + elif 'left' in self.status: + left_eye_x, right_eye_x = 18, 26 + elif 'right' in self.status: + left_eye_x, right_eye_x = 22, 30 + + # 1. Bottes (base du personnage) + pygame.draw.rect(self.image, BOOTS, (16, 38, 8, 6)) # Botte gauche + pygame.draw.rect(self.image, BOOTS, (24, 38, 8, 6)) # Botte droite + + # 2. Jambes (pantalon) + pygame.draw.rect(self.image, CLOTHES, (18, 28, 6, 12)) # Jambe gauche + pygame.draw.rect(self.image, CLOTHES, (24, 28, 6, 12)) # Jambe droite + + # 3. Ceinture + pygame.draw.rect(self.image, BELT, (14, 26, 20, 4)) + + # 4. Corps (Tunique) + pygame.draw.rect(self.image, SHIRT, (16, 18, 16, 10), border_radius=2) + # Col de la tunique + pygame.draw.rect(self.image, SHIRT, (18, 16, 12, 4), border_radius=1) + + # 5. Bras + pygame.draw.rect(self.image, SKIN, (12, 20, 4, 10)) # Bras gauche + pygame.draw.rect(self.image, SKIN, (32, 20, 4, 10)) # Bras droit + + # 5.5 Arme dans les bras (selon l'arme sélectionnée) + self.draw_weapon_in_hands() + + # 6. Tête (forme plus détaillée) + pygame.draw.ellipse(self.image, SKIN, (16, 6, 16, 14)) # Tête ovale + + # 7. Cheveux (style Zelda-like avec mèche) + pygame.draw.ellipse(self.image, HAIR, (14, 2, 20, 12)) # Chevelure principale + pygame.draw.ellipse(self.image, HAIR, (18, 0, 12, 8)) # Mèche frontale + + # 8. Yeux (plus détaillés) + # Pupilles + pygame.draw.circle(self.image, 'black', (left_eye_x, eye_y), 1) + pygame.draw.circle(self.image, 'black', (right_eye_x, eye_y), 1) + # Reflets blancs + pygame.draw.circle(self.image, 'white', (left_eye_x-1, eye_y-1), 1) + pygame.draw.circle(self.image, 'white', (right_eye_x-1, eye_y-1), 1) + + + # 11. Détails de la tunique (coutures) + pygame.draw.line(self.image, BELT, (20, 18), (20, 26), 1) # Couture centrale + pygame.draw.line(self.image, BELT, (16, 22), (30, 22), 1) # Ceinture horizontale + + # 12. Highlights sur les bottes + pygame.draw.line(self.image, HIGHLIGHT, (18, 40), (22, 40), 1) + pygame.draw.line(self.image, HIGHLIGHT, (26, 40), (30, 40), 1) + + def draw_weapon_in_hands(self): + """Dessine l'arme sélectionnée dans les mains du joueur""" + weapon = self.weapon_type if hasattr(self, 'weapon_type') else 'sword' + if weapon == 'sword': + # Épée dans la main droite (position verticale) + pygame.draw.rect(self.image, '#C0C0C0', (34, 18, 2, 12)) # Lame + pygame.draw.rect(self.image, '#8B4513', (33, 28, 4, 3)) # Garde + pygame.draw.circle(self.image, '#FFD700', (35, 16), 2) # Pompeau + + elif weapon == 'boomerang': + # Boomerang dans la main droite + pygame.draw.polygon(self.image, '#8B4513', [(34, 18), (38, 22), (34, 26), (30, 22)]) # Forme de boomerang + pygame.draw.circle(self.image, '#FFD700', (32, 22), 1) # Détail doré + + elif weapon == 'bomb': + # Bombe dans la main gauche + pygame.draw.circle(self.image, '#000000', (10, 22), 3) # Corps de la bombe + pygame.draw.circle(self.image, '#8B0000', (10, 22), 2) # Liquide rouge + pygame.draw.line(self.image, '#FFFFFF', (10, 20), (10, 19), 1) # Mèche + + elif weapon == 'bow': + # Arc dans les deux mains + pygame.draw.arc(self.image, '#8B4513', (8, 15, 24, 16), 0.3, 2.8, 2) # Arc courbé + pygame.draw.line(self.image, '#FFFFFF', (20, 17), (20, 27), 1) # Corde + + elif weapon == 'shotgun': + # Fusil à pompe dans les deux mains + pygame.draw.rect(self.image, '#654321', (8, 20, 8, 3)) # Corps du fusil + pygame.draw.rect(self.image, '#2F4F4F', (16, 19, 6, 5)) # Canon + pygame.draw.circle(self.image, '#FFD700', (8, 22), 1) # Détail métallique + + def input(self): + if not self.attacking: + keys = pygame.key.get_pressed() + + # Déplacement + if keys[pygame.K_UP]: + self.direction.y = -1 + self.status = 'up' + elif keys[pygame.K_DOWN]: + self.direction.y = 1 + self.status = 'down' + else: + self.direction.y = 0 + + if keys[pygame.K_RIGHT]: + self.direction.x = 1 + self.status = 'right' + elif keys[pygame.K_LEFT]: + self.direction.x = -1 + self.status = 'left' + else: + self.direction.x = 0 + + # Attaque Épée + if keys[pygame.K_SPACE]: + self.attacking = True + self.attack_time = pygame.time.get_ticks() + self.create_attack() + + # Attaque Magique - simple boule de feu + if keys[pygame.K_LCTRL] and self.can_cast: + self.attacking = True + self.attack_time = pygame.time.get_ticks() + self.create_magic('fireball', 15, 20) # force/cout de base + + def get_damage(self, amount, attack_type=None): + + """Version ultra-sécurisée contre les inversions d'arguments""" + + # 2. Correction dynamique de l'erreur TypeError + # Si 'amount' est l'objet Player et 'attack_type' est un nombre + if not isinstance(amount, (int, float)) and isinstance(attack_type, (int, float)): + actual_damage = attack_type + # Si 'amount' est un nombre (cas normal) + elif isinstance(amount, (int, float)): + actual_damage = amount + # Si aucun n'est un nombre (sécurité finale) + else: + actual_damage = 10 + + # 3. Application des dégâts + if self.vulnerable: + self.health -= actual_damage + self.vulnerable = False + self.hurt_time = pygame.time.get_ticks() + + # Reset Bonus 3 Coeurs + if self.life_hearts >= 3: + self.life_hearts = 0 + self.sword_level = 1 + + def move(self, speed): + if self.direction.magnitude() != 0: + self.direction = self.direction.normalize() + + self.hitbox.x += self.direction.x * speed + self.collision('horizontal') + self.hitbox.y += self.direction.y * speed + self.collision('vertical') + self.rect.center = self.hitbox.center + + def collision(self, direction): + if direction == 'horizontal': + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if self.direction.x > 0: self.hitbox.right = sprite.hitbox.left + if self.direction.x < 0: self.hitbox.left = sprite.hitbox.right + + if direction == 'vertical': + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if self.direction.y > 0: self.hitbox.bottom = sprite.hitbox.top + if self.direction.y < 0: self.hitbox.top = sprite.hitbox.bottom + + def cooldowns(self): + current_time = pygame.time.get_ticks() + if self.attacking: + if current_time - self.attack_time >= self.attack_cooldown: + self.attacking = False + self.destroy_attack() + + if not self.vulnerable: + if current_time - self.hurt_time >= self.invincibility_duration: + self.vulnerable = True + + def energy_recovery(self): + if self.energy < self.stats['magic']: + self.energy += 0.2 # Régénération plus rapide (0.2 au lieu de 0.05) + else: + self.energy = self.stats['magic'] + + def update(self): + self.input() + self.cooldowns() + self.energy_recovery() + + # Gestion de l'invisibilité (Alpha) + if self.invisible: + self.image.set_alpha(100) + else: + self.image.set_alpha(255) + + # Redessiner pour mettre à jour le regard et l'effet de clignotement + self.draw_player_shape() + self.move(self.speed) \ No newline at end of file diff --git a/SECONDGAME/sav1.txt b/SECONDGAME/sav1.txt new file mode 100644 index 0000000..cc26be4 --- /dev/null +++ b/SECONDGAME/sav1.txt @@ -0,0 +1,337 @@ +import pygame + +from settings import * + + + +class Player(pygame.sprite.Sprite): + + def __init__(self, pos, groups, obstacle_sprites, create_attack, destroy_attack, create_magic, save_data): + + super().__init__(groups) + + + + # 1. Configuration de base de l'image (Transparence) + + self.image = pygame.Surface((48, 48), pygame.SRCALPHA) + + self.rect = self.image.get_rect(topleft=pos) + + self.hitbox = self.rect.inflate(-6, -26) + + + + # 2. STATS (Chargées depuis la sauvegarde) + + self.stats = save_data['stats'] + + self.health = self.stats['health'] + + self.energy = self.stats['magic'] + + self.exp = save_data['exp'] + + self.speed = self.stats['speed'] + + self.rubies = save_data['rubies'] + + self.sword_level = save_data['sword_level'] + + + + # 3. ÉTAT ET MOUVEMENT (Définis AVANT le dessin pour éviter l'AttributeError) + + self.status = 'down' + + self.direction = pygame.math.Vector2() + + self.attacking = False + + self.attack_cooldown = 400 + + self.attack_time = None + + self.obstacle_sprites = obstacle_sprites + + + + # 4. DESSIN INITIAL (Maintenant que status existe) + + self.draw_player_shape() + + + + # 5. MÉTHODES ET MAGIE + + self.create_attack = create_attack + + self.destroy_attack = destroy_attack + + self.create_magic = create_magic + + + + self.magic_index = 0 + + self.magic_list = list(MAGIC_DATA.keys()) + + self.can_cast = True + + self.magic_time = None + + self.magic_cooldown = 500 + + + + # Invisibilité + + self.invisible = False + + + + def draw_player_shape(self): + + """Dessine un personnage humanoïde détaillé sans fichiers images.""" + + self.image.fill((0, 0, 0, 0)) # Efface avec transparence totale + + + + # Couleurs + + CLOTHES = 'blue' + + SKIN = '#FFCC99' + + HAIR = '#663300' + + + + # 1. Corps (Torse) + + pygame.draw.rect(self.image, CLOTHES, (14, 20, 20, 22), border_radius=3) + + + + # 2. Tête + + pygame.draw.circle(self.image, SKIN, (24, 14), 10) + + + + # 3. Cheveux (Petit dôme) + + pygame.draw.arc(self.image, HAIR, (14, 4, 20, 15), 0, 3.14, 8) + + + + # 4. Yeux (Adaptés selon self.status) + + eye_y = 12 + + if 'down' in self.status: + + pygame.draw.circle(self.image, 'black', (20, eye_y), 2) + + pygame.draw.circle(self.image, 'black', (28, eye_y), 2) + + elif 'up' in self.status: + + pass # Dos du personnage + + elif 'left' in self.status: + + pygame.draw.circle(self.image, 'black', (18, eye_y), 2) + + elif 'right' in self.status: + + pygame.draw.circle(self.image, 'black', (30, eye_y), 2) + + + + # 5. Bras (Animation statique simple) + + pygame.draw.rect(self.image, SKIN, (8, 22, 6, 12)) # Gauche + + pygame.draw.rect(self.image, SKIN, (34, 22, 6, 12)) # Droite + + + + def input(self): + + if not self.attacking: + + keys = pygame.key.get_pressed() + + + + # Déplacement + + if keys[pygame.K_UP]: + + self.direction.y = -1 + + self.status = 'up' + + elif keys[pygame.K_DOWN]: + + self.direction.y = 1 + + self.status = 'down' + + else: + + self.direction.y = 0 + + + + if keys[pygame.K_RIGHT]: + + self.direction.x = 1 + + self.status = 'right' + + elif keys[pygame.K_LEFT]: + + self.direction.x = -1 + + self.status = 'left' + + else: + + self.direction.x = 0 + + + + # Attaque Épée + + if keys[pygame.K_SPACE]: + + self.attacking = True + + self.attack_time = pygame.time.get_ticks() + + self.create_attack() + + + + # Attaque Magique + + if keys[pygame.K_LCTRL] and self.can_cast: + + self.attacking = True + + self.attack_time = pygame.time.get_ticks() + + style = self.magic_list[self.magic_index] + + strength = MAGIC_DATA[style]['strength'] + self.stats['attack'] + + cost = MAGIC_DATA[style]['cost'] + + self.create_magic(style, strength, cost) + + + + def move(self, speed): + + if self.direction.magnitude() != 0: + + self.direction = self.direction.normalize() + + + + self.hitbox.x += self.direction.x * speed + + self.collision('horizontal') + + self.hitbox.y += self.direction.y * speed + + self.collision('vertical') + + self.rect.center = self.hitbox.center + + + + def collision(self, direction): + + if direction == 'horizontal': + + for sprite in self.obstacle_sprites: + + if sprite.hitbox.colliderect(self.hitbox): + + if self.direction.x > 0: self.hitbox.right = sprite.hitbox.left + + if self.direction.x < 0: self.hitbox.left = sprite.hitbox.right + + + + if direction == 'vertical': + + for sprite in self.obstacle_sprites: + + if sprite.hitbox.colliderect(self.hitbox): + + if self.direction.y > 0: self.hitbox.bottom = sprite.hitbox.top + + if self.direction.y < 0: self.hitbox.top = sprite.hitbox.bottom + + + + def cooldowns(self): + + current_time = pygame.time.get_ticks() + + if self.attacking: + + if current_time - self.attack_time >= self.attack_cooldown: + + self.attacking = False + + self.destroy_attack() + + + + def energy_recovery(self): + + if self.energy < self.stats['magic']: + + self.energy += 0.05 + + else: + + self.energy = self.stats['magic'] + + + + def update(self): + + # L'ordre ici est important : on récupère l'input, on bouge, puis on redessine + + self.input() + + self.cooldowns() + + self.energy_recovery() + + + + # Effet d'invisibilité + + if self.invisible: + + self.image.set_alpha(100) + + else: + + self.image.set_alpha(255) + + + + # On redessine pour que le regard suive la direction + + self.draw_player_shape() + + self.move(self.speed) \ No newline at end of file diff --git a/SECONDGAME/sav2.txt b/SECONDGAME/sav2.txt new file mode 100644 index 0000000..f811e06 --- /dev/null +++ b/SECONDGAME/sav2.txt @@ -0,0 +1,134 @@ +import pygame +from settings import * + +class Player(pygame.sprite.Sprite): + def __init__(self, pos, groups, obstacle_sprites, create_attack, destroy_attack, create_magic, save_data): + super().__init__(groups) + + # 1. CHARGEMENT ET REDIMENSIONNEMENT DE TON IMAGE + try: + # Assure-toi que le fichier s'appelle link.png dans ce dossier + self.image = pygame.image.load('graphics/player/link.png').convert_alpha() + # Redimensionnement aux dimensions demandées : 55x64 + self.image = pygame.transform.scale(self.image, (55, 64)) + except: + # Image de secours si le fichier n'est pas trouvé + self.image = pygame.Surface((55, 64)) + self.image.fill('green') + print("Erreur : Le fichier 'graphics/player/link.png' est introuvable.") + + self.rect = self.image.get_rect(topleft=pos) + self.hitbox = self.rect.inflate(-10, -10) + + # 2. STATS (Correction du crash rubies inclus) + self.stats = save_data['stats'] + self.health = self.stats['health'] + self.energy = self.stats['magic'] # Réservoir pour la magie + self.exp = save_data['exp'] + self.speed = self.stats['speed'] + self.rubies = save_data.get('rubies', 0) + + # 3. MOUVEMENT ET ÉTATS + self.direction = pygame.math.Vector2() + self.status = 'down' + self.attacking = False + self.attack_cooldown = 400 + self.attack_time = None + self.obstacle_sprites = obstacle_sprites + + # 4. FONCTIONS DE COMBAT + self.create_attack = create_attack + self.destroy_attack = destroy_attack + self.create_magic = create_magic + + # Cooldown spécifique à la magie + self.can_cast = True + self.magic_time = None + self.magic_cooldown = 600 + + def input(self): + if not self.attacking: + keys = pygame.key.get_pressed() + + # --- Déplacements --- + if keys[pygame.K_UP]: + self.direction.y = -1 + self.status = 'up' + elif keys[pygame.K_DOWN]: + self.direction.y = 1 + self.status = 'down' + else: + self.direction.y = 0 + + if keys[pygame.K_RIGHT]: + self.direction.x = 1 + self.status = 'right' + elif keys[pygame.K_LEFT]: + self.direction.x = -1 + self.status = 'left' + else: + self.direction.x = 0 + + # --- Attaque à l'épée (Espace) --- + if keys[pygame.K_SPACE]: + self.attacking = True + self.attack_time = pygame.time.get_ticks() + self.create_attack() + + # --- Utilisation de la MAGIE (Ctrl Gauche) --- + if keys[pygame.K_LCTRL] and self.can_cast: + # Coût de la magie (ex: 20 points) + magic_cost = 20 + if self.energy >= magic_cost: + self.attacking = True + self.can_cast = False + self.attack_time = pygame.time.get_ticks() + self.magic_time = pygame.time.get_ticks() + + # Déclenchement du sort + self.create_magic('flame', 15, magic_cost) + self.energy -= magic_cost # Consommation de l'énergie + + def cooldowns(self): + current_time = pygame.time.get_ticks() + + # Cooldown attaque physique + if self.attacking: + if current_time - self.attack_time >= self.attack_cooldown: + self.attacking = False + self.destroy_attack() + + # Cooldown régénération magie + if not self.can_cast: + if current_time - self.magic_time >= self.magic_cooldown: + self.can_cast = True + + def move(self, speed): + if self.direction.magnitude() != 0: + self.direction = self.direction.normalize() + + self.hitbox.x += self.direction.x * speed + self.collision('horizontal') + self.hitbox.y += self.direction.y * speed + self.collision('vertical') + self.rect.center = self.hitbox.center + + def collision(self, direction): + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if direction == 'horizontal': + if self.direction.x > 0: self.hitbox.right = sprite.hitbox.left + if self.direction.x < 0: self.hitbox.left = sprite.hitbox.right + if direction == 'vertical': + if self.direction.y > 0: self.hitbox.bottom = sprite.hitbox.top + if self.direction.y < 0: self.hitbox.top = sprite.hitbox.bottom + + def update(self): + self.input() + self.cooldowns() + + # Récupération automatique et lente de l'énergie magique + if self.energy < self.stats['magic']: + self.energy += 0.1 + + self.move(self.speed) \ No newline at end of file diff --git a/SECONDGAME/sav3.txt b/SECONDGAME/sav3.txt new file mode 100644 index 0000000..37dc5b2 --- /dev/null +++ b/SECONDGAME/sav3.txt @@ -0,0 +1,194 @@ +import pygame +from settings import * +from tile import Tile +from player import Player +from support import * +from weapon import Weapon +from enemy import Enemy +from ui import UI +from magic import MagicPlayer +import random + +class Level: + def __init__(self, save_data, change_level): + # Surface d'affichage + self.display_surface = pygame.display.get_surface() + + # États du menu + self.show_talents = False + self.show_help = False + + # Groupes de sprites + self.visible_sprites = YSortCameraGroup() + self.obstacle_sprites = pygame.sprite.Group() + + # Sprites d'attaque et cibles + self.current_attack = None + self.attack_sprites = pygame.sprite.Group() + self.attackable_sprites = pygame.sprite.Group() + + # Données de progression + self.save_data = save_data + self.level_number = save_data['current_level'] + self.change_level = change_level + + # Création de la carte + self.create_map() + + # UI et Magie + self.ui = UI() + self.magic_player = MagicPlayer(None) + + def create_map(self): + layout = import_csv_layout(LEVEL_MAPS[self.level_number]) + + for row_index, row in enumerate(layout): + for col_index, col in enumerate(row): + x = col_index * TILESIZE + y = row_index * TILESIZE + + if col == 'x': + Tile((x,y), [self.visible_sprites, self.obstacle_sprites], 'wall') + if col == 'b': + Tile((x,y), [self.visible_sprites, self.obstacle_sprites, self.attackable_sprites], 'grass') + if col == 'p': + self.player = Player( + (x,y), [self.visible_sprites], + self.obstacle_sprites, + self.create_attack, + self.destroy_attack, + self.create_magic, + self.save_data) + if col == 'e': + monster_name = 'raccoon' if self.level_number > 2 else 'squid' + Enemy(monster_name, (x,y), [self.visible_sprites, self.attackable_sprites], + self.obstacle_sprites, self.damage_player, self.trigger_death_particles) + if col == 'B': + Enemy('boss', (x,y), [self.visible_sprites, self.attackable_sprites], + self.obstacle_sprites, self.damage_player, self.trigger_death_particles) + + # --- LOGIQUE DE COMBAT --- + def create_attack(self): + self.current_attack = Weapon(self.player, [self.visible_sprites, self.attack_sprites]) + + def create_magic(self, style, strength, cost): + if style == 'fireball': + self.magic_player.fireball(self.player, cost, [self.visible_sprites, self.attack_sprites], self.obstacle_sprites, self.attackable_sprites) + elif style == 'freeze': + self.magic_player.freeze(self.player, cost, self.attackable_sprites) + elif style == 'invisibility': + self.magic_player.invisibility(self.player, cost) + + def destroy_attack(self): + if self.current_attack: self.current_attack.kill() + self.current_attack = None + + def player_attack_logic(self): + if self.attack_sprites: + for attack_sprite in self.attack_sprites: + # On cherche les collisions avec les sprites vulnérables (ennemis) + collision_sprites = pygame.sprite.spritecollide(attack_sprite, self.attackable_sprites, False) + if collision_sprites: + for target_sprite in collision_sprites: + # On vérifie que c'est bien un ennemi et non un mur/herbe + if target_sprite.sprite_type == 'enemy': + target_sprite.get_damage(self.player, attack_sprite.sprite_type) + # Optionnel : si tu veux que l'herbe disparaisse quand on la coupe + elif target_sprite.sprite_type == 'grass': + target_sprite.kill() + + def damage_player(self, amount, attack_type): + if not self.player.invisible: + self.player.health -= amount + if self.player.health <= 0: + self.player.kill() + + def trigger_death_particles(self, pos, particle_type): + pass + + # --- PROGRESSION ET SORTIE --- + def create_exit_stair(self): + if not hasattr(self, 'exit_sprite'): + # L'escalier apparaît au centre ou à une position libre + self.exit_sprite = Tile((WIDTH//2, HEIGHT//2), [self.visible_sprites], 'exit') + + def check_exit_collision(self): + if hasattr(self, 'exit_sprite'): + if self.player.hitbox.colliderect(self.exit_sprite.hitbox): + self.next_level() + + def next_level(self): + self.save_data['current_level'] += 1 + # On met à jour les stats du joueur dans la sauvegarde avant de changer + self.save_data['stats']['health'] = self.player.stats['health'] + self.save_data['exp'] = self.player.exp + self.save_data['rubies'] = self.player.rubies + self.change_level(self.save_data) + + def victory(self): + self.save_data['sword_level'] += 1 + self.save_data['current_level'] = 1 + self.change_level(self.save_data) + + def input_talents(self): + keys = pygame.key.get_pressed() + available_pts = self.player.exp // 500 + if available_pts > 0: + if keys[pygame.K_1]: + self.player.stats['health'] += 20 + self.player.exp -= 500 + elif keys[pygame.K_2]: + self.player.stats['attack'] += 5 + self.player.exp -= 500 + elif keys[pygame.K_3]: + self.player.speed += 1 + self.player.exp -= 500 + + # --- BOUCLE PRINCIPALE DU NIVEAU --- + def run(self): + self.visible_sprites.custom_draw(self.player) + self.ui.display(self.player) + + # Compteur d'ennemis (on exclut les buissons) + enemies = [s for s in self.attackable_sprites if s.sprite_type == 'enemy'] + enemy_count = len(enemies) + self.ui.show_enemy_count(enemy_count) + + # Logique de sortie + if enemy_count == 0 and self.level_number < 5: + self.create_exit_stair() + self.check_exit_collision() + + # Gestion des menus (Pause le jeu si affichés) + keys = pygame.key.get_pressed() + if keys[pygame.K_h]: + self.ui.display_help() + elif self.show_talents: + self.ui.display_talent_menu(self.player) + self.input_talents() + else: + # Mise à jour du monde + self.visible_sprites.update() + self.visible_sprites.enemy_update(self.player) + self.player_attack_logic() + +class YSortCameraGroup(pygame.sprite.Group): + def __init__(self): + super().__init__() + self.display_surface = pygame.display.get_surface() + self.half_width = self.display_surface.get_size()[0] // 2 + self.half_height = self.display_surface.get_size()[1] // 2 + self.offset = pygame.math.Vector2() + + def custom_draw(self, player): + self.offset.x = player.rect.centerx - self.half_width + self.offset.y = player.rect.centery - self.half_height + + for sprite in sorted(self.sprites(), key=lambda sprite: sprite.rect.centery): + offset_pos = sprite.rect.topleft - self.offset + self.display_surface.blit(sprite.image, offset_pos) + + def enemy_update(self, player): + enemy_sprites = [sprite for sprite in self.sprites() if hasattr(sprite, 'sprite_type') and sprite.sprite_type == 'enemy'] + for enemy in enemy_sprites: + enemy.enemy_update(player) \ No newline at end of file diff --git a/SECONDGAME/save.txt b/SECONDGAME/save.txt new file mode 100644 index 0000000..cad802e --- /dev/null +++ b/SECONDGAME/save.txt @@ -0,0 +1,171 @@ +import pygame +from settings import * + +class Player(pygame.sprite.Sprite): + def __init__(self, pos, groups, obstacle_sprites, create_attack, destroy_attack, create_magic, save_data): + super().__init__(groups) + + # 1. Configuration de base de l'image (Transparence) + self.image = pygame.Surface((48, 48), pygame.SRCALPHA) + self.rect = self.image.get_rect(topleft=pos) + self.hitbox = self.rect.inflate(-6, -26) + + # 2. STATS (Chargées depuis la sauvegarde) + self.stats = save_data['stats'] + self.health = self.stats['health'] + self.energy = self.stats['magic'] + self.exp = save_data['exp'] + self.speed = self.stats['speed'] + self.rubies = save_data['rubies'] + self.sword_level = save_data['sword_level'] + + # 3. ÉTAT ET MOUVEMENT (Définis AVANT le dessin pour éviter l'AttributeError) + self.status = 'down' + self.direction = pygame.math.Vector2() + self.attacking = False + self.attack_cooldown = 400 + self.attack_time = None + self.obstacle_sprites = obstacle_sprites + + # 4. DESSIN INITIAL (Maintenant que status existe) + self.draw_player_shape() + + # 5. MÉTHODES ET MAGIE + self.create_attack = create_attack + self.destroy_attack = destroy_attack + self.create_magic = create_magic + + self.magic_index = 0 + self.magic_list = list(MAGIC_DATA.keys()) + self.can_cast = True + self.magic_time = None + self.magic_cooldown = 500 + + # Invisibilité + self.invisible = False + + def draw_player_shape(self): + """Dessine un personnage style Link 8-bit avec des formes.""" + # Le sprite original fait 16x16 pixels. On va l'agrandir pour le jeu. + self.pixel_size = 3 + self.image = pygame.Surface((16 * self.pixel_size, 16 * self.pixel_size), pygame.SRCALPHA) + self.hitbox = self.rect.inflate(-6, -10) + + # Couleurs extraites de ton image + self.colors = { + 'G': '#84D04D', # Vert clair (Tunique/Bonnet) + 'B': '#8C5424', # Brun (Bottes/Cheveux/Ceinture) + 'S': '#FFA011', # Peau / Orange (Visage/Mains) + 'W': '#FFFFFF', # Blanc (Yeux - optionnel) + '.': (0,0,0,0) # Transparent + } + + # Définition du sprite (Grille 16x16 identique à ton image) + self.link_map = [ + "....GGGGGG......", + "...GGGGGGGGGG...", + "..GGGGGGGGGGGG..", + "..GGGGGBBBBBGG..", + "..GGGBBSSSSSBG..", + "..GGBBSSSSSSSBG.", + "..GGBSSSSSSSSSB.", + "..GGBSSSSSSBSSB.", + "...BSSSSSSSBSSB.", + "...BSSBBSSSSSSB.", + "...BBSSSSSSSBG..", + "..GGBBSSSSSGG...", + "..GGGGGGGGGG....", + ".BBGGGGGGGGGG...", + "BBBB..BBBBBB....", + "BBBB...BBBB....." + ] + + def input(self): + if not self.attacking: + keys = pygame.key.get_pressed() + + # Déplacement + if keys[pygame.K_UP]: + self.direction.y = -1 + self.status = 'up' + elif keys[pygame.K_DOWN]: + self.direction.y = 1 + self.status = 'down' + else: + self.direction.y = 0 + + if keys[pygame.K_RIGHT]: + self.direction.x = 1 + self.status = 'right' + elif keys[pygame.K_LEFT]: + self.direction.x = -1 + self.status = 'left' + else: + self.direction.x = 0 + + # Attaque Épée + if keys[pygame.K_SPACE]: + self.attacking = True + self.attack_time = pygame.time.get_ticks() + self.create_attack() + + # Attaque Magique + if keys[pygame.K_LCTRL] and self.can_cast: + self.attacking = True + self.attack_time = pygame.time.get_ticks() + style = self.magic_list[self.magic_index] + strength = MAGIC_DATA[style]['strength'] + self.stats['attack'] + cost = MAGIC_DATA[style]['cost'] + self.create_magic(style, strength, cost) + + def move(self, speed): + if self.direction.magnitude() != 0: + self.direction = self.direction.normalize() + + self.hitbox.x += self.direction.x * speed + self.collision('horizontal') + self.hitbox.y += self.direction.y * speed + self.collision('vertical') + self.rect.center = self.hitbox.center + + def collision(self, direction): + if direction == 'horizontal': + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if self.direction.x > 0: self.hitbox.right = sprite.hitbox.left + if self.direction.x < 0: self.hitbox.left = sprite.hitbox.right + + if direction == 'vertical': + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if self.direction.y > 0: self.hitbox.bottom = sprite.hitbox.top + if self.direction.y < 0: self.hitbox.top = sprite.hitbox.bottom + + def cooldowns(self): + current_time = pygame.time.get_ticks() + if self.attacking: + if current_time - self.attack_time >= self.attack_cooldown: + self.attacking = False + self.destroy_attack() + + def energy_recovery(self): + if self.energy < self.stats['magic']: + self.energy += 0.05 + else: + self.energy = self.stats['magic'] + + def update(self): + # L'ordre ici est important : on récupère l'input, on bouge, puis on redessine + self.input() + self.cooldowns() + self.energy_recovery() + + # Effet d'invisibilité + if self.invisible: + self.image.set_alpha(100) + else: + self.image.set_alpha(255) + + # On redessine pour que le regard suive la direction + self.draw_player_shape() + self.move(self.speed) \ No newline at end of file diff --git a/SECONDGAME/save_data.json b/SECONDGAME/save_data.json new file mode 100644 index 0000000..96bbfd5 --- /dev/null +++ b/SECONDGAME/save_data.json @@ -0,0 +1,19 @@ +{ + "player_name": "Link", + "current_level": 4, + "rubies": 9, + "exp": 4700, + "talent_points": 0, + "sword_level": 1, + "stats": { + "health": 100, + "energy": 60, + "attack": 10, + "speed": 5, + "magic": 50 + }, + "unlocked_spells": [ + "fireball" + ], + "life_hearts": 2 +} \ No newline at end of file diff --git a/SECONDGAME/save_manager.py b/SECONDGAME/save_manager.py new file mode 100644 index 0000000..9732ea9 --- /dev/null +++ b/SECONDGAME/save_manager.py @@ -0,0 +1,50 @@ +import json +import os + +class SaveManager: + def __init__(self): + self.save_filename = "save_data.json" + # Statistiques de base par défaut pour une nouvelle partie + self.default_data = { + "player_name": "Link", + "current_level": 1, + "rubies": 0, + "exp": 0, + "talent_points": 0, + "sword_level": 1, + "stats": { + "health": 100, + "energy": 60, + "attack": 10, + "speed": 5, + "magic": 50 + }, + "unlocked_spells": ["fireball"] + } + + def save(self, data): + """Sauvegarde les données au format JSON.""" + try: + with open(self.save_filename, 'w') as f: + json.dump(data, f, indent=4) + print("Partie sauvegardée automatiquement.") + except Exception as e: + print(f"Erreur lors de la sauvegarde : {e}") + + def load(self): + """Charge les données. Si le fichier n'existe pas, retourne les données par défaut.""" + if not os.path.exists(self.save_filename): + print("Aucune sauvegarde trouvée. Chargement des données par défaut.") + return self.default_data + + try: + with open(self.save_filename, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Erreur lors du chargement : {e}") + return self.default_data + + def delete_save(self): + """Supprime la sauvegarde pour recommencer à zéro.""" + if os.path.exists(self.save_filename): + os.remove(self.save_filename) \ No newline at end of file diff --git a/SECONDGAME/settings.py b/SECONDGAME/settings.py new file mode 100644 index 0000000..8581b3c --- /dev/null +++ b/SECONDGAME/settings.py @@ -0,0 +1,59 @@ +import pygame + +# --- CONFIGURATION GÉNÉRALE --- +WIDTH = 1280 +HEIGHT = 720 +FPS = 60 +TILESIZE = 64 + +# --- INTERFACE UTILISATEUR (UI) --- +BAR_HEIGHT = 20 +HEALTH_BAR_WIDTH = 200 +ENERGY_BAR_WIDTH = 140 +UI_FONT = None +UI_FONT_SIZE = 18 + +# Couleurs +UI_BG_COLOR = '#222222' +UI_BORDER_COLOR = '#111111' +TEXT_COLOR = '#EEEEEE' +HEALTH_COLOR = '#ed3b3b' +ENERGY_COLOR = '#2662e2' +UI_BORDER_COLOR_ACTIVE = 'gold' + +# --- ÉQUILIBRAGE DU JEU --- +# Prix de la boutique +SHOP_PRICES = { + 'health_upgrade': 100, + 'strength_upgrade': 150, + 'speed_upgrade': 200, + 'magic_potion': 50 +} + +# Statistiques de la magie : on ne garde que la boule de feu de base +MAGIC_DATA = { + 'fireball': {'strength': 15, 'cost': 20}, +} + +# Données des armes : uniquement l'épée +WEAPON_DATA = { + 'sword': {'price': 0, 'damage': 25, 'unlocked': True}, # Gratuit et disponible dès le début +} + +# --- SYSTÈME DE NIVEAUX --- +# Liste des fichiers de cartes pour chaque niveau +LEVEL_MAPS = { + 1: 'map/level1.txt', + 2: 'map/level2.txt', + 3: 'map/level3.txt', + 4: 'map/level4.txt', + 5: 'map/boss_level.txt', + 6: 'map/level6.txt' +} + +# --- DONNÉES MONSTRES --- +MONSTER_DATA = { + 'squid': {'health': 100, 'exp': 100, 'damage': 10, 'speed': 3, 'resistance': 3, 'notice_radius': 400}, + 'raccoon': {'health': 250, 'exp': 250, 'damage': 30, 'speed': 4, 'resistance': 5, 'notice_radius': 500}, + 'boss': {'health': 2000, 'exp': 5000, 'damage': 50, 'speed': 2, 'resistance': 20, 'notice_radius': 1000} +} \ No newline at end of file diff --git a/SECONDGAME/support.py b/SECONDGAME/support.py new file mode 100644 index 0000000..b2b03bb --- /dev/null +++ b/SECONDGAME/support.py @@ -0,0 +1,46 @@ +import pygame +from os import walk + +def import_csv_layout(path): + """ + Lit un fichier texte (.txt ou .csv) représentant la carte + et le transforme en une liste utilisable. + """ + terrain_map = [] + try: + with open(path) as level_map: + for row in level_map: + # On nettoie la ligne et on sépare par virgule ou caractère + # Si vous utilisez des espaces, remplacez ',' par None + layout = list(row.strip().replace(',', '')) + terrain_map.append(layout) + return terrain_map + except FileNotFoundError: + print(f"Erreur : Le fichier {path} est introuvable.") + return [] + +def import_folder(path): + """ + Parcourt un dossier et charge toutes les images qu'il contient. + Utile pour les animations du joueur et des ennemis. + """ + surface_list = [] + for _, __, img_files in walk(path): + for image in sorted(img_files): + full_path = path + '/' + image + image_surf = pygame.image.load(full_path).convert_alpha() + surface_list.append(image_surf) + return surface_list + +def import_folder_dict(path): + """ + Charge les images d'un dossier sous forme de dictionnaire. + Clé = nom du fichier sans extension. + """ + surface_dict = {} + for _, __, img_files in walk(path): + for image in img_files: + full_path = path + '/' + image + image_surf = pygame.image.load(full_path).convert_alpha() + surface_dict[image.split('.')[0]] = image_surf + return surface_dict \ No newline at end of file diff --git a/SECONDGAME/tile.py b/SECONDGAME/tile.py new file mode 100644 index 0000000..7e84b13 --- /dev/null +++ b/SECONDGAME/tile.py @@ -0,0 +1,153 @@ +import pygame +from settings import * + +class Tile(pygame.sprite.Sprite): + def __init__(self, pos, groups, sprite_type='wall'): + super().__init__(groups) + self.sprite_type = sprite_type + + # Taille pour correspondre à Link (16x16 pixels d'origine agrandis 4 fois) + self.pixel_size = 4 + self.image = pygame.Surface((16 * self.pixel_size, 16 * self.pixel_size), pygame.SRCALPHA) + + self.rect = self.image.get_rect(topleft=pos) + + # Gestion des collisions : les murs bloquent, le reste non + if self.sprite_type == 'wall': + self.hitbox = self.rect.copy() + else: + # Hitbox minuscule au centre pour les objets non-solides (herbe, récompense, escalier) + self.hitbox = self.rect.inflate(-self.rect.width, -self.rect.height) + + self.draw_tile() + + def draw_tile(self): + """Dessine la tuile pixel par pixel selon son type.""" + self.image.fill((0, 0, 0, 0)) + + # Initialisation pour éviter les erreurs si sprite_type n'est pas reconnu + colors = {} + tile_map = [] + + # --- DESIGN DU MUR (Briques de Donjon NES) --- + if self.sprite_type == 'wall': + colors = { + 'D': '#404040', # Gris très foncé (Ombre) + 'L': '#A0A0A0', # Gris clair (Lumière) + 'M': '#808080' # Gris moyen + } + tile_map = [ + "LLLLLLLLLLLLLLLL", + "LMMMMMMMMMMMMMMD", + "LMMMMMMMMMMMMMMD", + "LMMMMMMMMMMMMMMD", + "LDDDDDDDDDDDDDDD", + "LMMMMMDLMMMMMMMD", + "LMMMMMDLMMMMMMMD", + "LMMMMMDLMMMMMMMD", + "LDDDDDDLDDDDDDDD", + "LLLLLLLLLLLLLLLL", + "LMMMMMMMMMMMMMMD", + "LMMMMMMMMMMMMMMD", + "LMMMMMMMMMMMMMMD", + "LDDDDDDDDDDDDDDD", + "LLLLLLLLLLLLLLLL", + "DDDDDDDDDDDDDDDD" + ] + + # --- DESIGN DE L'HERBE (Zelda 1 NES) --- + elif self.sprite_type == 'grass': + colors = { + 'G': '#1E7610', # Vert base + 'L': '#ACDF87', # Vert clair (Brin d'herbe) + 'D': '#114408' # Vert sombre (Ombre) + } + tile_map = [ + "GGGGGGGGGGGGGGGG", + "GGGGGGGGGGGGGGGG", + "GGGLGGGGGGGLGGGG", + "GGLLDGGGGGLLDGGG", + "GGLDDGGGGGLDDGGG", + "GGGGGGGGGGGGGGGG", + "GGGGGGGGGGGGGGGG", + "GGGGGGGGGGGGGGGG", + "GLGGGGGGGLGGGGGG", + "LLDGGGGGLLDGGGGG", + "LDDGGGGGLDDGGGGG", + "GGGGGGGGGGGGGGGG", + "GGGGGGGGGGGGGGGG", + "GGGGGGGGGGGGGGGG", + "GGGGGGGGGGGGGGGG", + "GGGGGGGGGGGGGGGG" + ] + + # --- DESIGN DE L'ESCALIER (Effet de trou/profondeur) --- + elif self.sprite_type == 'exit': + colors = { + '1': '#555555', # Bordure + '2': '#333333', # Marche 1 + '3': '#111111', # Marche 2 + '0': '#000000' # Fond du trou + } + tile_map = [ + "1111111111111111", + "1222222222222221", + "1233333333333221", + "1230000000003221", + "1230000000003221", + "1230000000003221", + "1230000000003221", + "1230000000003221", + "1230000000003221", + "1230000000003221", + "1230000000003221", + "1230000000003221", + "1230000000003221", + "1233333333333221", + "1222222222222221", + "1111111111111111" + ] + + # --- DESIGN DE LA RÉCOMPENSE (Cœur de Donjon) --- + elif self.sprite_type == 'reward': + colors = { + 'R': '#F83800', # Rouge vif (NES) + 'W': '#FFFFFF', # Blanc (Reflet) + 'B': '#000000' # Contour noir + } + tile_map = [ + " BBBBBB ", + " BBWWWWWWBB ", + " BBWWWWWWWWWWBB", + "BBWWWWWWWWWWWWBB", + "BBRRRRRRRRRRRRBB", + "BBRRRRRRRRRRRRBB", + "BBRRRRRRRRRRRRBB", + " BBRRRRRRRRBB ", + " BBRRRRRRRRBB ", + " BBRRRRBB ", + " BBRRRRBB ", + " BBBB ", + " BBBB ", + " BB ", + " ", + " " + ] + + # Application de la grille de couleurs sur la surface + if tile_map: + for row_index, row in enumerate(tile_map): + for col_index, char in enumerate(row): + if char in colors: + pygame.draw.rect( + self.image, + colors[char], + (col_index * self.pixel_size, row_index * self.pixel_size, self.pixel_size, self.pixel_size) + ) + else: + # Fallback si rien n'est défini + self.image.fill('purple') + + def destroy(self): + """Supprime la tuile du jeu (utilisé pour l'herbe)""" + self.kill() \ No newline at end of file diff --git a/SECONDGAME/ui.py b/SECONDGAME/ui.py new file mode 100644 index 0000000..3574db7 --- /dev/null +++ b/SECONDGAME/ui.py @@ -0,0 +1,187 @@ +import pygame +import math +from settings import * + +class UI: + def __init__(self): + # Informations générales + self.display_surface = pygame.display.get_surface() + self.font = pygame.font.Font(UI_FONT, UI_FONT_SIZE) + + # Configuration des barres + self.health_bar_rect = pygame.Rect(10, 10, HEALTH_BAR_WIDTH, BAR_HEIGHT) + self.energy_bar_rect = pygame.Rect(10, 34, ENERGY_BAR_WIDTH, BAR_HEIGHT) + + def show_bar(self, current, max_amount, bg_rect, color): + # Dessiner le fond + pygame.draw.rect(self.display_surface, UI_BG_COLOR, bg_rect) + + # Convertir les stats en pixels + ratio = current / max_amount + current_width = bg_rect.width * ratio + current_rect = bg_rect.copy() + current_rect.width = current_width + + # Dessiner la barre + pygame.draw.rect(self.display_surface, color, current_rect) + pygame.draw.rect(self.display_surface, UI_BORDER_COLOR, bg_rect, 3) + + def show_enemy_count(self, count): + """Affiche le nombre d'ennemis restants en haut à droite""" + text_surf = self.font.render(f'Ennemis: {count}', True, TEXT_COLOR) + if count == 0: + text_surf = self.font.render('Sortie déverrouillée !', True, 'Gold') + + text_rect = text_surf.get_rect(topright = (WIDTH - 20, 20)) + + # Fond du texte + pygame.draw.rect(self.display_surface, UI_BG_COLOR, text_rect.inflate(20, 20)) + self.display_surface.blit(text_surf, text_rect) + pygame.draw.rect(self.display_surface, UI_BORDER_COLOR, text_rect.inflate(20, 20), 3) + + def show_special_hearts(self, count): + """Affiche les cœurs de donjons récoltés""" + for i in range(count): + x = 10 + (i * 30) + y = 60 + # Dessine un petit cœur doré ou rouge + pygame.draw.circle(self.display_surface, 'Gold', (x + 15, y + 15), 10) + if count >= 3: # Indicateur épée laser + pygame.draw.circle(self.display_surface, 'White', (x + 15, y + 15), 4) + + def display_help(self): + """Affiche le guide des touches (H)""" + overlay = pygame.Surface((WIDTH, HEIGHT)) + overlay.set_alpha(150) + overlay.fill('black') + self.display_surface.blit(overlay, (0,0)) + + help_text = [ + "=== GUIDE DE SURVIE ===", + "Z,Q,S,D : Se déplacer", + "ESPACE : Attaquer à l'épée", + "CTRL : Lancer la boule de feu", + "L_SHIFT : Dash / Esquive", + "T : Menu des Talents (500 XP requis)", + "H : Fermer ce guide", + "", + "OBJECTIF : Tuez tous les ennemis pour", + "faire apparaître l'escalier de sortie." + ] + + for i, line in enumerate(help_text): + surf = self.font.render(line, True, 'White') + rect = surf.get_rect(center = (WIDTH//2, 150 + i * 40)) + self.display_surface.blit(surf, rect) + + def display_talent_menu(self, player): + """Interface des talents stylisée avec compteurs et cadres""" + # 1. Overlay de fond (Sombre et élégant) + overlay = pygame.Surface((WIDTH, HEIGHT)) + overlay.set_alpha(200) + overlay.fill('#050505') + self.display_surface.blit(overlay, (0,0)) + + # 2. Calcul des points + talent_pts = player.exp // 500 + next_point_xp = 500 - (player.exp % 500) + + # 3. Titre Principal + title_surf = self.font.render("MENU DES AMÉLIORATIONS", True, 'Gold') + title_rect = title_surf.get_rect(center=(WIDTH // 2, 80)) + self.display_surface.blit(title_surf, title_rect) + pygame.draw.line(self.display_surface, 'Gold', (WIDTH//2 - 150, 105), (WIDTH//2 + 150, 105), 3) + + # 4. Affichage des Stats Globales (XP et Points) + # Cadre pour les points + info_rect = pygame.Rect(WIDTH//2 - 200, 130, 400, 80) + pygame.draw.rect(self.display_surface, '#222222', info_rect, 0, 10) # Fond arrondi + pygame.draw.rect(self.display_surface, 'Gold', info_rect, 2, 10) # Bordure + + pts_surf = self.font.render(f"POINTS DE TALENT : {talent_pts}", True, '#00FF00') + self.display_surface.blit(pts_surf, (info_rect.x + 20, info_rect.y + 15)) + + xp_surf = self.font.render(f"XP TOTAL : {int(player.exp)}", True, 'White') + self.display_surface.blit(xp_surf, (info_rect.x + 20, info_rect.y + 45)) + + next_surf = self.font.render(f"Prochain point dans : {int(next_point_xp)} XP", True, '#AAAAAA') + self.display_surface.blit(next_surf, (info_rect.x + 180, info_rect.y + 45)) + + # 5. Options d'amélioration + options = [ + {"key": "1", "name": "Vigueur", "desc": "+20 Vie Maximum", "val": f"{int(player.health)}/{int(player.stats['health'])}"}, + {"key": "2", "name": "Force", "desc": "+5 Dégâts d'Attaque", "val": f"{player.stats['attack']}"}, + {"key": "3", "name": "Agilité", "desc": "+Vitesse de mouvement", "val": f"{round(player.speed, 1)}"} + ] + + for i, opt in enumerate(options): + y_pos = 250 + (i * 100) + item_rect = pygame.Rect(WIDTH//2 - 250, y_pos, 500, 80) + + # Couleur selon disponibilité + color_border = 'Gold' if talent_pts > 0 else '#444444' + color_text = 'White' if talent_pts > 0 else 'Gray' + + # Dessin du slot + pygame.draw.rect(self.display_surface, '#1A1A1A', item_rect, 0, 5) + pygame.draw.rect(self.display_surface, color_border, item_rect, 2, 5) + + # Texte de l'option + key_surf = self.font.render(f"[{opt['key']}]", True, color_border) + self.display_surface.blit(key_surf, (item_rect.x + 15, item_rect.y + 25)) + + name_surf = self.font.render(opt['name'], True, color_text) + self.display_surface.blit(name_surf, (item_rect.x + 70, item_rect.y + 15)) + + desc_surf = self.font.render(opt['desc'], True, '#888888') + self.display_surface.blit(desc_surf, (item_rect.x + 70, item_rect.y + 45)) + + val_surf = self.font.render(opt['val'], True, color_border) + self.display_surface.blit(val_surf, (item_rect.right - 100, item_rect.y + 25)) + + # 6. Footer + exit_surf = self.font.render("Appuyez sur [T] pour fermer", True, '#666666') + self.display_surface.blit(exit_surf, (WIDTH//2 - 100, HEIGHT - 50)) + + def display_weapon_menu(self, player, save_data): + """Menu des armes désactivé (une seule arme : épée).""" + pass + + def display_magic_menu(self, player, save_data, cursor_index=0): + """Menu de magie désactivé (une seule magie : boule de feu).""" + pass + def display(self, player, save_data=None): + # Barres de vie et énergie + self.show_bar(player.health, player.stats['health'], self.health_bar_rect, HEALTH_COLOR) + self.show_bar(player.energy, player.stats['energy'], self.energy_bar_rect, ENERGY_COLOR) + + # Affichage XP et Rubis + exp_surf = self.font.render(f'XP: {int(player.exp)}', True, TEXT_COLOR) + ruby_surf = self.font.render(f'Rubis: {player.rubies}', True, '#FFD700') + self.display_surface.blit(exp_surf, (10, HEIGHT - 60)) + self.display_surface.blit(ruby_surf, (10, HEIGHT - 30)) + + # Affichage simple des contrôles combat + controls_surf = self.font.render("Épée: ESPACE | Feu: CTRL", True, '#FFD700') + self.display_surface.blit(controls_surf, (WIDTH - 320, HEIGHT - 40)) + + def display_mission(self, mission): + """Affiche la mission active""" + if mission: + # Fond de la mission + mission_rect = pygame.Rect(WIDTH//2 - 200, 10, 400, 60) + pygame.draw.rect(self.display_surface, '#222222', mission_rect, 0, 10) + pygame.draw.rect(self.display_surface, '#FFD700', mission_rect, 2, 10) + + # Description + desc_surf = self.font.render(mission.description, True, 'White') + self.display_surface.blit(desc_surf, (mission_rect.x + 10, mission_rect.y + 10)) + + # Timer ou progression + if mission.type == 'speed_kill': + time_left = mission.get_time_left() // 1000 + progress_surf = self.font.render(f"Temps: {time_left}s | Tués: {mission.enemies_killed}/{mission.target_kills}", True, '#FFD700') + else: # elite_hunt + progress_surf = self.font.render("Trouvez et tuez l'ennemi d'élite !", True, '#FFD700') + + self.display_surface.blit(progress_surf, (mission_rect.x + 10, mission_rect.y + 35)) \ No newline at end of file diff --git a/SECONDGAME/weapon.py b/SECONDGAME/weapon.py new file mode 100644 index 0000000..d0dd4b4 --- /dev/null +++ b/SECONDGAME/weapon.py @@ -0,0 +1,325 @@ +import pygame +import math +import random +from settings import * + +class SwordTrail(pygame.sprite.Sprite): + """Classe pour créer les éclats de traînée derrière la lame""" + def __init__(self, groups, pos, image, angle): + super().__init__(groups) + self.sprite_type = 'effect' + # On crée une copie de l'image tournée pour la traînée + self.image = image.copy() + # On rend l'image toute blanche pour l'effet de traînée + self.image.fill((255, 255, 255, 180), special_flags=pygame.BLEND_RGBA_MULT) + self.rect = self.image.get_rect(center=pos) + self.alpha = 150 + self.fade_speed = 25 # Vitesse de disparition + + def update(self): + # L'effet s'estompe et disparaît + self.alpha -= self.fade_speed + self.image.set_alpha(self.alpha) + if self.alpha <= 0: + self.kill() + +class WeaponProjectile(pygame.sprite.Sprite): + """Classe de base pour les projectiles d'armes spéciales""" + def __init__(self, weapon_type, pos, direction, groups, obstacle_sprites, attackable_sprites, player): + super().__init__(groups) + self.sprite_type = 'weapon_projectile' + self.weapon_type = weapon_type + self.player = player + self.direction = direction + self.obstacle_sprites = obstacle_sprites + self.attackable_sprites = attackable_sprites + + # Configuration selon le type d'arme + self.setup_weapon() + + self.rect = self.image.get_rect(center=pos) + self.hitbox = self.rect.copy() + self.spawn_time = pygame.time.get_ticks() + + def setup_weapon(self): + if self.weapon_type == 'boomerang': + self.image = pygame.Surface((16, 16), pygame.SRCALPHA) + pygame.draw.polygon(self.image, '#8B4513', [(8, 2), (14, 8), (8, 14), (2, 8)]) + pygame.draw.circle(self.image, '#FFD700', (8, 8), 6, 2) + self.speed = 8 + self.damage = 25 + self.lifetime = 2000 + self.returning = False + self.return_distance = 0 + + elif self.weapon_type == 'bomb': + self.image = pygame.Surface((12, 12), pygame.SRCALPHA) + pygame.draw.circle(self.image, '#000000', (6, 6), 6) + pygame.draw.circle(self.image, '#8B0000', (6, 6), 4) + self.speed = 6 + self.damage = 50 + self.lifetime = 1500 + self.explosion_timer = None + self.explosion_radius = 60 + + elif self.weapon_type == 'arrow': + self.image = pygame.Surface((20, 4), pygame.SRCALPHA) + pygame.draw.rect(self.image, '#8B4513', (0, 1, 16, 2)) + pygame.draw.polygon(self.image, '#C0C0C0', [(16, 0), (20, 2), (16, 4)]) + self.speed = 12 + self.damage = 30 + self.lifetime = 3000 + + elif self.weapon_type == 'shotgun': + # Le shotgun tire 5 projectiles + self.speed = 10 + self.damage = 20 + self.lifetime = 1000 + + def update(self): + current_time = pygame.time.get_ticks() + + if self.weapon_type == 'boomerang': + self.update_boomerang(current_time) + elif self.weapon_type == 'bomb': + self.update_bomb(current_time) + elif self.weapon_type == 'arrow': + self.update_arrow(current_time) + elif self.weapon_type == 'shotgun': + self.update_shotgun(current_time) + + # Collision avec les murs + for sprite in self.obstacle_sprites: + if sprite.hitbox.colliderect(self.hitbox): + if self.weapon_type == 'bomb': + self.explode() + else: + self.kill() + return + + # Collision avec les ennemis + for sprite in self.attackable_sprites: + if sprite.hitbox.colliderect(self.hitbox) and hasattr(sprite, 'get_damage'): + damage_result = sprite.get_damage(self.player, 'weapon') + if self.weapon_type == 'bomb': + self.explode() + elif self.weapon_type != 'boomerang': # Le boomerang traverse les ennemis + self.kill() + break + + # Durée de vie écoulée + if current_time - self.spawn_time >= self.lifetime: + if self.weapon_type == 'bomb': + self.explode() + else: + self.kill() + + def update_boomerang(self, current_time): + if not self.returning: + # Aller vers l'avant + self.hitbox.x += self.direction.x * self.speed + self.hitbox.y += self.direction.y * self.speed + self.return_distance += self.speed + + # Commencer à revenir après une certaine distance + if self.return_distance >= 150: + self.returning = True + else: + # Revenir vers le joueur + player_vec = pygame.math.Vector2(self.player.rect.center) + projectile_vec = pygame.math.Vector2(self.hitbox.center) + return_direction = (player_vec - projectile_vec).normalize() + self.hitbox.x += return_direction.x * self.speed + self.hitbox.y += return_direction.y * self.speed + + # Ramasser si proche du joueur + if projectile_vec.distance_to(player_vec) < 20: + self.kill() + + self.rect.center = self.hitbox.center + + def update_bomb(self, current_time): + self.hitbox.x += self.direction.x * self.speed + self.hitbox.y += self.direction.y * self.speed + self.rect.center = self.hitbox.center + + # Faire tourner la bombe pour l'effet visuel + angle = (current_time // 50) % 360 + self.image = pygame.transform.rotate(pygame.Surface((12, 12), pygame.SRCALPHA), angle) + pygame.draw.circle(self.image, '#000000', (6, 6), 6) + pygame.draw.circle(self.image, '#8B0000', (6, 6), 4) + + def update_arrow(self, current_time): + self.hitbox.x += self.direction.x * self.speed + self.hitbox.y += self.direction.y * self.speed + self.rect.center = self.hitbox.center + + def update_shotgun(self, current_time): + self.hitbox.x += self.direction.x * self.speed + self.hitbox.y += self.direction.y * self.speed + self.rect.center = self.hitbox.center + + def explode(self): + """Explosion de la bombe""" + # Créer un effet d'explosion + explosion = ExplosionEffect(self.hitbox.center, self.explosion_radius, self.groups()) + explosion.damage_enemies(self.attackable_sprites, self.player, self.damage) + self.kill() + +class ExplosionEffect(pygame.sprite.Sprite): + """Effet visuel d'explosion""" + def __init__(self, pos, radius, groups): + super().__init__(groups) + self.sprite_type = 'effect' + self.pos = pos + self.radius = radius + self.max_radius = radius + self.current_radius = 5 + self.expand_speed = 10 + self.alpha = 200 + + def damage_enemies(self, attackable_sprites, player, damage): + """Inflige des dégâts à tous les ennemis dans le rayon""" + for sprite in attackable_sprites: + if hasattr(sprite, 'hitbox') and hasattr(sprite, 'get_damage'): + distance = pygame.math.Vector2(self.pos).distance_to(sprite.hitbox.center) + if distance <= self.radius: + sprite.get_damage(player, 'weapon') + + def update(self): + self.current_radius += self.expand_speed + if self.current_radius >= self.max_radius: + self.kill() + return + + # Créer l'effet visuel + self.image = pygame.Surface((self.current_radius * 2, self.current_radius * 2), pygame.SRCALPHA) + pygame.draw.circle(self.image, (255, 100, 0, self.alpha), (self.current_radius, self.current_radius), self.current_radius) + pygame.draw.circle(self.image, (255, 200, 0, self.alpha//2), (self.current_radius, self.current_radius), self.current_radius * 0.7) + self.rect = self.image.get_rect(center=self.pos) + self.alpha -= 10 + +class Weapon(pygame.sprite.Sprite): + def __init__(self, player, groups, weapon_type='sword'): + super().__init__(groups) + self.sprite_type = 'weapon' + self.player = player + self.weapon_type = 'sword' + self.groups_list = groups # On garde les groupes pour créer la traînée + + self.setup_sword() + + def setup_sword(self): + # Paramètres de l'animation + self.distance = 50 + self.angle_range = 140 + self.animation_speed = 15 # Plus rapide pour l'effet de traînée + self.current_angle_step = 0 + + # Déterminer l'angle de départ + direction = self.player.status.split('_')[0] + base_angles = {'up': 90, 'down': 270, 'left': 180, 'right': 0} + self.start_angle = base_angles.get(direction, 0) - (self.angle_range // 2) + + # Création du visuel de l'épée + self.original_image = pygame.Surface((54, 16), pygame.SRCALPHA) + self.draw_master_sword() + + self.image = self.original_image + self.rect = self.image.get_rect() + + def draw_master_sword(self): + # Lame + pygame.draw.rect(self.original_image, '#ACDDF1', (15, 4, 35, 8), border_radius=2) + # Garde + pygame.draw.rect(self.original_image, '#0000AA', (10, 0, 6, 16), border_radius=2) + # Poignée + pygame.draw.rect(self.original_image, '#8B4513', (0, 5, 10, 6)) + + def update(self): + self.update_sword() + + def update_sword(self): + # 1. Calcul de l'angle + self.current_angle_step += self.animation_speed + angle_rad = math.radians(self.start_angle + self.current_angle_step) + + # 2. Positionnement + self.rect.centerx = self.player.rect.centerx + math.cos(angle_rad) * self.distance + self.rect.centery = self.player.rect.centery - math.sin(angle_rad) * self.distance + + # 3. Rotation + rotation_angle = self.start_angle + self.current_angle_step + self.image = pygame.transform.rotate(self.original_image, rotation_angle) + self.rect = self.image.get_rect(center=self.rect.center) + + # 4. CRÉATION DE LA TRAÎNÉE + # On crée un effet visuel à la position actuelle avant que l'épée ne bouge plus + SwordTrail(self.groups_list, self.rect.center, self.image, rotation_angle) + + # 5. Fin de l'attaque + if self.current_angle_step >= self.angle_range: + self.kill() + +class NewWeapon(pygame.sprite.Sprite): + def __init__(self, player, groups, weapon_type='sword', obstacle_sprites=None, attackable_sprites=None): + super().__init__(groups) + self.sprite_type = 'weapon' + self.player = player + self.weapon_type = 'sword' + self.groups_list = groups + self.obstacle_sprites = obstacle_sprites + self.attackable_sprites = attackable_sprites + + self.setup_sword() + + def setup_sword(self): + # Paramètres de l'animation + self.distance = 50 + self.angle_range = 140 + self.animation_speed = 15 # Plus rapide pour l'effet de traînée + self.current_angle_step = 0 + + # Déterminer l'angle de départ + direction = self.player.status.split('_')[0] + base_angles = {'up': 90, 'down': 270, 'left': 180, 'right': 0} + self.start_angle = base_angles.get(direction, 0) - (self.angle_range // 2) + + # Création du visuel de l'épée + self.original_image = pygame.Surface((54, 16), pygame.SRCALPHA) + self.draw_master_sword() + + self.image = self.original_image + self.rect = self.image.get_rect() + + def draw_master_sword(self): + # Lame + pygame.draw.rect(self.original_image, '#ACDDF1', (15, 4, 35, 8), border_radius=2) + # Garde + pygame.draw.rect(self.original_image, '#0000AA', (10, 0, 6, 16), border_radius=2) + # Poignée + pygame.draw.rect(self.original_image, '#8B4513', (0, 5, 10, 6)) + + def update(self): + self.update_sword() + + def update_sword(self): + # 1. Calcul de l'angle + self.current_angle_step += self.animation_speed + angle_rad = math.radians(self.start_angle + self.current_angle_step) + + # 2. Positionnement + self.rect.centerx = self.player.rect.centerx + math.cos(angle_rad) * self.distance + self.rect.centery = self.player.rect.centery - math.sin(angle_rad) * self.distance + + # 3. Rotation + rotation_angle = self.start_angle + self.current_angle_step + self.image = pygame.transform.rotate(self.original_image, rotation_angle) + self.rect = self.image.get_rect(center=self.rect.center) + + # 4. CRÉATION DE LA TRAÎNÉE + SwordTrail(self.groups_list, self.rect.center, self.image, rotation_angle) + + # 5. Fin de l'attaque + if self.current_angle_step >= self.angle_range: + self.kill() \ No newline at end of file