diff --git a/__pycache__/family.cpython-311.pyc b/__pycache__/family.cpython-311.pyc index 224112a..012b68a 100644 Binary files a/__pycache__/family.cpython-311.pyc and b/__pycache__/family.cpython-311.pyc differ diff --git a/__pycache__/tree.cpython-311.pyc b/__pycache__/tree.cpython-311.pyc index e030fe4..67acaf3 100644 Binary files a/__pycache__/tree.cpython-311.pyc and b/__pycache__/tree.cpython-311.pyc differ diff --git a/arbre.png b/arbre.png index e5e91ec..2c9b0bc 100644 Binary files a/arbre.png and b/arbre.png differ diff --git a/bot.py b/bot.py index 384ed3f..0463c35 100644 --- a/bot.py +++ b/bot.py @@ -173,35 +173,32 @@ async def couple(interaction: discord.Interaction, partenaire: discord.Member): ephemeral=True ) -@bot.tree.command(name="renier", description="Renier un enfant") +@bot.tree.command(name="renier", description="Renier et supprimer un enfant") async def renier(interaction: discord.Interaction, enfant: discord.Member): parent = interaction.user - success = family.remove_child(str(parent.id), str(enfant.id)) - - if success: - # Retire le rôle - generation = family.get_generation(str(enfant.id)) - role_id = config["ROLE_IDS"].get( - "ENFANT" if generation == 1 else - "PETIT_ENFANT" if generation == 2 else - "ARRIERE_PETIT_ENFANT" - ) - - if role_id: - role = interaction.guild.get_role(role_id) - if role: - try: - await enfant.remove_roles(role) - except discord.Forbidden: - pass # On continue même si échec du retrait de rôle - - await interaction.response.send_message( - f"❌ {parent.mention} a renié {enfant.mention} !" - ) + + # Supprime d'abord le lien parental + family.remove_child(str(parent.id), str(enfant.id)) + + # Vérifie si l'enfant n'a plus aucun parent + parents_restants = family.get_parents(str(enfant.id)) + + if not parents_restants: + # Suppression complète + if family.remove_member(str(enfant.id)): + await interaction.response.send_message( + f"❌ {parent.mention} a renié et supprimé {enfant.mention} de la famille !" + ) + else: + await interaction.response.send_message( + "❌ Erreur lors de la suppression", + ephemeral=True + ) else: + # Cas où l'enfant a d'autres parents await interaction.response.send_message( - "❌ Aucun lien parental trouvé.", - ephemeral=True + f"⚠️ {enfant.mention} a encore des parents ({len(parents_restants)}). " + f"Seul le lien avec {parent.mention} a été rompu." ) @bot.tree.command(name="arbre", description="Afficher l'arbre généalogique") diff --git a/family.json b/family.json index 7450dbf..cc2aaa5 100644 --- a/family.json +++ b/family.json @@ -132,11 +132,6 @@ "avatar": "https://cdn.discordapp.com/avatars/582633859967877133/89c60709a5e4c47ee16001844f0d51ad.png?size=1024", "parents": [] }, - "1334641803059793940": { - "name": "Epic The Ticketal", - "avatar": "https://cdn.discordapp.com/avatars/1334641803059793940/e36465f33fa4c2cd35a9de212e6832fc.png?size=1024", - "parents": [] - }, "689487017557360662": { "name": "Ama-teramisu godess of Tiramisu", "avatar": "https://cdn.discordapp.com/avatars/689487017557360662/8bf7af66b2700f25a926d459f36f5b49.png?size=1024", @@ -180,4 +175,4 @@ "582633859967877133" ] ] -} \ No newline at end of file +} diff --git a/family.py b/family.py index 2e995bc..d47285e 100644 --- a/family.py +++ b/family.py @@ -192,3 +192,28 @@ class FamilyManager: """Liste toutes les racines""" data = self._load() return data.get("roots", []) + +def remove_member(self, member_id: str) -> bool: + """Supprime complètement un membre de la famille""" + data = self._load() + member_id = str(member_id) + + if member_id not in data["members"]: + return False + + # Supprime des couples + data["couples"] = [c for c in data["couples"] if member_id not in c] + + # Supprime des racines + if "roots" in data and member_id in data["roots"]: + data["roots"].remove(member_id) + + # Supprime le membre + del data["members"][member_id] + + # Nettoie les références parentales + for m in data["members"].values(): + m["parents"] = [p for p in m["parents"] if p["id"] != member_id] + + self._save(data) + return True diff --git a/tree.py b/tree.py index d85d223..9a32250 100644 --- a/tree.py +++ b/tree.py @@ -6,49 +6,37 @@ from PIL import Image, ImageDraw, ImageFont, ImageOps def create_avatar_with_name(user_id, name, avatar_path, output_path): """Crée une image combinant avatar rond et pseudo en dessous""" try: - # Charge l'avatar original avatar = Image.open(avatar_path).convert("RGBA") - - # Redimensionne et arrondit l'avatar avatar = avatar.resize((150, 150)) + mask = Image.new('L', (150, 150), 0) draw = ImageDraw.Draw(mask) draw.ellipse((0, 0, 150, 150), fill=255) rounded_avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5)) rounded_avatar.putalpha(mask) - # Crée une nouvelle image avec espace pour le texte combined = Image.new('RGBA', (200, 200), (0, 0, 0, 0)) combined.paste(rounded_avatar, (25, 0), rounded_avatar) - - # Ajoute le texte + draw = ImageDraw.Draw(combined) try: font = ImageFont.truetype("arial.ttf", 14) except: font = ImageFont.load_default() - + text_width = draw.textlength(name, font=font) draw.text(((200 - text_width) / 2, 160), name, font=font, fill="white") - + combined.save(output_path) return True except Exception as e: print(f"Erreur création avatar+texte {user_id}: {e}") return False -def generate_tree(input_file, output_file, root_id=None): # root_id devient optionnel +def generate_tree(input_file, output_file, root_id=None): with open(input_file) as f: data = json.load(f) - roots = data.get("roots", [root_id] if root_id else []) - if not roots: - roots = [mid for mid, m in data["members"].items() if not m.get("parents")] - - with open(input_file) as f: - data = json.load(f) - - # Configuration du graphe G = pgv.AGraph( directed=True, rankdir="TB", @@ -58,7 +46,6 @@ def generate_tree(input_file, output_file, root_id=None): # root_id devient opt splines="true" ) - # Style minimal pour les nœuds node_style = { "shape": "none", "imagescale": "false", @@ -68,21 +55,19 @@ def generate_tree(input_file, output_file, root_id=None): # root_id devient opt "fixedsize": "true" } - # Style des flèches edge_style = { "color": "#FFFFFF", "penwidth": "3", "arrowsize": "1.2" } - # Dossier temporaire pour les images combinées os.makedirs("temp_avatars", exist_ok=True) # Traitement de tous les membres for user_id, info in data["members"].items(): original_avatar = f"avatars/{user_id}.png" combined_avatar = f"temp_avatars/combined_{user_id}.png" - + if os.path.exists(original_avatar): create_avatar_with_name( user_id, @@ -96,37 +81,53 @@ def generate_tree(input_file, output_file, root_id=None): # root_id devient opt **node_style ) else: - # Fallback si pas d'avatar G.add_node( user_id, label=info["name"], **{**node_style, "fontcolor": "white"} ) - # Organisation hiérarchique - levels = {0: [root_id], 1: [], 2: []} - - for user_id, info in data["members"].items(): - if user_id == root_id: - continue - - parents = [p["id"] for p in info.get("parents", [])] - if root_id in parents: - levels[1].append(user_id) - elif any(p in levels[1] for p in parents): - levels[2].append(user_id) + # Nouveau : Dictionnaire pour forcer les rangs des couples + forced_ranks = {} - # Ajout des partenaires au bon niveau + # Traitement des couples for couple in data.get("couples", []): if all(m in data["members"] for m in couple): - for member in couple: - if member not in levels[1] and any(m in levels[1] for m in couple): - levels[1].append(member) + member1, member2 = couple + + # Style spécial pour les liens de couple + G.add_edge(member1, member2, + style="dashed", + color="#FF69B4", + penwidth="2.5", + dir="none") + + # Force les partenaires au même rang + partner_group = f"couple_{min(couple)}_{max(couple)}" + G.add_subgraph(couple, name=partner_group, rank="same") + forced_ranks[member1] = partner_group + forced_ranks[member2] = partner_group + + # Organisation hiérarchique améliorée + if root_id: + roots = [root_id] + else: + roots = data.get("roots", [k for k,v in data["members"].items() if not v.get("parents")]) + + # Nouvelle approche pour les niveaux + levels = {} + for user_id in data["members"]: + gen = data["members"][user_id].get("generation", + len(data["members"][user_id].get("parents", []))) + if gen not in levels: + levels[gen] = [] + levels[gen].append(user_id) # Création des sous-graphes par niveau for level, members in levels.items(): - if members: - G.add_subgraph(members, name=f"rank{level}", rank="same") + filtered_members = [m for m in members if m not in forced_ranks] + if filtered_members: + G.add_subgraph(filtered_members, name=f"rank{level}", rank="same") # Connexions parent-enfant for user_id, info in data["members"].items(): @@ -134,18 +135,9 @@ def generate_tree(input_file, output_file, root_id=None): # root_id devient opt if parent["id"] in data["members"]: G.add_edge(parent["id"], user_id, **edge_style) - # Connexions des couples - for couple in data.get("couples", []): - if all(m in data["members"] for m in couple): - G.add_edge(couple[0], couple[1], - style="dashed", - color="#FF69B4", - penwidth="2.5", - dir="none") - G.layout(prog="dot") G.draw(output_file) - - # Nettoyage des fichiers temporaires + + # Nettoyage for f in os.listdir("temp_avatars"): os.remove(f"temp_avatars/{f}")