From 6621599fc6a1e4e9ac26c5b566387025c90e33ab Mon Sep 17 00:00:00 2001 From: LE BERRE Mickael Date: Mon, 1 Jun 2026 13:56:36 +0200 Subject: [PATCH] i18n: translate all French comments, docstrings and logs to English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit discord.py's built-in !help command exposes cog docstrings directly to Discord members — leaving them in French made the output partially French. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 9 + bot.py | 162 ++++++------ cogs/guide.py | 238 ++++++++--------- cogs/mercy.py | 2 +- cogs/mystats.py | 196 +++++++------- cogs/pbchimera.py | 36 +-- cogs/pbcvc.py | 36 +-- cogs/pbhydra.py | 36 +-- cogs/top10.py | 206 +++++++-------- config.py | 102 ++++---- utils/DatabaseManager_class.py | 426 +++++++++++++++---------------- utils/MercyManager_class.py | 18 +- utils/ScreenshotManager_class.py | 130 +++++----- utils/helpers.py | 212 +++++++-------- utils/leaderboard_handler.py | 148 +++++------ utils/pb_handler.py | 370 +++++++++++++-------------- 16 files changed, 1168 insertions(+), 1159 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bde4310..68a8a21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,6 +56,15 @@ Dockerfile # Image Python 3.9-slim docker-compose.yml # Déploiement container ``` +## Langue du code + +Tout le code source doit être **exclusivement en anglais** : +- Docstrings et commentaires +- Messages Discord affichés aux utilisateurs +- Logs (`print`, `logging`) + +Le `!help` de discord.py affiche les docstrings des cogs directement — les laisser en français les rend visibles aux membres. + ## Boss supportés - **Hydra** : normal, hard, brutal, nightmare diff --git a/bot.py b/bot.py index 7a50e74..b494bbf 100644 --- a/bot.py +++ b/bot.py @@ -1,81 +1,81 @@ -# -*- coding: utf-8 -*- -import discord -import os -import sys -from discord.ext import commands -from config import DISCORD_TOKEN - -from utils.DatabaseManager_class import DatabaseManager -from utils.ScreenshotManager_class import ScreenshotManager -from utils.MercyManager_class import MercyManager -from utils.pb_handler import set_managers -from utils.leaderboard_handler import set_db_manager - -# Force UTF-8 - -os.environ["PYTHONIOENCODING"] = "utf-8" -sys.stdout.reconfigure(encoding='utf-8') - -# Définir les intents -intents = discord.Intents.default() -intents.message_content = True - -# Initialisation des managers -db_manager = DatabaseManager() -screenshot_manager = ScreenshotManager() -mercy_manager = MercyManager() - -# Injection des managers dans les handlers -set_managers(db_manager, screenshot_manager) # pb_handler -set_db_manager(db_manager) # leaderboard_handler - -# Liste des cogs -initial_cogs = [ - "cogs.guide", - "cogs.pbhydra", - "cogs.pbchimera", - "cogs.pbcvc", - "cogs.top10", - "cogs.mystats", - "cogs.mercy", -] - -# Liste des dossiers -folders = [ - "screenshots/hydra/normal", - "screenshots/hydra/hard", - "screenshots/hydra/brutal", - "screenshots/hydra/nightmare", - "screenshots/chimera/easy", - "screenshots/chimera/normal", - "screenshots/chimera/hard", - "screenshots/chimera/brutal", - "screenshots/chimera/nightmare", - "screenshots/chimera/ultra", - "screenshots/cvc", -] - -# Création des dossiers si nécessaire (exist_ok=True évite d'écraser) -for f in folders: - os.makedirs(f, exist_ok=True) - -class MyBot(commands.Bot): - def __init__(self): - super().__init__(command_prefix="!", intents=intents) - self.db_manager = db_manager - self.screenshot_manager = screenshot_manager - self.mercy_manager = mercy_manager - - async def setup_hook(self): - for cog in initial_cogs: - try: - await self.load_extension(cog) - print(f"[OK] Cog {cog} chargé") - except Exception as e: - print(f"[ERREUR] Impossible de charger {cog}: {e}") - - async def on_ready(self): - print(f"{self.user.name} est connecté !") - -bot = MyBot() -bot.run(DISCORD_TOKEN) +# -*- coding: utf-8 -*- +import discord +import os +import sys +from discord.ext import commands +from config import DISCORD_TOKEN + +from utils.DatabaseManager_class import DatabaseManager +from utils.ScreenshotManager_class import ScreenshotManager +from utils.MercyManager_class import MercyManager +from utils.pb_handler import set_managers +from utils.leaderboard_handler import set_db_manager + +# Force UTF-8 + +os.environ["PYTHONIOENCODING"] = "utf-8" +sys.stdout.reconfigure(encoding='utf-8') + +# Define intents +intents = discord.Intents.default() +intents.message_content = True + +# Initialize managers +db_manager = DatabaseManager() +screenshot_manager = ScreenshotManager() +mercy_manager = MercyManager() + +# Inject managers into handlers +set_managers(db_manager, screenshot_manager) # pb_handler +set_db_manager(db_manager) # leaderboard_handler + +# Cog list +initial_cogs = [ + "cogs.guide", + "cogs.pbhydra", + "cogs.pbchimera", + "cogs.pbcvc", + "cogs.top10", + "cogs.mystats", + "cogs.mercy", +] + +# Directory list +folders = [ + "screenshots/hydra/normal", + "screenshots/hydra/hard", + "screenshots/hydra/brutal", + "screenshots/hydra/nightmare", + "screenshots/chimera/easy", + "screenshots/chimera/normal", + "screenshots/chimera/hard", + "screenshots/chimera/brutal", + "screenshots/chimera/nightmare", + "screenshots/chimera/ultra", + "screenshots/cvc", +] + +# Create directories if needed (exist_ok=True avoids overwriting) +for f in folders: + os.makedirs(f, exist_ok=True) + +class MyBot(commands.Bot): + def __init__(self): + super().__init__(command_prefix="!", intents=intents) + self.db_manager = db_manager + self.screenshot_manager = screenshot_manager + self.mercy_manager = mercy_manager + + async def setup_hook(self): + for cog in initial_cogs: + try: + await self.load_extension(cog) + print(f"[OK] Cog {cog} loaded") + except Exception as e: + print(f"[ERROR] Failed to load {cog}: {e}") + + async def on_ready(self): + print(f"{self.user.name} connected!") + +bot = MyBot() +bot.run(DISCORD_TOKEN) diff --git a/cogs/guide.py b/cogs/guide.py index 320e8fa..b81d1ed 100644 --- a/cogs/guide.py +++ b/cogs/guide.py @@ -1,119 +1,119 @@ -# -*- coding: utf-8 -*- -import discord -from discord.ext import commands -from config import AUTHORIZED_CHANNEL_ID - -class Guide(commands.Cog): - """Affiche la liste des commandes disponibles""" - - def __init__(self, bot): - self.bot = bot - - @commands.command(name="guide") - async def guide(self, ctx): - """Affiche toutes les commandes disponibles avec les difficultés""" - if ctx.channel.id != AUTHORIZED_CHANNEL_ID: - return - - embed = discord.Embed( - title="🧐 TEA Bot - Commands Guide", - description="Here are all available commands for tracking your Personal Bests!", - color=0x00bfff - ) - - # Info sur les formats de dégâts - embed.add_field( - name="💠 Damage Formats", - value="**Accepted formats:** `1500000`, `1.5M`, `500K`, `2B`\n" - "**Suffixes:** K = thousands, M = millions, B = billions\n" - "**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare", - inline=False - ) - - # Commandes PB Hydra - embed.add_field( - name="🐍 Hydra Commands", - value="**Difficulties:** Normal | Hard | Brutal | Nightmare (nm)\n" - "`!pbhydra ` - Submit PB + screenshot\n" - "`!pbhydra ` - Show your PB\n" - "`!pbhydra ` - Show user's PB", - inline=False - ) - - # Commandes PB Chimera - embed.add_field( - name="🦁 Chimera Commands", - value="**Difficulties:** Easy | Normal | Hard | Brutal | Nightmare (nm) | Ultra (unm)\n" - "`!pbchimera ` - Submit PB + screenshot\n" - "`!pbchimera ` - Show your PB\n" - "`!pbchimera ` - Show user's PB", - inline=False - ) - - # Commandes PB CvC - embed.add_field( - name="⚔️ CvC Commands", - value="`!pbcvc ` - Submit PB + screenshot\n" - "`!pbcvc` - Show your PB\n" - "`!pbcvc ` - Show user's PB", - inline=False - ) - - # Commandes Mercy - embed.add_field( - name="🎲 Mercy Commands", - value="`!mercy show` - Show your current mercy pulls\n" - "`!mercy add ` - Add pulls to a shard type\n" - "`!mercy reset ` - Reset pulls for a shard type\n" - "**Available types:** ancient, void, sacred, primal, remnant", - inline=False - ) - - # Classements globaux - embed.add_field( - name="🌍 Global Leaderboards", - value="`!top10hydra ` - Global Hydra rankings\n" - "`!top10chimera ` - Global Chimera rankings\n" - "`!top10cvc` - Global CvC rankings", - inline=False - ) - - # Classements par clan - embed.add_field( - name="🏆 Clan Leaderboards", - value="**🔥 TEAI (Inferno):** `!teaihydra ` `!teaichimera ` `!teaicvc`\n" - "**🛡️ TEAF (Flame):** `!teafhydra ` `!teafchimera ` `!teafcvc`\n" - "**⚔️ TEAC (Cinder):** `!teachydra ` `!teachimera ` `!teaccvc`\n" - "**👑 TEACO (Corrupted Olympians):** `!teacohydra ` `!teacochimera ` `!teacocvc`", - inline=False - ) - - # Stats et aide - embed.add_field( - name="📈 Stats & Info", - value="`!mystats` - View all your PBs\n" - "`!mystats ` - View someone's PBs\n" - "`!guide` - Show this help message", - inline=False - ) - - # Instructions - embed.add_field( - name="⚡ Examples", - value="`!pbhydra brutal 1.5M` - Submit Brutal Hydra PB\n" - "`!pbchimera unm 500K` - Submit Ultra Nightmare PB\n" - "`!pbcvc 2.3M` - Submit CvC PB\n" - "`!mercy add 50 primal` - Add 50 pulls to Primal shard\n" - "`!mercy show` - Show your mercy pulls\n" - "`!teaihydra nm` - TEAI clan Nightmare rankings\n" - "**Always attach screenshot when submitting PBs!**", - inline=False - ) - - embed.set_footer(text="🎮 Old screenshots are automatically deleted when you set new PBs!") - - await ctx.send(embed=embed) - - -async def setup(bot): - await bot.add_cog(Guide(bot)) +# -*- coding: utf-8 -*- +import discord +from discord.ext import commands +from config import AUTHORIZED_CHANNEL_ID + +class Guide(commands.Cog): + """Shows the list of available commands""" + + def __init__(self, bot): + self.bot = bot + + @commands.command(name="guide") + async def guide(self, ctx): + """Shows all available commands with difficulties""" + if ctx.channel.id != AUTHORIZED_CHANNEL_ID: + return + + embed = discord.Embed( + title="🧐 TEA Bot - Commands Guide", + description="Here are all available commands for tracking your Personal Bests!", + color=0x00bfff + ) + + # Damage format info + embed.add_field( + name="💠 Damage Formats", + value="**Accepted formats:** `1500000`, `1.5M`, `500K`, `2B`\n" + "**Suffixes:** K = thousands, M = millions, B = billions\n" + "**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare", + inline=False + ) + + # Hydra PB commands + embed.add_field( + name="🐍 Hydra Commands", + value="**Difficulties:** Normal | Hard | Brutal | Nightmare (nm)\n" + "`!pbhydra ` - Submit PB + screenshot\n" + "`!pbhydra ` - Show your PB\n" + "`!pbhydra ` - Show user's PB", + inline=False + ) + + # Chimera PB commands + embed.add_field( + name="🦁 Chimera Commands", + value="**Difficulties:** Easy | Normal | Hard | Brutal | Nightmare (nm) | Ultra (unm)\n" + "`!pbchimera ` - Submit PB + screenshot\n" + "`!pbchimera ` - Show your PB\n" + "`!pbchimera ` - Show user's PB", + inline=False + ) + + # CvC PB commands + embed.add_field( + name="⚔️ CvC Commands", + value="`!pbcvc ` - Submit PB + screenshot\n" + "`!pbcvc` - Show your PB\n" + "`!pbcvc ` - Show user's PB", + inline=False + ) + + # Mercy commands + embed.add_field( + name="🎲 Mercy Commands", + value="`!mercy show` - Show your current mercy pulls\n" + "`!mercy add ` - Add pulls to a shard type\n" + "`!mercy reset ` - Reset pulls for a shard type\n" + "**Available types:** ancient, void, sacred, primal, remnant", + inline=False + ) + + # Global leaderboards + embed.add_field( + name="🌍 Global Leaderboards", + value="`!top10hydra ` - Global Hydra rankings\n" + "`!top10chimera ` - Global Chimera rankings\n" + "`!top10cvc` - Global CvC rankings", + inline=False + ) + + # Clan leaderboards + embed.add_field( + name="🏆 Clan Leaderboards", + value="**🔥 TEAI (Inferno):** `!teaihydra ` `!teaichimera ` `!teaicvc`\n" + "**🛡️ TEAF (Flame):** `!teafhydra ` `!teafchimera ` `!teafcvc`\n" + "**⚔️ TEAC (Cinder):** `!teachydra ` `!teachimera ` `!teaccvc`\n" + "**👑 TEACO (Corrupted Olympians):** `!teacohydra ` `!teacochimera ` `!teacocvc`", + inline=False + ) + + # Stats and help + embed.add_field( + name="📈 Stats & Info", + value="`!mystats` - View all your PBs\n" + "`!mystats ` - View someone's PBs\n" + "`!guide` - Show this help message", + inline=False + ) + + # Examples + embed.add_field( + name="⚡ Examples", + value="`!pbhydra brutal 1.5M` - Submit Brutal Hydra PB\n" + "`!pbchimera unm 500K` - Submit Ultra Nightmare PB\n" + "`!pbcvc 2.3M` - Submit CvC PB\n" + "`!mercy add 50 primal` - Add 50 pulls to Primal shard\n" + "`!mercy show` - Show your mercy pulls\n" + "`!teaihydra nm` - TEAI clan Nightmare rankings\n" + "**Always attach screenshot when submitting PBs!**", + inline=False + ) + + embed.set_footer(text="🎮 Old screenshots are automatically deleted when you set new PBs!") + + await ctx.send(embed=embed) + + +async def setup(bot): + await bot.add_cog(Guide(bot)) diff --git a/cogs/mercy.py b/cogs/mercy.py index b7fde74..de2949f 100644 --- a/cogs/mercy.py +++ b/cogs/mercy.py @@ -8,7 +8,7 @@ from utils.helpers import calc_chance_and_guarantee VALID_SHARDS = ["ancient", "void", "sacred", "primal", "remnant"] class Mercy(commands.Cog): - """Cog pour gérer les pulls de Mercy""" + """Cog for managing Mercy pulls""" def __init__(self, bot): self.bot = bot diff --git a/cogs/mystats.py b/cogs/mystats.py index 2992238..c574576 100644 --- a/cogs/mystats.py +++ b/cogs/mystats.py @@ -1,98 +1,98 @@ -# -*- coding: utf-8 -*- -import discord -from discord.ext import commands -from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG -from utils.helpers import format_damage_display, format_date_only -from utils.pb_handler import db_manager # Assurez-vous que db_manager est initialisé correctement - -class MyStats(commands.Cog): - """Cog pour afficher tous les PB d'un utilisateur""" - - def __init__(self, bot): - self.bot = bot - - @commands.command(name="mystats") - async def mystats(self, ctx, *, target_user: str = None): - """Affiche tous les PB d'un utilisateur avec les nouvelles difficultés""" - if ctx.channel.id != AUTHORIZED_CHANNEL_ID: - return - - try: - if target_user: - matches = db_manager.find_user_by_name(target_user) - if not matches: - await ctx.send(f"❌ No data found for **{target_user}**.") - return - if len(matches) > 1: - await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.") - return - user_id, display_name = matches[0] - else: - user_id = ctx.author.id - display_name = ctx.author.display_name - - user_data = db_manager.get_user_all_pbs(user_id) - - if not user_data: - await ctx.send(f"❌ No data found for **{display_name}**.") - return - - embed = discord.Embed( - title=f"📊 {display_name}'s Complete Stats", - color=0x00bfff - ) - - # Hydra - toutes les difficultés - hydra_stats = [] - for difficulty in BOSS_CONFIG['hydra']['difficulties']: - pb_key = f'pb_hydra_{difficulty}' - date_key = f'pb_hydra_{difficulty}_date' - - if pb_key in user_data and user_data[pb_key] > 0: - pb_value = user_data[pb_key] - pb_date = user_data.get(date_key) - date_text = f" • {format_date_only(pb_date)}" if pb_date else "" - hydra_stats.append(f"**{difficulty.title()}:** {format_damage_display(pb_value)}{date_text}") - - hydra_text = "\n".join(hydra_stats) if hydra_stats else "No records" - embed.add_field(name="⚔️ Hydra PBs", value=hydra_text, inline=False) - - # Chimera - toutes les difficultés - chimera_stats = [] - for difficulty in BOSS_CONFIG['chimera']['difficulties']: - pb_key = f'pb_chimera_{difficulty}' - date_key = f'pb_chimera_{difficulty}_date' - - if pb_key in user_data and user_data[pb_key] > 0: - pb_value = user_data[pb_key] - pb_date = user_data.get(date_key) - date_text = f" • {format_date_only(pb_date)}" if pb_date else "" - display_name = "Ultra Nightmare" if difficulty == "ultra" else difficulty.title() - chimera_stats.append(f"**{display_name}:** {format_damage_display(pb_value)}{date_text}") - - chimera_text = "\n".join(chimera_stats) if chimera_stats else "No records" - embed.add_field(name="🛡️ Chimera PBs", value=chimera_text, inline=False) - - # CvC - cvc_pb = user_data.get('pb_cvc', 0) - cvc_date = user_data.get('pb_cvc_date') - cvc_text = f"**{format_damage_display(cvc_pb)} damage**" if cvc_pb > 0 else "No record" - if cvc_pb > 0 and cvc_date: - formatted_date = format_date_only(cvc_date) - if formatted_date: - cvc_text += f" • {formatted_date}" - embed.add_field(name="🗡️ CvC PB", value=cvc_text, inline=False) - - # Total combiné - total_damage = sum(user_data.get(f'pb_hydra_{d}', 0) for d in BOSS_CONFIG['hydra']['difficulties']) - total_damage += sum(user_data.get(f'pb_chimera_{d}', 0) for d in BOSS_CONFIG['chimera']['difficulties']) - total_damage += user_data.get('pb_cvc', 0) - embed.add_field(name="💯 Total Combined Damage", value=f"**{format_damage_display(total_damage)}**", inline=False) - - await ctx.send(embed=embed) - - except Exception as e: - await ctx.send(f"❌ Error: {e}") - -async def setup(bot): - await bot.add_cog(MyStats(bot)) +# -*- coding: utf-8 -*- +import discord +from discord.ext import commands +from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG +from utils.helpers import format_damage_display, format_date_only +from utils.pb_handler import db_manager # Make sure db_manager is initialized correctly + +class MyStats(commands.Cog): + """Cog for displaying all PBs for a user""" + + def __init__(self, bot): + self.bot = bot + + @commands.command(name="mystats") + async def mystats(self, ctx, *, target_user: str = None): + """Shows all PBs for a user across all difficulties""" + if ctx.channel.id != AUTHORIZED_CHANNEL_ID: + return + + try: + if target_user: + matches = db_manager.find_user_by_name(target_user) + if not matches: + await ctx.send(f"❌ No data found for **{target_user}**.") + return + if len(matches) > 1: + await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.") + return + user_id, display_name = matches[0] + else: + user_id = ctx.author.id + display_name = ctx.author.display_name + + user_data = db_manager.get_user_all_pbs(user_id) + + if not user_data: + await ctx.send(f"❌ No data found for **{display_name}**.") + return + + embed = discord.Embed( + title=f"📊 {display_name}'s Complete Stats", + color=0x00bfff + ) + + # Hydra - all difficulties + hydra_stats = [] + for difficulty in BOSS_CONFIG['hydra']['difficulties']: + pb_key = f'pb_hydra_{difficulty}' + date_key = f'pb_hydra_{difficulty}_date' + + if pb_key in user_data and user_data[pb_key] > 0: + pb_value = user_data[pb_key] + pb_date = user_data.get(date_key) + date_text = f" • {format_date_only(pb_date)}" if pb_date else "" + hydra_stats.append(f"**{difficulty.title()}:** {format_damage_display(pb_value)}{date_text}") + + hydra_text = "\n".join(hydra_stats) if hydra_stats else "No records" + embed.add_field(name="⚔️ Hydra PBs", value=hydra_text, inline=False) + + # Chimera - all difficulties + chimera_stats = [] + for difficulty in BOSS_CONFIG['chimera']['difficulties']: + pb_key = f'pb_chimera_{difficulty}' + date_key = f'pb_chimera_{difficulty}_date' + + if pb_key in user_data and user_data[pb_key] > 0: + pb_value = user_data[pb_key] + pb_date = user_data.get(date_key) + date_text = f" • {format_date_only(pb_date)}" if pb_date else "" + display_name = "Ultra Nightmare" if difficulty == "ultra" else difficulty.title() + chimera_stats.append(f"**{display_name}:** {format_damage_display(pb_value)}{date_text}") + + chimera_text = "\n".join(chimera_stats) if chimera_stats else "No records" + embed.add_field(name="🛡️ Chimera PBs", value=chimera_text, inline=False) + + # CvC + cvc_pb = user_data.get('pb_cvc', 0) + cvc_date = user_data.get('pb_cvc_date') + cvc_text = f"**{format_damage_display(cvc_pb)} damage**" if cvc_pb > 0 else "No record" + if cvc_pb > 0 and cvc_date: + formatted_date = format_date_only(cvc_date) + if formatted_date: + cvc_text += f" • {formatted_date}" + embed.add_field(name="🗡️ CvC PB", value=cvc_text, inline=False) + + # Combined total + total_damage = sum(user_data.get(f'pb_hydra_{d}', 0) for d in BOSS_CONFIG['hydra']['difficulties']) + total_damage += sum(user_data.get(f'pb_chimera_{d}', 0) for d in BOSS_CONFIG['chimera']['difficulties']) + total_damage += user_data.get('pb_cvc', 0) + embed.add_field(name="💯 Total Combined Damage", value=f"**{format_damage_display(total_damage)}**", inline=False) + + await ctx.send(embed=embed) + + except Exception as e: + await ctx.send(f"❌ Error: {e}") + +async def setup(bot): + await bot.add_cog(MyStats(bot)) diff --git a/cogs/pbchimera.py b/cogs/pbchimera.py index c13120a..364f8e4 100644 --- a/cogs/pbchimera.py +++ b/cogs/pbchimera.py @@ -1,18 +1,18 @@ -# -*- coding: utf-8 -*- -import discord -from discord.ext import commands -from utils.pb_handler import handle_pb_command - -class Pbchimera(commands.Cog): - """Cog pour gérer les Personal Bests Chimera""" - - def __init__(self, bot): - self.bot = bot - - @commands.command(name="pbchimera") - async def pbchimera(self, ctx, arg1: str = None, *, arg2: str = None): - """Commande !pbchimera avec gestion des difficultés""" - await handle_pb_command(ctx, 'chimera', arg1, arg2) - -async def setup(bot): - await bot.add_cog(Pbchimera(bot)) +# -*- coding: utf-8 -*- +import discord +from discord.ext import commands +from utils.pb_handler import handle_pb_command + +class Pbchimera(commands.Cog): + """Cog for managing Chimera Personal Bests""" + + def __init__(self, bot): + self.bot = bot + + @commands.command(name="pbchimera") + async def pbchimera(self, ctx, arg1: str = None, *, arg2: str = None): + """!pbchimera command with difficulty handling""" + await handle_pb_command(ctx, 'chimera', arg1, arg2) + +async def setup(bot): + await bot.add_cog(Pbchimera(bot)) diff --git a/cogs/pbcvc.py b/cogs/pbcvc.py index ba9cd51..bf834eb 100644 --- a/cogs/pbcvc.py +++ b/cogs/pbcvc.py @@ -1,18 +1,18 @@ -# -*- coding: utf-8 -*- -import discord -from discord.ext import commands -from utils.pb_handler import handle_pb_command - -class Pbcvc(commands.Cog): - """Cog pour gérer les Personal Bests CvC (sans difficultés)""" - - def __init__(self, bot): - self.bot = bot - - @commands.command(name="pbcvc") - async def pbcvc(self, ctx, *, target_user: str = None): - """Commande !pbcvc""" - await handle_pb_command(ctx, 'cvc', target_user) - -async def setup(bot): - await bot.add_cog(Pbcvc(bot)) +# -*- coding: utf-8 -*- +import discord +from discord.ext import commands +from utils.pb_handler import handle_pb_command + +class Pbcvc(commands.Cog): + """Cog for managing CvC Personal Bests (no difficulties)""" + + def __init__(self, bot): + self.bot = bot + + @commands.command(name="pbcvc") + async def pbcvc(self, ctx, *, target_user: str = None): + """!pbcvc command""" + await handle_pb_command(ctx, 'cvc', target_user) + +async def setup(bot): + await bot.add_cog(Pbcvc(bot)) diff --git a/cogs/pbhydra.py b/cogs/pbhydra.py index 1697912..f16a7fa 100644 --- a/cogs/pbhydra.py +++ b/cogs/pbhydra.py @@ -1,18 +1,18 @@ -# -*- coding: utf-8 -*- -import discord -from discord.ext import commands -from utils.pb_handler import handle_pb_command - -class Pbhydra(commands.Cog): - """Cog pour gérer les Personal Bests Hydra""" - - def __init__(self, bot): - self.bot = bot - - @commands.command(name="pbhydra") - async def pbhydra(self, ctx, arg1: str = None, *, arg2: str = None): - """Commande !pbhydra avec gestion des difficultés""" - await handle_pb_command(ctx, 'hydra', arg1, arg2) - -async def setup(bot): - await bot.add_cog(Pbhydra(bot)) +# -*- coding: utf-8 -*- +import discord +from discord.ext import commands +from utils.pb_handler import handle_pb_command + +class Pbhydra(commands.Cog): + """Cog for managing Hydra Personal Bests""" + + def __init__(self, bot): + self.bot = bot + + @commands.command(name="pbhydra") + async def pbhydra(self, ctx, arg1: str = None, *, arg2: str = None): + """!pbhydra command with difficulty handling""" + await handle_pb_command(ctx, 'hydra', arg1, arg2) + +async def setup(bot): + await bot.add_cog(Pbhydra(bot)) diff --git a/cogs/top10.py b/cogs/top10.py index 515a43f..a765e88 100644 --- a/cogs/top10.py +++ b/cogs/top10.py @@ -1,103 +1,103 @@ -# -*- coding: utf-8 -*- -import discord -from discord.ext import commands -from utils.leaderboard_handler import show_leaderboard -from utils.helpers import normalize_difficulty -from config import BOSS_CONFIG - -class Top10(commands.Cog): - """Cog regroupant toutes les commandes de leaderboard globales et par clan""" - - def __init__(self, bot): - self.bot = bot - - # --- Commandes globales --- - @commands.command() - async def top10hydra(self, ctx, difficulty: str = None): - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['hydra']['difficulties']: - await show_leaderboard(ctx, 'hydra', difficulty) - else: - difficulties = " | ".join(BOSS_CONFIG['hydra']['difficulties']) - await ctx.send(f"❌ Please specify difficulty: `!top10hydra `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare") - - @commands.command() - async def top10chimera(self, ctx, difficulty: str = None): - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['chimera']['difficulties']: - await show_leaderboard(ctx, 'chimera', difficulty) - else: - difficulties = " | ".join(BOSS_CONFIG['chimera']['difficulties']) - await ctx.send(f"❌ Please specify difficulty: `!top10chimera `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra") - - @commands.command() - async def top10cvc(self, ctx): - await show_leaderboard(ctx, 'cvc') - - # --- Commandes par clan TEAI (Inferno) --- - @commands.command() - async def teaihydra(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAI') - - @commands.command() - async def teaichimera(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAI') - - @commands.command() - async def teaicvc(self, ctx): - await show_leaderboard(ctx, 'cvc', clan='TEAI') - - # --- Commandes par clan TEAF (Flame) --- - @commands.command() - async def teafhydra(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAF') - - @commands.command() - async def teafchimera(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAF') - - @commands.command() - async def teafcvc(self, ctx): - await show_leaderboard(ctx, 'cvc', clan='TEAF') - - # --- Commandes par clan TEAC (Cinder) --- - @commands.command() - async def teachydra(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAC') - - @commands.command() - async def teachimera(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAC') - - @commands.command() - async def teaccvc(self, ctx): - await show_leaderboard(ctx, 'cvc', clan='TEAC') - - # --- Commandes par clan TEACO (Corrupted Olympians) --- - @commands.command() - async def teacohydra(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEACO') - - @commands.command() - async def teacochimera(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEACO') - - @commands.command() - async def teacocvc(self, ctx): - await show_leaderboard(ctx, 'cvc', clan='TEACO') - - # --- Méthode interne pour éviter la répétition --- - async def _show_clan_leaderboard(self, ctx, boss_type, difficulty, clan): - """Affiche le leaderboard pour un boss et un clan spécifique""" - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG[boss_type]['difficulties']: - await show_leaderboard(ctx, boss_type, difficulty, clan) - elif boss_type != 'cvc': # CvC n’a pas de difficultés - difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties']) - await ctx.send( - f"❌ Please specify difficulty: `!{ctx.command.name} `\n" - f"**Available:** {difficulties}\n" - f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra" - ) - else: - await show_leaderboard(ctx, boss_type, clan=clan) - -async def setup(bot): - await bot.add_cog(Top10(bot)) +# -*- coding: utf-8 -*- +import discord +from discord.ext import commands +from utils.leaderboard_handler import show_leaderboard +from utils.helpers import normalize_difficulty +from config import BOSS_CONFIG + +class Top10(commands.Cog): + """Cog grouping all global and per-clan leaderboard commands""" + + def __init__(self, bot): + self.bot = bot + + # --- Global commands --- + @commands.command() + async def top10hydra(self, ctx, difficulty: str = None): + if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['hydra']['difficulties']: + await show_leaderboard(ctx, 'hydra', difficulty) + else: + difficulties = " | ".join(BOSS_CONFIG['hydra']['difficulties']) + await ctx.send(f"❌ Please specify difficulty: `!top10hydra `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare") + + @commands.command() + async def top10chimera(self, ctx, difficulty: str = None): + if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['chimera']['difficulties']: + await show_leaderboard(ctx, 'chimera', difficulty) + else: + difficulties = " | ".join(BOSS_CONFIG['chimera']['difficulties']) + await ctx.send(f"❌ Please specify difficulty: `!top10chimera `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra") + + @commands.command() + async def top10cvc(self, ctx): + await show_leaderboard(ctx, 'cvc') + + # --- TEAI clan commands (Inferno) --- + @commands.command() + async def teaihydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAI') + + @commands.command() + async def teaichimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAI') + + @commands.command() + async def teaicvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='TEAI') + + # --- TEAF clan commands (Flame) --- + @commands.command() + async def teafhydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAF') + + @commands.command() + async def teafchimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAF') + + @commands.command() + async def teafcvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='TEAF') + + # --- TEAC clan commands (Cinder) --- + @commands.command() + async def teachydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAC') + + @commands.command() + async def teachimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAC') + + @commands.command() + async def teaccvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='TEAC') + + # --- TEACO clan commands (Corrupted Olympians) --- + @commands.command() + async def teacohydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEACO') + + @commands.command() + async def teacochimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEACO') + + @commands.command() + async def teacocvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='TEACO') + + # --- Internal helper method --- + async def _show_clan_leaderboard(self, ctx, boss_type, difficulty, clan): + """Shows the leaderboard for a specific boss and clan""" + if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG[boss_type]['difficulties']: + await show_leaderboard(ctx, boss_type, difficulty, clan) + elif boss_type != 'cvc': # CvC has no difficulties + difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties']) + await ctx.send( + f"❌ Please specify difficulty: `!{ctx.command.name} `\n" + f"**Available:** {difficulties}\n" + f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra" + ) + else: + await show_leaderboard(ctx, boss_type, clan=clan) + +async def setup(bot): + await bot.add_cog(Top10(bot)) diff --git a/config.py b/config.py index 592aee9..c210166 100644 --- a/config.py +++ b/config.py @@ -1,51 +1,51 @@ -# -*- coding: utf-8 -*- -import os -from dotenv import load_dotenv - -load_dotenv() - -# Token et channel autorisé -DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") -AUTHORIZED_CHANNEL_ID = int(os.getenv("AUTHORIZED_CHANNEL_ID")) - -# Chemins -SCREENSHOTS_BASE_PATH = "/app/screenshots" -DATABASE_PATH = "/app/data/bot_data.db" - -# Configuration des clans TEA - The Ember Accord -CLAN_CONFIG = { - 'TEAI': {'name': 'TEAI', 'full_name': 'Inferno', 'emoji': '🔥', 'color': 0xff4500}, - 'TEAF': {'name': 'TEAF', 'full_name': 'Flame', 'emoji': '🛡️', 'color': 0x00ff00}, - 'TEAC': {'name': 'TEAC', 'full_name': 'Cinder', 'emoji': '⚔️', 'color': 0x1e90ff}, - 'TEACO': {'name': 'TEACO', 'full_name': 'Corrupted Olympians', 'emoji': '👑', 'color': 0x9932cc}, -} - -# Mapping role Discord ID → clé de clan -CLAN_ROLE_IDS = { - 1190674529731747901: 'TEAI', - 1197646966599983185: 'TEAF', - 1220014404809261076: 'TEAC', - 1496965820868198550: 'TEACO', -} - -# Mapping anciens clans → nouveaux (migration base existante) -CLAN_MIGRATION = { - 'RTF': 'TEAI', - 'RTFC': 'TEAF', - 'RTFR': 'TEAC', -} - -# Configuration des boss avec difficultés -BOSS_CONFIG = { - 'hydra': {'name': 'Hydra', 'emoji': '📍', 'color': 0xff6b35, - 'difficulties': ['normal', 'hard', 'brutal', 'nightmare']}, - 'chimera': {'name': 'Chimera', 'emoji': '🦁', 'color': 0x9932cc, - 'difficulties': ['easy', 'normal', 'hard', 'brutal', 'nightmare', 'ultra']}, - 'cvc': {'name': 'Clan vs Clan', 'emoji': '✔️', 'color': 0xff0000, 'difficulties': []} -} - -# Mappings pour diminutifs de difficultés -DIFFICULTY_SHORTCUTS = { - 'nm': 'nightmare', - 'unm': 'ultra' -} +# -*- coding: utf-8 -*- +import os +from dotenv import load_dotenv + +load_dotenv() + +# Token and authorized channel +DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") +AUTHORIZED_CHANNEL_ID = int(os.getenv("AUTHORIZED_CHANNEL_ID")) + +# Paths +SCREENSHOTS_BASE_PATH = "/app/screenshots" +DATABASE_PATH = "/app/data/bot_data.db" + +# TEA clan configuration - The Ember Accord +CLAN_CONFIG = { + 'TEAI': {'name': 'TEAI', 'full_name': 'Inferno', 'emoji': '🔥', 'color': 0xff4500}, + 'TEAF': {'name': 'TEAF', 'full_name': 'Flame', 'emoji': '🛡️', 'color': 0x00ff00}, + 'TEAC': {'name': 'TEAC', 'full_name': 'Cinder', 'emoji': '⚔️', 'color': 0x1e90ff}, + 'TEACO': {'name': 'TEACO', 'full_name': 'Corrupted Olympians', 'emoji': '👑', 'color': 0x9932cc}, +} + +# Discord role ID to clan key mapping +CLAN_ROLE_IDS = { + 1190674529731747901: 'TEAI', + 1197646966599983185: 'TEAF', + 1220014404809261076: 'TEAC', + 1496965820868198550: 'TEACO', +} + +# Old clan to new clan mapping (existing database migration) +CLAN_MIGRATION = { + 'RTF': 'TEAI', + 'RTFC': 'TEAF', + 'RTFR': 'TEAC', +} + +# Boss configuration with difficulties +BOSS_CONFIG = { + 'hydra': {'name': 'Hydra', 'emoji': '📍', 'color': 0xff6b35, + 'difficulties': ['normal', 'hard', 'brutal', 'nightmare']}, + 'chimera': {'name': 'Chimera', 'emoji': '🦁', 'color': 0x9932cc, + 'difficulties': ['easy', 'normal', 'hard', 'brutal', 'nightmare', 'ultra']}, + 'cvc': {'name': 'Clan vs Clan', 'emoji': '✔️', 'color': 0xff0000, 'difficulties': []} +} + +# Difficulty shortcut mappings +DIFFICULTY_SHORTCUTS = { + 'nm': 'nightmare', + 'unm': 'ultra' +} diff --git a/utils/DatabaseManager_class.py b/utils/DatabaseManager_class.py index 4157df8..4523b43 100644 --- a/utils/DatabaseManager_class.py +++ b/utils/DatabaseManager_class.py @@ -1,213 +1,213 @@ -# -*- coding: utf-8 -*- -from config import DATABASE_PATH, CLAN_MIGRATION -import sqlite3, os - -class DatabaseManager: - def __init__(self, db_path=DATABASE_PATH): - self.db_path = db_path - self.init_database() - - def init_database(self): - """Initialise la base de données avec les nouvelles colonnes pour les difficultés""" - os.makedirs(os.path.dirname(self.db_path), exist_ok=True) - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # Table principale avec toutes les difficultés - cursor.execute(''' - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - discord_id TEXT UNIQUE, - discord_username TEXT, - - -- Hydra difficulties - pb_hydra_normal INTEGER DEFAULT 0, - pb_hydra_normal_screenshot TEXT, - pb_hydra_normal_date TIMESTAMP, - pb_hydra_hard INTEGER DEFAULT 0, - pb_hydra_hard_screenshot TEXT, - pb_hydra_hard_date TIMESTAMP, - pb_hydra_brutal INTEGER DEFAULT 0, - pb_hydra_brutal_screenshot TEXT, - pb_hydra_brutal_date TIMESTAMP, - pb_hydra_nightmare INTEGER DEFAULT 0, - pb_hydra_nightmare_screenshot TEXT, - pb_hydra_nightmare_date TIMESTAMP, - - -- Chimera difficulties - pb_chimera_easy INTEGER DEFAULT 0, - pb_chimera_easy_screenshot TEXT, - pb_chimera_easy_date TIMESTAMP, - pb_chimera_normal INTEGER DEFAULT 0, - pb_chimera_normal_screenshot TEXT, - pb_chimera_normal_date TIMESTAMP, - pb_chimera_hard INTEGER DEFAULT 0, - pb_chimera_hard_screenshot TEXT, - pb_chimera_hard_date TIMESTAMP, - pb_chimera_brutal INTEGER DEFAULT 0, - pb_chimera_brutal_screenshot TEXT, - pb_chimera_brutal_date TIMESTAMP, - pb_chimera_nightmare INTEGER DEFAULT 0, - pb_chimera_nightmare_screenshot TEXT, - pb_chimera_nightmare_date TIMESTAMP, - pb_chimera_ultra INTEGER DEFAULT 0, - pb_chimera_ultra_screenshot TEXT, - pb_chimera_ultra_date TIMESTAMP, - - -- CvC (inchangé) - pb_cvc INTEGER DEFAULT 0, - pb_cvc_screenshot TEXT, - pb_cvc_date TIMESTAMP, - - total_attempts INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # Migration des données existantes (si nécessaire) - cursor.execute("PRAGMA table_info(users)") - columns = [row[1] for row in cursor.fetchall()] - - if 'discord_id' not in columns: - cursor.execute('ALTER TABLE users ADD COLUMN discord_id TEXT') - - if 'clan' not in columns: - cursor.execute('ALTER TABLE users ADD COLUMN clan TEXT') - # Migration automatique : déduction du clan depuis l'ancien préfixe du pseudo - for old_tag, new_clan in CLAN_MIGRATION.items(): - cursor.execute( - "UPDATE users SET clan = ? WHERE clan IS NULL AND (" - "discord_username LIKE ? OR discord_username LIKE ?)", - (new_clan, f'[{old_tag}] %', f'[{old_tag}]%') - ) - - # Table pour l'historique global - cursor.execute(''' - CREATE TABLE IF NOT EXISTS pb_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - discord_id TEXT, - username TEXT, - boss_type TEXT, - difficulty TEXT, - damage INTEGER, - screenshot_filename TEXT, - date TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - - def get_user_pb(self, user_id, boss_type, difficulty=None): - """Récupère le PB d'un utilisateur pour un boss et difficulté spécifique""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - if difficulty: - column_prefix = f"pb_{boss_type}_{difficulty}" - else: - column_prefix = f"pb_{boss_type}" - - cursor.execute( - f"SELECT {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date FROM users WHERE discord_id = ?", - (str(user_id),) - ) - result = cursor.fetchone() - conn.close() - - return result if result else (0, None, None) - - def update_user_pb(self, user_id, username, boss_type, damage, screenshot_filename, difficulty=None, clan=None): - """Met à jour le PB d'un utilisateur et supprime l'ancien screenshot""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # Récupérer l'ancien screenshot pour le supprimer - old_data = self.get_user_pb(user_id, boss_type, difficulty) - old_screenshot = old_data[1] if old_data else None - - if difficulty: - column_prefix = f"pb_{boss_type}_{difficulty}" - else: - column_prefix = f"pb_{boss_type}" - - # COALESCE(?, clan) : on n'écrase pas le clan existant si la détection retourne None - cursor.execute(f''' - INSERT INTO users (discord_id, discord_username, clan, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts) - VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, 1) - ON CONFLICT(discord_id) - DO UPDATE SET - discord_username = ?, - clan = COALESCE(?, clan), - {column_prefix} = ?, - {column_prefix}_screenshot = ?, - {column_prefix}_date = CURRENT_TIMESTAMP, - total_attempts = total_attempts + 1 - ''', (str(user_id), username, clan, damage, screenshot_filename, username, clan, damage, screenshot_filename)) - - # Ajouter à l'historique - cursor.execute(''' - INSERT INTO pb_history (discord_id, username, boss_type, difficulty, damage, screenshot_filename) - VALUES (?, ?, ?, ?, ?, ?) - ''', (str(user_id), username, boss_type, difficulty or 'none', damage, screenshot_filename)) - - conn.commit() - conn.close() - - return old_screenshot - - def get_leaderboard(self, boss_type, difficulty=None, limit=10, clan=None): - """Récupère le classement pour un boss et difficulté spécifique""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - if difficulty: - column_prefix = f"pb_{boss_type}_{difficulty}" - else: - column_prefix = f"pb_{boss_type}" - - base_query = f''' - SELECT discord_username, {column_prefix}, {column_prefix}_date, clan - FROM users - WHERE {column_prefix} > 0 - ''' - - params = [] - if clan: - base_query += ' AND clan = ?' - params.append(clan) - - base_query += f' ORDER BY {column_prefix} DESC LIMIT ?' - params.append(limit) - - cursor.execute(base_query, params) - results = cursor.fetchall() - conn.close() - return results - - def get_user_all_pbs(self, user_id): - """Récupère tous les PB d'un utilisateur""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # Récupérer toutes les colonnes de PB - cursor.execute('SELECT * FROM users WHERE discord_id = ?', (str(user_id),)) - result = cursor.fetchone() - columns = [desc[0] for desc in cursor.description] - conn.close() - - if not result: - return None - - return dict(zip(columns, result)) - - def find_user_by_name(self, username): - """Trouve un utilisateur par son nom (pour rétrocompatibilité)""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute('SELECT discord_id, discord_username FROM users WHERE discord_username LIKE ?', (f'%{username}%',)) - results = cursor.fetchall() - conn.close() - - return results \ No newline at end of file +# -*- coding: utf-8 -*- +from config import DATABASE_PATH, CLAN_MIGRATION +import sqlite3, os + +class DatabaseManager: + def __init__(self, db_path=DATABASE_PATH): + self.db_path = db_path + self.init_database() + + def init_database(self): + """Initializes the database with all columns for difficulties""" + os.makedirs(os.path.dirname(self.db_path), exist_ok=True) + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Main table with all difficulties + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + discord_id TEXT UNIQUE, + discord_username TEXT, + + -- Hydra difficulties + pb_hydra_normal INTEGER DEFAULT 0, + pb_hydra_normal_screenshot TEXT, + pb_hydra_normal_date TIMESTAMP, + pb_hydra_hard INTEGER DEFAULT 0, + pb_hydra_hard_screenshot TEXT, + pb_hydra_hard_date TIMESTAMP, + pb_hydra_brutal INTEGER DEFAULT 0, + pb_hydra_brutal_screenshot TEXT, + pb_hydra_brutal_date TIMESTAMP, + pb_hydra_nightmare INTEGER DEFAULT 0, + pb_hydra_nightmare_screenshot TEXT, + pb_hydra_nightmare_date TIMESTAMP, + + -- Chimera difficulties + pb_chimera_easy INTEGER DEFAULT 0, + pb_chimera_easy_screenshot TEXT, + pb_chimera_easy_date TIMESTAMP, + pb_chimera_normal INTEGER DEFAULT 0, + pb_chimera_normal_screenshot TEXT, + pb_chimera_normal_date TIMESTAMP, + pb_chimera_hard INTEGER DEFAULT 0, + pb_chimera_hard_screenshot TEXT, + pb_chimera_hard_date TIMESTAMP, + pb_chimera_brutal INTEGER DEFAULT 0, + pb_chimera_brutal_screenshot TEXT, + pb_chimera_brutal_date TIMESTAMP, + pb_chimera_nightmare INTEGER DEFAULT 0, + pb_chimera_nightmare_screenshot TEXT, + pb_chimera_nightmare_date TIMESTAMP, + pb_chimera_ultra INTEGER DEFAULT 0, + pb_chimera_ultra_screenshot TEXT, + pb_chimera_ultra_date TIMESTAMP, + + -- CvC (unchanged) + pb_cvc INTEGER DEFAULT 0, + pb_cvc_screenshot TEXT, + pb_cvc_date TIMESTAMP, + + total_attempts INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Existing data migration (if needed) + cursor.execute("PRAGMA table_info(users)") + columns = [row[1] for row in cursor.fetchall()] + + if 'discord_id' not in columns: + cursor.execute('ALTER TABLE users ADD COLUMN discord_id TEXT') + + if 'clan' not in columns: + cursor.execute('ALTER TABLE users ADD COLUMN clan TEXT') + # Auto-migration: derive clan from old username prefix + for old_tag, new_clan in CLAN_MIGRATION.items(): + cursor.execute( + "UPDATE users SET clan = ? WHERE clan IS NULL AND (" + "discord_username LIKE ? OR discord_username LIKE ?)", + (new_clan, f'[{old_tag}] %', f'[{old_tag}]%') + ) + + # Global history table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pb_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + discord_id TEXT, + username TEXT, + boss_type TEXT, + difficulty TEXT, + damage INTEGER, + screenshot_filename TEXT, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + conn.close() + + def get_user_pb(self, user_id, boss_type, difficulty=None): + """Returns PB for a user on a specific boss and difficulty""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + if difficulty: + column_prefix = f"pb_{boss_type}_{difficulty}" + else: + column_prefix = f"pb_{boss_type}" + + cursor.execute( + f"SELECT {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date FROM users WHERE discord_id = ?", + (str(user_id),) + ) + result = cursor.fetchone() + conn.close() + + return result if result else (0, None, None) + + def update_user_pb(self, user_id, username, boss_type, damage, screenshot_filename, difficulty=None, clan=None): + """Updates a user's PB and deletes the previous screenshot""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Get old screenshot for deletion + old_data = self.get_user_pb(user_id, boss_type, difficulty) + old_screenshot = old_data[1] if old_data else None + + if difficulty: + column_prefix = f"pb_{boss_type}_{difficulty}" + else: + column_prefix = f"pb_{boss_type}" + + # COALESCE(?, clan): keeps existing clan if detection returns None + cursor.execute(f''' + INSERT INTO users (discord_id, discord_username, clan, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts) + VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, 1) + ON CONFLICT(discord_id) + DO UPDATE SET + discord_username = ?, + clan = COALESCE(?, clan), + {column_prefix} = ?, + {column_prefix}_screenshot = ?, + {column_prefix}_date = CURRENT_TIMESTAMP, + total_attempts = total_attempts + 1 + ''', (str(user_id), username, clan, damage, screenshot_filename, username, clan, damage, screenshot_filename)) + + # Add to history + cursor.execute(''' + INSERT INTO pb_history (discord_id, username, boss_type, difficulty, damage, screenshot_filename) + VALUES (?, ?, ?, ?, ?, ?) + ''', (str(user_id), username, boss_type, difficulty or 'none', damage, screenshot_filename)) + + conn.commit() + conn.close() + + return old_screenshot + + def get_leaderboard(self, boss_type, difficulty=None, limit=10, clan=None): + """Returns the leaderboard for a specific boss and difficulty""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + if difficulty: + column_prefix = f"pb_{boss_type}_{difficulty}" + else: + column_prefix = f"pb_{boss_type}" + + base_query = f''' + SELECT discord_username, {column_prefix}, {column_prefix}_date, clan + FROM users + WHERE {column_prefix} > 0 + ''' + + params = [] + if clan: + base_query += ' AND clan = ?' + params.append(clan) + + base_query += f' ORDER BY {column_prefix} DESC LIMIT ?' + params.append(limit) + + cursor.execute(base_query, params) + results = cursor.fetchall() + conn.close() + return results + + def get_user_all_pbs(self, user_id): + """Returns all PBs for a user""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Retrieve all PB columns + cursor.execute('SELECT * FROM users WHERE discord_id = ?', (str(user_id),)) + result = cursor.fetchone() + columns = [desc[0] for desc in cursor.description] + conn.close() + + if not result: + return None + + return dict(zip(columns, result)) + + def find_user_by_name(self, username): + """Finds a user by name (for backwards compatibility)""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute('SELECT discord_id, discord_username FROM users WHERE discord_username LIKE ?', (f'%{username}%',)) + results = cursor.fetchall() + conn.close() + + return results diff --git a/utils/MercyManager_class.py b/utils/MercyManager_class.py index 9841c79..4382b84 100644 --- a/utils/MercyManager_class.py +++ b/utils/MercyManager_class.py @@ -3,7 +3,7 @@ import sqlite3 from datetime import datetime from config import DATABASE_PATH -# Règles de mercy pour stockage +# Mercy rules for storage MERCY_RULES = { "ancient": {"threshold": 200, "increment": 0.5, "base": 0}, "void": {"threshold": 200, "increment": 0.5, "base": 0}, @@ -19,7 +19,7 @@ class MercyManager: self.init_table() def init_table(self): - """Initialise la table des compteurs de mercy""" + """Initializes the mercy counters table""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(""" @@ -35,7 +35,7 @@ class MercyManager: conn.close() def get_pulls(self, user_id, shard_type): - """Retourne le nombre de pulls actuels pour un utilisateur et un type de shard""" + """Returns current pull count for a user and shard type""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute( @@ -47,11 +47,11 @@ class MercyManager: return row[0] if row else 0 def add_pulls(self, user_id, shard_type, pulls): - """Ajoute des pulls pour un utilisateur en gérant correctement l'INSERT/UPDATE""" + """Adds pulls for a user, handling INSERT/UPDATE correctly""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() - # Vérifie si l'enregistrement existe + # Check if record exists cursor.execute( "SELECT pulls FROM mercy_counters WHERE user_id = ? AND shard_type = ?", (user_id, shard_type) @@ -76,7 +76,7 @@ class MercyManager: return new_pulls def reset_pulls(self, user_id, shard_type): - """Réinitialise les pulls d'un utilisateur pour un shard""" + """Resets pull count for a user on a shard""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute( @@ -87,7 +87,7 @@ class MercyManager: conn.close() def get_all_pulls(self, user_id): - """Retourne tous les pulls d'un utilisateur pour tous les shards""" + """Returns all pull counts for a user across all shards""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute( @@ -99,14 +99,14 @@ class MercyManager: return {shard_type: pulls for shard_type, pulls in rows} def get_mercy_chance(self, shard_type, pulls): - """Calcule la probabilité de mercy selon le nombre de pulls""" + """Calculates mercy probability based on pull count""" rule = MERCY_RULES[shard_type] if pulls <= rule["threshold"]: return rule["base"] return rule["base"] + (pulls - rule["threshold"]) * rule["increment"] def pulls_until_guaranteed(self, shard_type, pulls): - """Retourne combien de pulls restent avant un loot garanti""" + """Returns pulls remaining until guaranteed loot""" rules = { "ancient": {"start": 200, "increment": 5, "base": 0.5}, "void": {"start": 200, "increment": 5, "base": 0.5}, diff --git a/utils/ScreenshotManager_class.py b/utils/ScreenshotManager_class.py index 6b1cb01..4fa1dc4 100644 --- a/utils/ScreenshotManager_class.py +++ b/utils/ScreenshotManager_class.py @@ -1,65 +1,65 @@ -# -*- coding: utf-8 -*- -import sqlite3, os -import aiohttp -from datetime import datetime -from config import SCREENSHOTS_BASE_PATH, BOSS_CONFIG - -class ScreenshotManager: - def __init__(self, base_path=SCREENSHOTS_BASE_PATH): - self.base_path = base_path - # Créer les dossiers pour chaque boss et difficulté - for boss_type in BOSS_CONFIG.keys(): - boss_path = os.path.join(base_path, boss_type) - os.makedirs(boss_path, exist_ok=True) - - # Créer sous-dossiers pour les difficultés - for difficulty in BOSS_CONFIG[boss_type]['difficulties']: - difficulty_path = os.path.join(boss_path, difficulty) - os.makedirs(difficulty_path, exist_ok=True) - - async def save_screenshot(self, attachment, username, damage, boss_type, difficulty=None): - """Sauvegarde le screenshot localement""" - try: - timestamp = int(datetime.now().timestamp()) - file_extension = attachment.filename.split('.')[-1].lower() - filename = f"{username.lower()}_{damage}_{timestamp}.{file_extension}" - - if difficulty: - boss_path = os.path.join(self.base_path, boss_type, difficulty) - else: - boss_path = os.path.join(self.base_path, boss_type) - - filepath = os.path.join(boss_path, filename) - - async with aiohttp.ClientSession() as session: - async with session.get(attachment.url) as resp: - if resp.status == 200: - # Ouverture en binaire, pas de problème d'encodage - with open(filepath, 'wb') as f: - f.write(await resp.read()) - return filename - return None - - except Exception as e: - print(f"Erreur sauvegarde screenshot: {str(e)}") - return None - - def get_screenshot_path(self, filename, boss_type, difficulty=None): - """Retourne le chemin complet du screenshot""" - if filename: - if difficulty: - return os.path.join(self.base_path, boss_type, difficulty, filename) - else: - return os.path.join(self.base_path, boss_type, filename) - return None - - def delete_old_screenshot(self, filename, boss_type, difficulty=None): - """Supprime l'ancien screenshot""" - if filename: - old_path = self.get_screenshot_path(filename, boss_type, difficulty) - if old_path and os.path.exists(old_path): - try: - os.remove(old_path) - print(f"Ancien screenshot supprimé: {filename}") - except Exception as e: - print(f"Erreur suppression screenshot: {str(e)}") +# -*- coding: utf-8 -*- +import sqlite3, os +import aiohttp +from datetime import datetime +from config import SCREENSHOTS_BASE_PATH, BOSS_CONFIG + +class ScreenshotManager: + def __init__(self, base_path=SCREENSHOTS_BASE_PATH): + self.base_path = base_path + # Create directories for each boss and difficulty + for boss_type in BOSS_CONFIG.keys(): + boss_path = os.path.join(base_path, boss_type) + os.makedirs(boss_path, exist_ok=True) + + # Create subdirectories for difficulties + for difficulty in BOSS_CONFIG[boss_type]['difficulties']: + difficulty_path = os.path.join(boss_path, difficulty) + os.makedirs(difficulty_path, exist_ok=True) + + async def save_screenshot(self, attachment, username, damage, boss_type, difficulty=None): + """Saves the screenshot locally""" + try: + timestamp = int(datetime.now().timestamp()) + file_extension = attachment.filename.split('.')[-1].lower() + filename = f"{username.lower()}_{damage}_{timestamp}.{file_extension}" + + if difficulty: + boss_path = os.path.join(self.base_path, boss_type, difficulty) + else: + boss_path = os.path.join(self.base_path, boss_type) + + filepath = os.path.join(boss_path, filename) + + async with aiohttp.ClientSession() as session: + async with session.get(attachment.url) as resp: + if resp.status == 200: + # Binary mode, no encoding issues + with open(filepath, 'wb') as f: + f.write(await resp.read()) + return filename + return None + + except Exception as e: + print(f"Screenshot save error: {str(e)}") + return None + + def get_screenshot_path(self, filename, boss_type, difficulty=None): + """Returns the full path to the screenshot""" + if filename: + if difficulty: + return os.path.join(self.base_path, boss_type, difficulty, filename) + else: + return os.path.join(self.base_path, boss_type, filename) + return None + + def delete_old_screenshot(self, filename, boss_type, difficulty=None): + """Deletes the old screenshot""" + if filename: + old_path = self.get_screenshot_path(filename, boss_type, difficulty) + if old_path and os.path.exists(old_path): + try: + os.remove(old_path) + print(f"Old screenshot deleted: {filename}") + except Exception as e: + print(f"Screenshot deletion error: {str(e)}") diff --git a/utils/helpers.py b/utils/helpers.py index cbf4cf6..ac93fd8 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -1,106 +1,106 @@ -# -*- coding: utf-8 -*- -import re -from datetime import datetime -from typing import Optional -from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS, CLAN_ROLE_IDS - -def parse_damage_amount(damage_str): - """Convertit les montants avec suffixes (K, M, B) en nombres entiers""" - if not damage_str: - return None - damage_str = damage_str.strip().upper() - if damage_str.isdigit(): - return int(damage_str) - match = re.match(r'^([0-9]*\.?[0-9]+)([KMB]?)$', damage_str) - if not match: - return None - number_str, suffix = match.groups() - try: - number = float(number_str) - except ValueError: - return None - multipliers = {'K': 1_000, 'M': 1_000_000, 'B': 1_000_000_000, '': 1} - return int(number * multipliers[suffix]) - -def format_damage_display(damage): - """Formate un montant de dégâts avec le suffixe approprié""" - if damage >= 1_000_000_000: - billions = damage / 1_000_000_000 - return f"{int(billions)}B" if billions == int(billions) else f"{billions:.1f}B" - elif damage >= 1_000_000: - millions = damage / 1_000_000 - return f"{int(millions)}M" if millions == int(millions) else f"{millions:.1f}M" - elif damage >= 1_000: - thousands = damage / 1_000 - return f"{int(thousands)}K" if thousands == int(thousands) else f"{thousands:.1f}K" - return str(damage) - -def normalize_difficulty(difficulty): - """Normalise une difficulté en gérant les diminutifs""" - if not difficulty: - return None - difficulty_lower = difficulty.lower() - if difficulty_lower in DIFFICULTY_SHORTCUTS: - return DIFFICULTY_SHORTCUTS[difficulty_lower] - return difficulty_lower - -def get_clan_from_member(member) -> Optional[str]: - """Détecte le clan d'un membre via ses rôles Discord""" - for role in member.roles: - if role.id in CLAN_ROLE_IDS: - return CLAN_ROLE_IDS[role.id] - return None - -def format_datetime(date_str): - """Formate une date en format AM/PM""" - if not date_str: - return None - try: - dt = datetime.fromisoformat(date_str) - return dt.strftime("%m/%d/%Y at %I:%M %p") - except: - return None - -def format_date_only(date_str): - """Formate une date sans l'heure""" - if not date_str: - return None - try: - dt = datetime.fromisoformat(date_str) - return dt.strftime("%m/%d/%Y") - except: - return None - -def get_difficulty_display_name(difficulty): - """Convertit le nom de difficulté en nom d'affichage""" - difficulty_names = { - 'ultra': 'Ultra Nightmare', - 'nightmare': 'Nightmare', - 'brutal': 'Brutal', - 'hard': 'Hard', - 'normal': 'Normal', - 'easy': 'Easy' - } - return difficulty_names.get(difficulty, difficulty.title()) - -def is_authorized_channel(ctx): - return ctx.channel.id == AUTHORIZED_CHANNEL_ID - -MERCY_RULES = { - "ancient": {"start": 200, "increment": 5, "base": 0.5}, - "void": {"start": 200, "increment": 5, "base": 0.5}, - "sacred": {"start": 12, "increment": 2, "base": 6}, - "primal_legendary": {"start": 75, "increment": 1, "base": 1}, - "primal_mythical": {"start": 200, "increment": 10, "base": 0.5}, - "remnant": {"start": 24, "increment": 1, "base": 0}, -} - -def calc_chance_and_guarantee(shard_type, pulls): - """Retourne chance, pull garanti et pulls restants""" - if shard_type not in MERCY_RULES: - return 0, None, None - rule = MERCY_RULES[shard_type] - chance = rule["base"] if pulls < rule["start"] else rule["base"] + (pulls - rule["start"]) * rule["increment"] - guaranteed_at = int(rule["start"] + (100 - rule["base"]) / rule["increment"]) - remaining = max(0, guaranteed_at - pulls) - return chance, guaranteed_at, remaining \ No newline at end of file +# -*- coding: utf-8 -*- +import re +from datetime import datetime +from typing import Optional +from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS, CLAN_ROLE_IDS + +def parse_damage_amount(damage_str): + """Converts amounts with suffixes (K, M, B) to integers""" + if not damage_str: + return None + damage_str = damage_str.strip().upper() + if damage_str.isdigit(): + return int(damage_str) + match = re.match(r'^([0-9]*\.?[0-9]+)([KMB]?)$', damage_str) + if not match: + return None + number_str, suffix = match.groups() + try: + number = float(number_str) + except ValueError: + return None + multipliers = {'K': 1_000, 'M': 1_000_000, 'B': 1_000_000_000, '': 1} + return int(number * multipliers[suffix]) + +def format_damage_display(damage): + """Formats a damage amount with the appropriate suffix""" + if damage >= 1_000_000_000: + billions = damage / 1_000_000_000 + return f"{int(billions)}B" if billions == int(billions) else f"{billions:.1f}B" + elif damage >= 1_000_000: + millions = damage / 1_000_000 + return f"{int(millions)}M" if millions == int(millions) else f"{millions:.1f}M" + elif damage >= 1_000: + thousands = damage / 1_000 + return f"{int(thousands)}K" if thousands == int(thousands) else f"{thousands:.1f}K" + return str(damage) + +def normalize_difficulty(difficulty): + """Normalizes a difficulty string, handling shortcuts""" + if not difficulty: + return None + difficulty_lower = difficulty.lower() + if difficulty_lower in DIFFICULTY_SHORTCUTS: + return DIFFICULTY_SHORTCUTS[difficulty_lower] + return difficulty_lower + +def get_clan_from_member(member) -> Optional[str]: + """Detects a member's clan via their Discord roles""" + for role in member.roles: + if role.id in CLAN_ROLE_IDS: + return CLAN_ROLE_IDS[role.id] + return None + +def format_datetime(date_str): + """Formats a date in AM/PM format""" + if not date_str: + return None + try: + dt = datetime.fromisoformat(date_str) + return dt.strftime("%m/%d/%Y at %I:%M %p") + except: + return None + +def format_date_only(date_str): + """Formats a date without the time component""" + if not date_str: + return None + try: + dt = datetime.fromisoformat(date_str) + return dt.strftime("%m/%d/%Y") + except: + return None + +def get_difficulty_display_name(difficulty): + """Converts an internal difficulty name to display name""" + difficulty_names = { + 'ultra': 'Ultra Nightmare', + 'nightmare': 'Nightmare', + 'brutal': 'Brutal', + 'hard': 'Hard', + 'normal': 'Normal', + 'easy': 'Easy' + } + return difficulty_names.get(difficulty, difficulty.title()) + +def is_authorized_channel(ctx): + return ctx.channel.id == AUTHORIZED_CHANNEL_ID + +MERCY_RULES = { + "ancient": {"start": 200, "increment": 5, "base": 0.5}, + "void": {"start": 200, "increment": 5, "base": 0.5}, + "sacred": {"start": 12, "increment": 2, "base": 6}, + "primal_legendary": {"start": 75, "increment": 1, "base": 1}, + "primal_mythical": {"start": 200, "increment": 10, "base": 0.5}, + "remnant": {"start": 24, "increment": 1, "base": 0}, +} + +def calc_chance_and_guarantee(shard_type, pulls): + """Returns chance, guaranteed pull count, and remaining pulls""" + if shard_type not in MERCY_RULES: + return 0, None, None + rule = MERCY_RULES[shard_type] + chance = rule["base"] if pulls < rule["start"] else rule["base"] + (pulls - rule["start"]) * rule["increment"] + guaranteed_at = int(rule["start"] + (100 - rule["base"]) / rule["increment"]) + remaining = max(0, guaranteed_at - pulls) + return chance, guaranteed_at, remaining diff --git a/utils/leaderboard_handler.py b/utils/leaderboard_handler.py index 9e0684c..4a22eef 100644 --- a/utils/leaderboard_handler.py +++ b/utils/leaderboard_handler.py @@ -1,74 +1,74 @@ -# -*- coding: utf-8 -*- -import os -import discord -from discord.ext import commands -from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG, CLAN_CONFIG -from utils.helpers import normalize_difficulty, get_difficulty_display_name, format_damage_display, format_date_only - -db_manager = None - -def set_db_manager(db): - global db_manager - db_manager = db - -async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None): - """Fonction générique pour afficher les classements""" - if ctx.channel.id != AUTHORIZED_CHANNEL_ID: - return - - try: - # Normaliser la difficulté si spécifiée - if difficulty: - difficulty = normalize_difficulty(difficulty) - if difficulty not in BOSS_CONFIG[boss_type]['difficulties']: - difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties']) - await ctx.send(f"⚠️ Invalid difficulty. Available: {difficulties}") - return - - boss_info = BOSS_CONFIG[boss_type] - leaderboard = db_manager.get_leaderboard(boss_type, difficulty, 10, clan) - - if not leaderboard: - clan_text = f" for clan {clan}" if clan else "" - difficulty_text = f" {get_difficulty_display_name(difficulty)}" if difficulty else "" - await ctx.send(f"⚠️ No{difficulty_text} {boss_info['name']} records found{clan_text} yet!") - return - - # Titre avec clan et difficulté si spécifiés - difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" - title = f"🏆 {difficulty_name} {boss_info['name']} Leaderboard - Top 10" - - if clan: - clan_info = CLAN_CONFIG.get(clan, {'name': clan, 'emoji': '🏛️'}) - title = f"{clan_info['emoji']} {clan_info['name']} - {difficulty_name} {boss_info['name']} Top 10" - - embed = discord.Embed( - title=title, - color=boss_info['color'] if not clan else CLAN_CONFIG.get(clan, {'color': boss_info['color']})['color'] - ) - - medals = ["🥇", "🥈", "🥉"] + ["🏅"] * 7 - - for i, (username, damage, date, row_clan) in enumerate(leaderboard): - date_text = "" - if date: - formatted_date = format_date_only(date) - if formatted_date: - date_text = f" • {formatted_date}" - - # Afficher l'emoji du clan si le leaderboard est global (pas filtré par clan) - display_name = username - if not clan and row_clan: - clan_emoji = CLAN_CONFIG.get(row_clan, {'emoji': '🏛️'})['emoji'] - display_name = f"{clan_emoji} {username}" - - embed.add_field( - name=f"{medals[i]} #{i+1} {display_name}", - value=f"**{format_damage_display(damage)} damage**{date_text}", - inline=False - ) - - await ctx.send(embed=embed) - - except Exception as e: - await ctx.send(f"⚠️ Error: {e}") +# -*- coding: utf-8 -*- +import os +import discord +from discord.ext import commands +from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG, CLAN_CONFIG +from utils.helpers import normalize_difficulty, get_difficulty_display_name, format_damage_display, format_date_only + +db_manager = None + +def set_db_manager(db): + global db_manager + db_manager = db + +async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None): + """Generic function to display leaderboards""" + if ctx.channel.id != AUTHORIZED_CHANNEL_ID: + return + + try: + # Normalize difficulty if specified + if difficulty: + difficulty = normalize_difficulty(difficulty) + if difficulty not in BOSS_CONFIG[boss_type]['difficulties']: + difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties']) + await ctx.send(f"⚠️ Invalid difficulty. Available: {difficulties}") + return + + boss_info = BOSS_CONFIG[boss_type] + leaderboard = db_manager.get_leaderboard(boss_type, difficulty, 10, clan) + + if not leaderboard: + clan_text = f" for clan {clan}" if clan else "" + difficulty_text = f" {get_difficulty_display_name(difficulty)}" if difficulty else "" + await ctx.send(f"⚠️ No{difficulty_text} {boss_info['name']} records found{clan_text} yet!") + return + + # Title with clan and difficulty if specified + difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" + title = f"🏆 {difficulty_name} {boss_info['name']} Leaderboard - Top 10" + + if clan: + clan_info = CLAN_CONFIG.get(clan, {'name': clan, 'emoji': '🏛️'}) + title = f"{clan_info['emoji']} {clan_info['name']} - {difficulty_name} {boss_info['name']} Top 10" + + embed = discord.Embed( + title=title, + color=boss_info['color'] if not clan else CLAN_CONFIG.get(clan, {'color': boss_info['color']})['color'] + ) + + medals = ["🥇", "🥈", "🥉"] + ["🏅"] * 7 + + for i, (username, damage, date, row_clan) in enumerate(leaderboard): + date_text = "" + if date: + formatted_date = format_date_only(date) + if formatted_date: + date_text = f" • {formatted_date}" + + # Show clan emoji if leaderboard is global (not filtered by clan) + display_name = username + if not clan and row_clan: + clan_emoji = CLAN_CONFIG.get(row_clan, {'emoji': '🏛️'})['emoji'] + display_name = f"{clan_emoji} {username}" + + embed.add_field( + name=f"{medals[i]} #{i+1} {display_name}", + value=f"**{format_damage_display(damage)} damage**{date_text}", + inline=False + ) + + await ctx.send(embed=embed) + + except Exception as e: + await ctx.send(f"⚠️ Error: {e}") diff --git a/utils/pb_handler.py b/utils/pb_handler.py index 6d209a6..071830f 100644 --- a/utils/pb_handler.py +++ b/utils/pb_handler.py @@ -1,185 +1,185 @@ -# -*- coding: utf-8 -*- -import os -import discord -from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG -from utils.helpers import ( - parse_damage_amount, - normalize_difficulty, - get_difficulty_display_name, - format_damage_display, - format_datetime, - get_clan_from_member, -) - -db_manager = None -screenshot_manager = None - -def set_managers(db, ss): - """Injection des managers (appelée une seule fois depuis bot.py)""" - global db_manager, screenshot_manager - db_manager = db - screenshot_manager = ss - -async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None): - """Fonction générique pour gérer toutes les commandes PB avec difficultés""" - if ctx.channel.id != AUTHORIZED_CHANNEL_ID: - return - - boss_info = BOSS_CONFIG[boss_type] - difficulties = boss_info['difficulties'] - - try: - # Pour CvC (pas de difficultés) - if not difficulties: - if arg1: - damage = parse_damage_amount(arg1) - if damage is not None: - await handle_pb_submission(ctx, boss_type, None, damage) - else: - await show_user_pb(ctx, boss_type, None, arg1) - else: - await show_user_pb(ctx, boss_type, None, ctx.author.display_name) - return - - # Pour Hydra et Chimera (avec difficultés) - if not arg1: - difficulty_list = " | ".join([d.title() for d in difficulties]) - await ctx.send( - f"⚠️ Please specify difficulty and damage!\n" - f"**Available difficulties:** {difficulty_list}\n" - f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare\n" - f"**Examples:**\n" - f"`!pb{boss_type} normal 1.5M` - Submit PB with screenshot\n" - f"`!pb{boss_type} nm 500K` - Submit Nightmare PB\n" - f"`!pb{boss_type} hard` - Show your Hard PB\n" - f"`!pb{boss_type} brutal username` - Show user's Brutal PB" - ) - return - - normalized_difficulty = normalize_difficulty(arg1) - - if normalized_difficulty in difficulties: - difficulty = normalized_difficulty - - if arg2: - damage = parse_damage_amount(arg2) - if damage is not None: - await handle_pb_submission(ctx, boss_type, difficulty, damage) - else: - await show_user_pb(ctx, boss_type, difficulty, arg2) - else: - await show_user_pb(ctx, boss_type, difficulty, ctx.author.display_name) - else: - difficulty_list = " | ".join([d.title() for d in difficulties]) - await ctx.send( - f"⚠️ Invalid difficulty: `{arg1}`\n" - f"**Available difficulties:** {difficulty_list}\n" - f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare" - ) - - except Exception as e: - await ctx.send(f"⚠️ Error: {str(e)}") - -async def handle_pb_submission(ctx, boss_type, difficulty, damage): - """Gère la soumission d'un nouveau PB""" - if not ctx.message.attachments: - await ctx.send("⚠️ Please attach a screenshot to validate your PB!") - return - - attachment = ctx.message.attachments[0] - if not any(attachment.filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']): - await ctx.send("⚠️ Please attach a valid image file!") - return - - user_id = ctx.author.id - username = ctx.author.display_name - clan = get_clan_from_member(ctx.author) - current_pb, _, _ = db_manager.get_user_pb(user_id, boss_type, difficulty) - - if damage > current_pb: - screenshot_filename = await screenshot_manager.save_screenshot( - attachment, username, damage, boss_type, difficulty - ) - - if screenshot_filename: - old_screenshot = db_manager.update_user_pb( - user_id, username, boss_type, damage, screenshot_filename, difficulty, clan - ) - - if old_screenshot: - screenshot_manager.delete_old_screenshot(old_screenshot, boss_type, difficulty) - - improvement = damage - current_pb if current_pb > 0 else damage - boss_info = BOSS_CONFIG[boss_type] - difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" - - embed = discord.Embed( - title=f"🎉 NEW {boss_info['name'].upper()} PB! 🎉", - description=f"**{username}** just hit **{format_damage_display(damage)} damage** on {difficulty_name} {boss_info['name']}!", - color=0x00ff00 - ) - embed.add_field(name="📈 Improvement", value=f"+{format_damage_display(improvement)} damage", inline=True) - - # Envoi du screenshot correctement pour Discord - screenshot_path = screenshot_manager.get_screenshot_path(screenshot_filename, boss_type, difficulty) - if screenshot_path and os.path.exists(screenshot_path): - file = discord.File(screenshot_path, filename=screenshot_filename) - embed.set_image(url=f"attachment://{screenshot_filename}") - await ctx.send(embed=embed, file=file) - else: - await ctx.send(embed=embed) - else: - await ctx.send("⚠️ Failed to save screenshot. Please try again.") - else: - # Si le PB n'est pas battu, on montre le PB existant - await show_user_pb(ctx, boss_type, difficulty, username) - -async def show_user_pb(ctx, boss_type, difficulty, target_user): - """Affiche le PB actuel d'un utilisateur""" - # Si target_user est un nom d'utilisateur, on essaie de le trouver - if isinstance(target_user, str) and not target_user.isdigit(): - # D'abord, vérifier si c'est l'utilisateur actuel - if target_user.lower() == ctx.author.display_name.lower(): - user_id = ctx.author.id - display_name = ctx.author.display_name - else: - # Chercher dans la base de données - matches = db_manager.find_user_by_name(target_user) - if not matches: - await ctx.send(f"⚠️ User **{target_user}** not found in database.") - return - elif len(matches) > 1: - await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.") - return - else: - user_id, display_name = matches[0] - else: - # Si c'est l'utilisateur actuel - user_id = ctx.author.id - display_name = ctx.author.display_name - - current_pb, screenshot, date = db_manager.get_user_pb(user_id, boss_type, difficulty) - boss_info = BOSS_CONFIG[boss_type] - difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" - - if current_pb > 0: - embed = discord.Embed( - title=f"📊 {display_name}'s {difficulty_name} {boss_info['name']} PB", - description=f"**{format_damage_display(current_pb)} damage**", - color=0x00bfff - ) - if date: - embed.add_field(name="📅 Date", value=format_datetime(date), inline=True) - - # Envoi du screenshot local correctement - if screenshot: - screenshot_path = screenshot_manager.get_screenshot_path(screenshot, boss_type, difficulty) - if screenshot_path and os.path.exists(screenshot_path): - file = discord.File(screenshot_path, filename=screenshot) - embed.set_image(url=f"attachment://{screenshot}") - await ctx.send(embed=embed, file=file) - return - - await ctx.send(embed=embed) - else: - await ctx.send(f"⚠️ No PB found for **{display_name}** on {difficulty_name} {boss_info['name']}.") \ No newline at end of file +# -*- coding: utf-8 -*- +import os +import discord +from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG +from utils.helpers import ( + parse_damage_amount, + normalize_difficulty, + get_difficulty_display_name, + format_damage_display, + format_datetime, + get_clan_from_member, +) + +db_manager = None +screenshot_manager = None + +def set_managers(db, ss): + """Injects managers (called once from bot.py)""" + global db_manager, screenshot_manager + db_manager = db + screenshot_manager = ss + +async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None): + """Generic handler for all PB commands""" + if ctx.channel.id != AUTHORIZED_CHANNEL_ID: + return + + boss_info = BOSS_CONFIG[boss_type] + difficulties = boss_info['difficulties'] + + try: + # For CvC (no difficulties) + if not difficulties: + if arg1: + damage = parse_damage_amount(arg1) + if damage is not None: + await handle_pb_submission(ctx, boss_type, None, damage) + else: + await show_user_pb(ctx, boss_type, None, arg1) + else: + await show_user_pb(ctx, boss_type, None, ctx.author.display_name) + return + + # For Hydra and Chimera (with difficulties) + if not arg1: + difficulty_list = " | ".join([d.title() for d in difficulties]) + await ctx.send( + f"⚠️ Please specify difficulty and damage!\n" + f"**Available difficulties:** {difficulty_list}\n" + f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare\n" + f"**Examples:**\n" + f"`!pb{boss_type} normal 1.5M` - Submit PB with screenshot\n" + f"`!pb{boss_type} nm 500K` - Submit Nightmare PB\n" + f"`!pb{boss_type} hard` - Show your Hard PB\n" + f"`!pb{boss_type} brutal username` - Show user's Brutal PB" + ) + return + + normalized_difficulty = normalize_difficulty(arg1) + + if normalized_difficulty in difficulties: + difficulty = normalized_difficulty + + if arg2: + damage = parse_damage_amount(arg2) + if damage is not None: + await handle_pb_submission(ctx, boss_type, difficulty, damage) + else: + await show_user_pb(ctx, boss_type, difficulty, arg2) + else: + await show_user_pb(ctx, boss_type, difficulty, ctx.author.display_name) + else: + difficulty_list = " | ".join([d.title() for d in difficulties]) + await ctx.send( + f"⚠️ Invalid difficulty: `{arg1}`\n" + f"**Available difficulties:** {difficulty_list}\n" + f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare" + ) + + except Exception as e: + await ctx.send(f"⚠️ Error: {str(e)}") + +async def handle_pb_submission(ctx, boss_type, difficulty, damage): + """Handles submission of a new PB""" + if not ctx.message.attachments: + await ctx.send("⚠️ Please attach a screenshot to validate your PB!") + return + + attachment = ctx.message.attachments[0] + if not any(attachment.filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']): + await ctx.send("⚠️ Please attach a valid image file!") + return + + user_id = ctx.author.id + username = ctx.author.display_name + clan = get_clan_from_member(ctx.author) + current_pb, _, _ = db_manager.get_user_pb(user_id, boss_type, difficulty) + + if damage > current_pb: + screenshot_filename = await screenshot_manager.save_screenshot( + attachment, username, damage, boss_type, difficulty + ) + + if screenshot_filename: + old_screenshot = db_manager.update_user_pb( + user_id, username, boss_type, damage, screenshot_filename, difficulty, clan + ) + + if old_screenshot: + screenshot_manager.delete_old_screenshot(old_screenshot, boss_type, difficulty) + + improvement = damage - current_pb if current_pb > 0 else damage + boss_info = BOSS_CONFIG[boss_type] + difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" + + embed = discord.Embed( + title=f"🎉 NEW {boss_info['name'].upper()} PB! 🎉", + description=f"**{username}** just hit **{format_damage_display(damage)} damage** on {difficulty_name} {boss_info['name']}!", + color=0x00ff00 + ) + embed.add_field(name="📈 Improvement", value=f"+{format_damage_display(improvement)} damage", inline=True) + + # Send screenshot to Discord + screenshot_path = screenshot_manager.get_screenshot_path(screenshot_filename, boss_type, difficulty) + if screenshot_path and os.path.exists(screenshot_path): + file = discord.File(screenshot_path, filename=screenshot_filename) + embed.set_image(url=f"attachment://{screenshot_filename}") + await ctx.send(embed=embed, file=file) + else: + await ctx.send(embed=embed) + else: + await ctx.send("⚠️ Failed to save screenshot. Please try again.") + else: + # PB not beaten, show current PB + await show_user_pb(ctx, boss_type, difficulty, username) + +async def show_user_pb(ctx, boss_type, difficulty, target_user): + """Displays the current PB for a user""" + # If target_user is a username, try to find them + if isinstance(target_user, str) and not target_user.isdigit(): + # First check if it's the current user + if target_user.lower() == ctx.author.display_name.lower(): + user_id = ctx.author.id + display_name = ctx.author.display_name + else: + # Search in database + matches = db_manager.find_user_by_name(target_user) + if not matches: + await ctx.send(f"⚠️ User **{target_user}** not found in database.") + return + elif len(matches) > 1: + await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.") + return + else: + user_id, display_name = matches[0] + else: + # Current user + user_id = ctx.author.id + display_name = ctx.author.display_name + + current_pb, screenshot, date = db_manager.get_user_pb(user_id, boss_type, difficulty) + boss_info = BOSS_CONFIG[boss_type] + difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" + + if current_pb > 0: + embed = discord.Embed( + title=f"📊 {display_name}'s {difficulty_name} {boss_info['name']} PB", + description=f"**{format_damage_display(current_pb)} damage**", + color=0x00bfff + ) + if date: + embed.add_field(name="📅 Date", value=format_datetime(date), inline=True) + + # Send local screenshot to Discord + if screenshot: + screenshot_path = screenshot_manager.get_screenshot_path(screenshot, boss_type, difficulty) + if screenshot_path and os.path.exists(screenshot_path): + file = discord.File(screenshot_path, filename=screenshot) + embed.set_image(url=f"attachment://{screenshot}") + await ctx.send(embed=embed, file=file) + return + + await ctx.send(embed=embed) + else: + await ctx.send(f"⚠️ No PB found for **{display_name}** on {difficulty_name} {boss_info['name']}.")