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 0000000..273847e Binary files /dev/null and b/SECONDGAME/graphics/player/player.png differ 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