From 1b02dd8b222813f76f035957769312a29a9e98e1 Mon Sep 17 00:00:00 2001 From: ArcElewyn Date: Tue, 26 Aug 2025 19:35:54 +0200 Subject: [PATCH] nettoyage et standardisation --- bot.py | 20 +++-- cogs/guide.py | 106 +++++++++++++------------- cogs/mercy.py | 74 +++++++++++++----- cogs/mystats.py | 69 ++++++++--------- cogs/pbchimera.py | 6 +- cogs/pbcvc.py | 8 +- cogs/pbhydra.py | 6 +- cogs/top10.py | 9 ++- config.py | 20 ++--- docker-compose.yml | 6 +- utils/DatabaseManager_class.py | 11 +-- utils/MercyManager_class.py | 27 +++++-- utils/ScreenshotManager_class.py | 6 +- utils/helpers.py | 19 ++--- utils/leaderboard_handler.py | 23 +++--- utils/pb_handler.py | 126 ++++++++++++------------------- 16 files changed, 286 insertions(+), 250 deletions(-) diff --git a/bot.py b/bot.py index ca5c6f4..63c7c1a 100644 --- a/bot.py +++ b/bot.py @@ -1,18 +1,24 @@ +# -*- coding: utf-8 -*- import os -import asyncio import discord from discord.ext import commands from config import DISCORD_TOKEN + +# Import des managers from utils.DatabaseManager_class import DatabaseManager from utils.ScreenshotManager_class import ScreenshotManager +# Définir les intents intents = discord.Intents.default() intents.message_content = True + bot = commands.Bot(command_prefix="!", intents=intents) +# Initialisation unique des managers db_manager = DatabaseManager() screenshot_manager = ScreenshotManager() +# Liste des Cogs à charger initial_cogs = [ "cogs.guide", "cogs.pbhydra", @@ -23,7 +29,7 @@ initial_cogs = [ "cogs.mercy", ] -async def load_all_cogs(): +async def load_cogs(): for cog in initial_cogs: try: await bot.load_extension(cog) @@ -35,10 +41,8 @@ async def load_all_cogs(): async def on_ready(): print(f"{bot.user.name} est connecté !") -async def main(): - await load_all_cogs() - await bot.start(DISCORD_TOKEN) - -if __name__ == "__main__": - asyncio.run(main()) +# Charger les cogs avant le run +bot.loop.create_task(load_cogs()) +# Lancer le bot +bot.run(DISCORD_TOKEN) diff --git a/cogs/guide.py b/cogs/guide.py index 3c260b5..d6853aa 100644 --- a/cogs/guide.py +++ b/cogs/guide.py @@ -1,114 +1,118 @@ +# -*- 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 la liste des commandes disponibles avec les nouvelles difficultés""" + """Affiche toutes les commandes disponibles avec les difficultés""" if ctx.channel.id != AUTHORIZED_CHANNEL_ID: return - + embed = discord.Embed( - title="🤖 RTF Bot - Commands Guide", + title="🧐 RTF Bot - Commands Guide", description="Here are all available commands for tracking your Personal Bests!", color=0x00bfff ) - - # Info sur les formats de dégâts + + # 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" + + 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" + + 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" + + 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" + + 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_legendary, primal_mythical, remnant", + 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" + + 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="**RTF:** `!rtfhydra ` `!rtfchimera ` `!rtfcvc`\n" + - "**RTFC:** `!rtfchydra ` `!rtfcchimera ` `!rtfccvc`\n" + + name="🏆 Clan Leaderboards", + value="**RTF:** `!rtfhydra ` `!rtfchimera ` `!rtfcvc`\n" + "**RTFC:** `!rtfchydra ` `!rtfcchimera ` `!rtfccvc`\n" "**RTFR:** `!rtfrhydra ` `!rtfrchimera ` `!rtfrcvc`", inline=False ) - + # Stats et aide embed.add_field( - name="📈 Stats & Info", - value="`!mystats` - View all your PBs\n" + - "`!mystats ` - View someone's PBs\n" + + 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 ancient` - Add 50 pulls to Ancient shard\n" + - "`!mercy show` - Show your mercy pulls\n" + - "`!rtfhydra nm` - RTF clan Nightmare rankings\n" + + 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" + "`!rtfhydra nm` - RTF 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!") - + + 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 aadb857..b7fde74 100644 --- a/cogs/mercy.py +++ b/cogs/mercy.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import discord from discord.ext import commands from config import AUTHORIZED_CHANNEL_ID @@ -7,6 +8,8 @@ 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""" + def __init__(self, bot): self.bot = bot self.mercy_manager = MercyManager() @@ -18,61 +21,96 @@ class Mercy(commands.Cog): user_id = str(ctx.author.id) + # ----- SHOW ----- if action == "show": pulls_dict = self.mercy_manager.get_all_pulls(user_id) if not pulls_dict: - await ctx.send("❌ You don't have any mercy data yet.") + await ctx.send("ℹ️ You don't have any mercy data yet.") return - embed = discord.Embed(title=f"🎲 Mercy Status for {ctx.author.display_name}", color=0x00bfff) + embed = discord.Embed( + title=f"🎲 Mercy Status for {ctx.author.display_name}", + color=0x00bfff + ) for shard_type, pulls in pulls_dict.items(): if shard_type == "primal": for sub_type in ["primal_legendary", "primal_mythical"]: - chance, guaranteed_at = self.calc_chance_and_guarantee(sub_type, pulls) - guaranteed_text = f" (Guaranteed at {int(guaranteed_at)} pulls)" if guaranteed_at else "" + sub_pulls = pulls_dict.get(sub_type, 0) + chance, guaranteed_at, remaining = calc_chance_and_guarantee(sub_type, sub_pulls) + guaranteed_text = f" (Guaranteed at {guaranteed_at} pulls, {remaining} remaining)" if guaranteed_at else "" embed.add_field( name=sub_type.replace("_", " ").title(), - value=f"Pulled: **{pulls} shards** → {chance:.1f}% chance{guaranteed_text}", + value=f"Pulled: **{sub_pulls}/{guaranteed_at} shards** → {chance:.1f}% chance{guaranteed_text}", inline=False ) else: - chance, guaranteed_at = self.calc_chance_and_guarantee(shard_type, pulls) - guaranteed_text = f" (Guaranteed at {int(guaranteed_at)} pulls)" if guaranteed_at else "" + chance, guaranteed_at, remaining = calc_chance_and_guarantee(shard_type, pulls) + guaranteed_text = f" (Guaranteed at {guaranteed_at} pulls, {remaining} remaining)" if guaranteed_at else "" embed.add_field( name=shard_type.replace("_", " ").title(), - value=f"Pulled: **{pulls} shards** → {chance:.1f}% chance{guaranteed_text}", + value=f"Pulled: **{pulls}/{guaranteed_at} shards** → {chance:.1f}% chance{guaranteed_text}", inline=False ) await ctx.send(embed=embed) + # ----- ADD ----- elif action == "add" and arg1 and arg2: try: pulls_to_add = int(arg1) except ValueError: - await ctx.send("❌ Number of pulls must be an integer.") + await ctx.send("❌ Number of pulls must be an integer.") return shard_type = arg2.lower() if shard_type not in VALID_SHARDS: - await ctx.send(f"❌ Invalid shard type. Available: {', '.join(VALID_SHARDS)}") + await ctx.send(f"❌ Invalid shard type. Available: {', '.join(VALID_SHARDS)}") return - new_pulls = self.mercy_manager.add_pulls(user_id, shard_type, pulls_to_add) - await ctx.send(f"✅ Added {pulls_to_add} pulls to **{shard_type}** mercy. Total: {new_pulls}") + if shard_type == "primal": + messages = [] + for sub_type in ["primal_legendary", "primal_mythical"]: + new_pulls = self.mercy_manager.add_pulls(user_id, sub_type, pulls_to_add) + chance, guaranteed_at, remaining = calc_chance_and_guarantee(sub_type, new_pulls) + messages.append(f"✅ Added {pulls_to_add} pulls to **{sub_type.replace('_', ' ').title()}**: {new_pulls}/{guaranteed_at} → {chance:.1f}% ({remaining} remaining)") + await ctx.send("\n".join(messages)) + else: + new_pulls = self.mercy_manager.add_pulls(user_id, shard_type, pulls_to_add) + chance, guaranteed_at, remaining = calc_chance_and_guarantee(shard_type, new_pulls) + await ctx.send(f"✅ Added {pulls_to_add} pulls to **{shard_type}** mercy. Now: **{new_pulls}/{guaranteed_at}** pulls → {chance:.1f}% chance ({remaining} remaining)") + # ----- RESET ----- elif action == "reset" and arg1: shard_type = arg1.lower() - if shard_type not in VALID_SHARDS: - await ctx.send(f"❌ Invalid shard type. Available: {', '.join(VALID_SHARDS)}") - return + sub_type = arg2.lower() if arg2 else None - self.mercy_manager.reset_pulls(user_id, shard_type) - await ctx.send(f"🔄 Mercy for **{shard_type}** has been reset.") + if shard_type == "primal": + if sub_type == "legendary": + self.mercy_manager.reset_pulls(user_id, "primal_legendary") + await ctx.send("🧾 Mercy for Primal Legendary has been reset.") + elif sub_type == "mythical": + self.mercy_manager.reset_pulls(user_id, "primal_mythical") + await ctx.send("🧾 Mercy for Primal Mythical has been reset.") + elif sub_type is None: + messages = [] + for s in ["primal_legendary", "primal_mythical"]: + self.mercy_manager.reset_pulls(user_id, s) + messages.append(f"🧾 Mercy for {s.replace('_', ' ').title()} has been reset.") + await ctx.send("\n".join(messages)) + else: + await ctx.send("❌ Invalid primal subtype. Use `legendary` or `mythical`.") + else: + if shard_type not in VALID_SHARDS: + await ctx.send(f"❌ Invalid shard type. Available: {', '.join(VALID_SHARDS)}") + return + self.mercy_manager.reset_pulls(user_id, shard_type) + await ctx.send(f"🧾 Mercy for **{shard_type}** has been reset.") + # ----- HELP ----- else: - await ctx.send("❌ Usage: `!mercy add `, `!mercy reset `, `!mercy show`") + await ctx.send("ℹ️ Usage: `!mercy add `, `!mercy reset [subtype]`, `!mercy show`") + async def setup(bot): await bot.add_cog(Mercy(bot)) diff --git a/cogs/mystats.py b/cogs/mystats.py index 9e25528..96b9f84 100644 --- a/cogs/mystats.py +++ b/cogs/mystats.py @@ -1,63 +1,66 @@ +# -*- 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 # ou set_db_manager si nécessaire +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""" + """Affiche tous les PB d'un utilisateur avec les nouvelles difficultés""" if ctx.channel.id != AUTHORIZED_CHANNEL_ID: return - + try: username = target_user if target_user else ctx.author.display_name user_data = db_manager.get_user_all_pbs(username) - + if not user_data: - await ctx.send(f"❌ No data found for **{username}**.") + await ctx.send(f"❌ No data found for **{username}**.") return - + embed = discord.Embed( - title=f"📊 {username}'s Complete Stats", + title=f"📊 {username}'s Complete Stats", color=0x00bfff ) - - # Hydra - toutes les difficultés + + # 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 "" + 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 + 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 "" + 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) - + 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') @@ -65,25 +68,19 @@ class MyStats(commands.Cog): 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 = 0 - for difficulty in BOSS_CONFIG['hydra']['difficulties']: - total_damage += user_data.get(f'pb_hydra_{difficulty}', 0) - for difficulty in BOSS_CONFIG['chimera']['difficulties']: - total_damage += user_data.get(f'pb_chimera_{difficulty}', 0) + 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) - + 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}") + await ctx.send(f"❌ Error: {e}") - -# Pour charger le Cog async def setup(bot): await bot.add_cog(MyStats(bot)) diff --git a/cogs/pbchimera.py b/cogs/pbchimera.py index ff542a8..4511fed 100644 --- a/cogs/pbchimera.py +++ b/cogs/pbchimera.py @@ -1,9 +1,11 @@ +# -*- coding: utf-8 -*- import discord from discord.ext import commands -from config import AUTHORIZED_CHANNEL_ID 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 @@ -13,4 +15,4 @@ class Pbchimera(commands.Cog): await handle_pb_command(ctx, 'chimera', arg1, arg2) async def setup(bot): - await bot.add_cog(Pbchimera(bot)) \ No newline at end of file + await bot.add_cog(Pbchimera(bot)) diff --git a/cogs/pbcvc.py b/cogs/pbcvc.py index ea2cdb2..a5da460 100644 --- a/cogs/pbcvc.py +++ b/cogs/pbcvc.py @@ -1,16 +1,18 @@ +# -*- coding: utf-8 -*- import discord from discord.ext import commands -from config import AUTHORIZED_CHANNEL_ID 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 (sans difficultées)""" + """Commande !pbcvc""" await handle_pb_command(ctx, 'cvc', target_user) async def setup(bot): - await bot.add_cog(Pbcvc(bot)) \ No newline at end of file + await bot.add_cog(Pbcvc(bot)) diff --git a/cogs/pbhydra.py b/cogs/pbhydra.py index d4ea60f..b1fecfa 100644 --- a/cogs/pbhydra.py +++ b/cogs/pbhydra.py @@ -1,9 +1,11 @@ +# -*- coding: utf-8 -*- import discord from discord.ext import commands -from config import AUTHORIZED_CHANNEL_ID 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 @@ -13,4 +15,4 @@ class Pbhydra(commands.Cog): await handle_pb_command(ctx, 'hydra', arg1, arg2) async def setup(bot): - await bot.add_cog(Pbhydra(bot)) \ No newline at end of file + await bot.add_cog(Pbhydra(bot)) diff --git a/cogs/top10.py b/cogs/top10.py index 6958499..15ba344 100644 --- a/cogs/top10.py +++ b/cogs/top10.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import discord from discord.ext import commands from utils.leaderboard_handler import show_leaderboard @@ -77,9 +78,13 @@ class Top10(commands.Cog): 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**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra") + 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)) \ No newline at end of file + await bot.add_cog(Top10(bot)) diff --git a/config.py b/config.py index cfca3f9..ba492df 100644 --- a/config.py +++ b/config.py @@ -8,26 +8,26 @@ DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") AUTHORIZED_CHANNEL_ID = int(os.getenv("AUTHORIZED_CHANNEL_ID")) # Chemins -SCREENSHOTS_BASE_PATH = "/share/Container/discord-bot/screenshots" -DATABASE_PATH = "/share/Container/discord-bot/bot_data.db" +SCREENSHOTS_BASE_PATH = "/app/screenshots" +DATABASE_PATH = "/app/data/bot_data.db" # Configuration des clans CLAN_CONFIG = { - 'RTF': {'name': 'RTF', 'emoji': '⭐', 'color': 0x00ff00}, - 'RTFC': {'name': 'RTFC', 'emoji': '🔥', 'color': 0xff4500}, - 'RTFR': {'name': 'RTFR', 'emoji': '⚡', 'color': 0x1e90ff} + 'RTF': {'name': 'RTF', 'emoji': '⭐', 'color': 0x00ff00}, + 'RTFC': {'name': 'RTFC', 'emoji': '🔥', 'color': 0xff4500}, + 'RTFR': {'name': 'RTFR', 'emoji': 'âš¡', 'color': 0x1e90ff} } -# Configuration des boss avec difficultés +# Configuration des boss avec difficultées BOSS_CONFIG = { - 'hydra': {'name': 'Hydra', 'emoji': '🐍', 'color': 0xff6b35, + 'hydra': {'name': 'Hydra', 'emoji': '🐍', 'color': 0xff6b35, 'difficulties': ['normal', 'hard', 'brutal', 'nightmare']}, - 'chimera': {'name': 'Chimera', 'emoji': '🦁', 'color': 0x9932cc, + 'chimera': {'name': 'Chimera', 'emoji': '🦁', 'color': 0x9932cc, 'difficulties': ['easy', 'normal', 'hard', 'brutal', 'nightmare', 'ultra']}, - 'cvc': {'name': 'Clan vs Clan', 'emoji': '⚔️', 'color': 0xff0000, 'difficulties': []} + 'cvc': {'name': 'Clan vs Clan', 'emoji': '⚔️', 'color': 0xff0000, 'difficulties': []} } -# Mappings pour diminutifs de difficultés +# Mappings pour diminutifs de difficultés DIFFICULTY_SHORTCUTS = { 'nm': 'nightmare', 'unm': 'ultra' diff --git a/docker-compose.yml b/docker-compose.yml index b5188f0..63c3d5b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,11 +7,11 @@ services: env_file: - .env volumes: - # Montre le code source pour hot-reload + # Monte le code source pour hot-reload - ./:/app - # Montre les dossiers de stockage persistants + # Monte les dossiers de stockage persistants - ./screenshots:/app/screenshots - - ./bot_data.db:/app/bot_data.db + - ./data:/app/data - ./logs:/app/logs environment: - TZ=Europe/Paris diff --git a/utils/DatabaseManager_class.py b/utils/DatabaseManager_class.py index fd45f60..d1eac49 100644 --- a/utils/DatabaseManager_class.py +++ b/utils/DatabaseManager_class.py @@ -1,5 +1,7 @@ +# -*- coding: utf-8 -*- from config import DATABASE_PATH import sqlite3, os + class DatabaseManager: def __init__(self, db_path=DATABASE_PATH): self.db_path = db_path @@ -51,7 +53,7 @@ class DatabaseManager: pb_chimera_ultra_screenshot TEXT, pb_chimera_ultra_date TIMESTAMP, - -- CvC (unchanged) + -- CvC (inchangé) pb_cvc INTEGER DEFAULT 0, pb_cvc_screenshot TEXT, pb_cvc_date TIMESTAMP, @@ -170,11 +172,10 @@ class DatabaseManager: # Récupérer toutes les colonnes de PB cursor.execute('SELECT * FROM users WHERE discord_username = ?', (username.lower(),)) result = cursor.fetchone() + columns = [desc[0] for desc in cursor.description] conn.close() if not result: return None - - # Convertir en dictionnaire pour faciliter l'accès - columns = [desc[0] for desc in cursor.description] - return dict(zip(columns, result)) if result else None + + return dict(zip(columns, result)) diff --git a/utils/MercyManager_class.py b/utils/MercyManager_class.py index 3d9939d..9841c79 100644 --- a/utils/MercyManager_class.py +++ b/utils/MercyManager_class.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import sqlite3 from datetime import datetime from config import DATABASE_PATH @@ -18,6 +19,7 @@ class MercyManager: self.init_table() def init_table(self): + """Initialise la table des compteurs de mercy""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(""" @@ -33,6 +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""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute( @@ -44,24 +47,36 @@ class MercyManager: return row[0] if row else 0 def add_pulls(self, user_id, shard_type, pulls): - current = self.get_pulls(user_id, shard_type) + """Ajoute des pulls pour un utilisateur en gérant correctement l'INSERT/UPDATE""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() - if current: + + # Vérifie si l'enregistrement existe + cursor.execute( + "SELECT pulls FROM mercy_counters WHERE user_id = ? AND shard_type = ?", + (user_id, shard_type) + ) + row = cursor.fetchone() + + if row: + new_pulls = row[0] + pulls cursor.execute( "UPDATE mercy_counters SET pulls = ?, last_reset = ? WHERE user_id = ? AND shard_type = ?", - (current + pulls, datetime.utcnow(), user_id, shard_type) + (new_pulls, datetime.utcnow(), user_id, shard_type) ) else: + new_pulls = pulls cursor.execute( "INSERT INTO mercy_counters (user_id, shard_type, pulls, last_reset) VALUES (?, ?, ?, ?)", - (user_id, shard_type, pulls, datetime.utcnow()) + (user_id, shard_type, new_pulls, datetime.utcnow()) ) + conn.commit() conn.close() - return current + pulls + return new_pulls def reset_pulls(self, user_id, shard_type): + """Réinitialise les pulls d'un utilisateur pour un shard""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute( @@ -72,6 +87,7 @@ class MercyManager: conn.close() def get_all_pulls(self, user_id): + """Retourne tous les pulls d'un utilisateur pour tous les shards""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute( @@ -83,6 +99,7 @@ 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""" rule = MERCY_RULES[shard_type] if pulls <= rule["threshold"]: return rule["base"] diff --git a/utils/ScreenshotManager_class.py b/utils/ScreenshotManager_class.py index 86028d9..6b1cb01 100644 --- a/utils/ScreenshotManager_class.py +++ b/utils/ScreenshotManager_class.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import sqlite3, os import aiohttp from datetime import datetime @@ -33,13 +34,14 @@ class ScreenshotManager: 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: {e}") + print(f"Erreur sauvegarde screenshot: {str(e)}") return None def get_screenshot_path(self, filename, boss_type, difficulty=None): @@ -60,4 +62,4 @@ class ScreenshotManager: os.remove(old_path) print(f"Ancien screenshot supprimé: {filename}") except Exception as e: - print(f"Erreur suppression screenshot: {e}") \ No newline at end of file + print(f"Erreur suppression screenshot: {str(e)}") diff --git a/utils/helpers.py b/utils/helpers.py index a73ff67..d5eb931 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import re from datetime import datetime from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS @@ -17,7 +18,7 @@ def parse_damage_amount(damage_str): number = float(number_str) except ValueError: return None - multipliers = {'K': 1000, 'M': 1_000_000, 'B': 1_000_000_000, '': 1} + multipliers = {'K': 1_000, 'M': 1_000_000, 'B': 1_000_000_000, '': 1} return int(number * multipliers[suffix]) def format_damage_display(damage): @@ -37,31 +38,22 @@ def normalize_difficulty(difficulty): """Normalise une difficulté en gérant les diminutifs""" if not difficulty: return None - difficulty_lower = difficulty.lower() - - # Vérifier les diminutifs d'abord if difficulty_lower in DIFFICULTY_SHORTCUTS: return DIFFICULTY_SHORTCUTS[difficulty_lower] - - # Sinon retourner tel quel return difficulty_lower -# Fonctions utilitaires def get_user_clan(username): - """Détermine le clan d'un utilisateur basé sur son pseudo - Version corrigée""" + """Détermine le clan d'un utilisateur basé sur son pseudo""" username_upper = username.upper() - - # Chercher les tags avec crochets et espace + # Tags avec crochets et espace for clan_tag in ['[RTF] ', '[RTFC] ', '[RTFR] ']: if username_upper.startswith(clan_tag): return clan_tag.replace('[', '').replace(']', '').strip() - - # Chercher les tags avec crochets sans espace + # Tags avec crochets sans espace for clan_tag in ['[RTF]', '[RTFC]', '[RTFR]']: if username_upper.startswith(clan_tag): return clan_tag.replace('[', '').replace(']', '') - return None def format_datetime(date_str): @@ -117,4 +109,3 @@ def calc_chance_and_guarantee(shard_type, pulls): 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 6bc5ce0..b46b58c 100644 --- a/utils/leaderboard_handler.py +++ b/utils/leaderboard_handler.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os import discord from discord.ext import commands @@ -11,17 +12,17 @@ def set_db_manager(db): db_manager = db async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None): - """Fonction générique pour afficher les classements""" + """Fonction générique pour afficher les classements""" if ctx.channel.id != AUTHORIZED_CHANNEL_ID: return try: - # Normaliser la difficulté si spécifiée + # 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}") + await ctx.send(f"⚠️ Invalid difficulty. Available: {difficulties}") return boss_info = BOSS_CONFIG[boss_type] @@ -30,15 +31,15 @@ async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None): 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!") + 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 + # 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" + title = f"🏆 {difficulty_name} {boss_info['name']} Leaderboard - Top 10" if clan: - clan_info = CLAN_CONFIG.get(clan, {'name': clan, 'emoji': '🏛️'}) + 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( @@ -46,21 +47,21 @@ async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None): color=boss_info['color'] if not clan else CLAN_CONFIG.get(clan, {'color': boss_info['color']})['color'] ) - medals = ["🥇", "🥈", "🥉"] + ["🏅"] * 7 + medals = ["🥇", "🥈", "🥉"] + ["🏅"] * 7 for i, (username, damage, date) in enumerate(leaderboard): date_text = "" if date: formatted_date = format_date_only(date) if formatted_date: - date_text = f" • {formatted_date}" + date_text = f" • {formatted_date}" # Afficher le clan dans le nom si pas de filtre par clan display_name = username if not clan: user_clan = get_user_clan(username) if user_clan: - clan_emoji = CLAN_CONFIG.get(user_clan, {'emoji': '🏛️'})['emoji'] + clan_emoji = CLAN_CONFIG.get(user_clan, {'emoji': '🏛️'})['emoji'] display_name = f"{clan_emoji} {username}" embed.add_field( @@ -72,4 +73,4 @@ async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None): await ctx.send(embed=embed) except Exception as e: - await ctx.send(f"❌ Error: {e}") \ No newline at end of file + await ctx.send(f"⚠️ Error: {e}") diff --git a/utils/pb_handler.py b/utils/pb_handler.py index 0a398a8..b2ae68c 100644 --- a/utils/pb_handler.py +++ b/utils/pb_handler.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os import discord from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG @@ -13,13 +14,13 @@ db_manager = None screenshot_manager = None def set_managers(db, ss): - """Injection des managers (appelée une seule fois depuis bot.py)""" + """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""" + """Fonction générique pour gérer toutes les commandes PB avec difficultés""" if ctx.channel.id != AUTHORIZED_CHANNEL_ID: return @@ -27,25 +28,23 @@ async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None): difficulties = boss_info['difficulties'] try: - # Pour CvC (pas de difficultés) + # Pour CvC (pas de difficultés) if not difficulties: - # Utiliser l'ancienne logique pour CvC avec parsing des montants if arg1: damage = parse_damage_amount(arg1) if damage is not None: await handle_pb_submission(ctx, boss_type, None, damage) - else: # Username + else: await show_user_pb(ctx, boss_type, None, arg1) - else: # Montrer son propre PB + else: await show_user_pb(ctx, boss_type, None, ctx.author.display_name) return - # Pour Hydra et Chimera (avec difficultés) + # Pour Hydra et Chimera (avec difficultés) if not arg1: - # !pbhydra sans arguments - montrer aide difficulty_list = " | ".join([d.title() for d in difficulties]) await ctx.send( - f"❌ Please specify difficulty and damage!\n" + f"⚠️ Please specify difficulty and damage!\n" f"**Available difficulties:** {difficulty_list}\n" f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare\n" f"**Examples:**\n" @@ -56,63 +55,54 @@ async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None): ) return - # Normaliser la difficulté (gérer les diminutifs) normalized_difficulty = normalize_difficulty(arg1) - # Vérifier si arg1 est une difficulté valide if normalized_difficulty in difficulties: difficulty = normalized_difficulty if arg2: damage = parse_damage_amount(arg2) if damage is not None: - # !pbhydra normal 1.5M - Soumission PB await handle_pb_submission(ctx, boss_type, difficulty, damage) else: - # !pbhydra normal username - Voir PB d'un utilisateur await show_user_pb(ctx, boss_type, difficulty, arg2) else: - # !pbhydra normal - Voir son propre PB await show_user_pb(ctx, boss_type, difficulty, ctx.author.display_name) else: - # arg1 n'est pas une difficulté valide difficulty_list = " | ".join([d.title() for d in difficulties]) await ctx.send( - f"❌ Invalid difficulty: `{arg1}`\n" + 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: {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""" + """Gère la soumission d'un nouveau PB""" if not ctx.message.attachments: - await ctx.send("❌ Please attach a screenshot to validate your PB!") + 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!") + await ctx.send("⚠️ Please attach a valid image file!") return username = ctx.author.display_name current_pb, _, _ = db_manager.get_user_pb(username, boss_type, difficulty) if damage > current_pb: - # Sauvegarder la nouvelle screenshot screenshot_filename = await screenshot_manager.save_screenshot( attachment, username, damage, boss_type, difficulty ) if screenshot_filename: - # Mettre à jour la base et récupérer l'ancien screenshot old_screenshot = db_manager.update_user_pb( username, boss_type, damage, screenshot_filename, difficulty ) - # Supprimer l'ancien screenshot if old_screenshot: screenshot_manager.delete_old_screenshot(old_screenshot, boss_type, difficulty) @@ -121,70 +111,50 @@ async def handle_pb_submission(ctx, boss_type, difficulty, damage): difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" embed = discord.Embed( - title=f"🎉 NEW {boss_info['name'].upper()} PB! 🎉", + 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) - embed.set_image(url=attachment.url) - - await ctx.send(embed=embed) + 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.") + await ctx.send("⚠️ Failed to save screenshot. Please try again.") else: - difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" - embed = discord.Embed( - title="💪 Nice attempt!", - description=f"Your damage: **{format_damage_display(damage)}**\nCurrent PB: **{format_damage_display(current_pb)}**", - color=0xffa500 - ) - embed.add_field( - name="Keep going!", - value=f"You need **{format_damage_display(current_pb - damage + 1)}** more damage for a new {difficulty_name} PB!", - inline=False - ) - await ctx.send(embed=embed) + # 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, username): - """Affiche le PB d'un utilisateur""" - pb_data = db_manager.get_user_pb(username, boss_type, difficulty) - pb_damage, screenshot_filename, pb_date = pb_data - +async def show_user_pb(ctx, boss_type, difficulty, target_user): + """Affiche le PB actuel d'un utilisateur""" + current_pb, screenshot, date = db_manager.get_user_pb(target_user, boss_type, difficulty) boss_info = BOSS_CONFIG[boss_type] difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" - if pb_damage == 0: + if current_pb > 0: embed = discord.Embed( - title=f"{boss_info['emoji']} {username}'s {difficulty_name} {boss_info['name']} PB", - description="**No record yet**", - color=0x666666 - ) - embed.add_field( - name="💡 Get started!", - value=f"Use `!pb{boss_type} {difficulty} ` with a screenshot to set your first record!\nAccepts K/M/B suffixes: `1.5M`, `500K`, etc.", - inline=False + title=f"📊 {target_user}'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) - return - - embed = discord.Embed( - title=f"{boss_info['emoji']} {username}'s {difficulty_name} {boss_info['name']} PB", - description=f"**{format_damage_display(pb_damage)} damage**", - color=boss_info['color'] - ) - if pb_date: - formatted_date = format_datetime(pb_date) - if formatted_date: - embed.add_field(name="📅 Record Date", value=formatted_date, inline=False) - - # Envoyer la screenshot si elle existe - if screenshot_filename: - 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=f"{username}_{boss_type}_{difficulty}_pb.png") - embed.set_image(url=f"attachment://{username}_{boss_type}_{difficulty}_pb.png") - await ctx.send(embed=embed, file=file) - return - - await ctx.send(embed=embed) + else: + await ctx.send(f"⚠️ No PB found for **{target_user}** on {difficulty_name} {boss_info['name']}.")