From 768c4bc3f38d1d20f8056968f3dc54b0670b9f04 Mon Sep 17 00:00:00 2001 From: mleberre Date: Fri, 22 Aug 2025 15:25:34 +0200 Subject: [PATCH] Refactor: ajout des cogs et handlers --- .env | 2 + Dockerfile | 25 + README.md | 38 +- RTF.py | 955 ------------------------------- bot.py | 35 ++ cogs/guide.py | 102 ++++ cogs/mystats.py | 89 +++ cogs/pbchimera.py | 16 + cogs/pbcvc.py | 16 + cogs/pbhydra.py | 16 + cogs/top10.py | 84 +++ config.py | 34 ++ docker-compose.yml | 23 + requirements.txt | 3 + utils/DatabaseManager_class.py | 180 ++++++ utils/ScreenshotManager_class.py | 63 ++ utils/helpers.py | 101 ++++ utils/leaderboard_handler.py | 75 +++ utils/pb_handler.py | 190 ++++++ 19 files changed, 1075 insertions(+), 972 deletions(-) create mode 100644 .env create mode 100644 Dockerfile delete mode 100644 RTF.py create mode 100644 bot.py create mode 100644 cogs/guide.py create mode 100644 cogs/mystats.py create mode 100644 cogs/pbchimera.py create mode 100644 cogs/pbcvc.py create mode 100644 cogs/pbhydra.py create mode 100644 cogs/top10.py create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 requirements.txt create mode 100644 utils/DatabaseManager_class.py create mode 100644 utils/ScreenshotManager_class.py create mode 100644 utils/helpers.py create mode 100644 utils/leaderboard_handler.py create mode 100644 utils/pb_handler.py diff --git a/.env b/.env new file mode 100644 index 0000000..be8fefa --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DISCORD_TOKEN=your_bot_token_here +AUTHORIZED_CHANNEL_ID=your_channel_id_here \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8051df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.9-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +# Copy dependency file +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy source code +COPY bot.py . + +# Create required folders +RUN mkdir -p 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 + +# Environment variables +ENV PYTHONUNBUFFERED=1 + +CMD ["python", "bot.py"] \ No newline at end of file diff --git a/README.md b/README.md index 61373a7..7ecb65f 100644 --- a/README.md +++ b/README.md @@ -30,25 +30,29 @@ Create the following structure on your QNAP: ``` /share/Container/discord-bot/ -├── bot.py # Main bot script -├── requirements.txt # Python dependencies -├── .env # Environment variables -├── docker-compose.yml # Docker configuration -├── bot_data.db # SQLite database (auto-created) -├── logs/ # Bot logs (optional) -└── screenshots/ # PB screenshots +├── bot.py # Script principal minimal +├── config.py # Variables centrales (token, channel ID) +├── requirements.txt # Dépendances Python +├── .env # Tokens, IDs de channel +├── docker-compose.yml +├── bot_data.db # SQLite DB +├── cogs/ # Toutes les commandes du bot +│ ├── guide.py # Commande !guide +│ ├── pbhydra.py # Commandes PB Hydra +│ ├── pbchimera.py # Commandes PB Chimera +│ ├── pbcvc.py # Commandes PB CvC +│ ├── top10.py # Classements globaux +│ └── mystats.py # Commande !mystats +├── utils/ # Fonctions utilitaires partagées +│ ├── DatabaseManager_class.py # Gestion DB SQLite +│ ├── ScreenshotManager_class.py # Gestion des screenshots +│ ├── leaderboard_handler.py # Gestion tableau de score +│ ├── pbhandler.py # Gestion des pbs +│ └── helpers.py # Fonctions génériques (ex: channel autorisé) +├── logs/ # Logs du bot (optionnel) +└── screenshots/ # Screenshots organisés par boss/difficulté ├── hydra/ - │ ├── normal/ - │ ├── hard/ - │ ├── brutal/ - │ └── nightmare/ ├── chimera/ - │ ├── easy/ - │ ├── normal/ - │ ├── hard/ - │ ├── brutal/ - │ ├── nightmare/ - │ └── ultra/ └── cvc/ ``` diff --git a/RTF.py b/RTF.py deleted file mode 100644 index 3ae46bb..0000000 --- a/RTF.py +++ /dev/null @@ -1,955 +0,0 @@ -import discord -from discord.ext import commands -import sqlite3 -import os -import aiohttp -from datetime import datetime -import re - -intents = discord.Intents.default() -intents.message_content = True -bot = commands.Bot(command_prefix="!", intents=intents) - -# Configuration -AUTHORIZED_CHANNEL_ID = 0 # TODO: input channel ID here -SCREENSHOTS_BASE_PATH = "/share/Container/discord-bot/screenshots" -DATABASE_PATH = "/share/Container/discord-bot/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 - } -} - -# 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': [] # Pas de difficultés pour CvC - } -} - -# Mappings pour les diminutifs de difficultés -DIFFICULTY_SHORTCUTS = { - 'nm': 'nightmare', - 'unm': 'ultra' -} - -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() - - # Si c'est déjà un nombre sans suffixe - if damage_str.isdigit(): - return int(damage_str) - - # Utiliser regex pour extraire le nombre et le suffixe - 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 - - # Conversion selon le suffixe - multipliers = { - 'K': 1000, - 'M': 1000000, - 'B': 1000000000, - '': 1 - } - - if suffix in multipliers: - result = int(number * multipliers[suffix]) - return result - - return None - -def format_damage_display(damage): - """Formate un montant de dégâts avec le suffixe approprié""" - if damage == 0: - return "0" - - if damage >= 1000000000: - # Milliards - billions = damage / 1000000000 - if billions == int(billions): - return f"{int(billions)}B" - else: - return f"{billions:.1f}B" - elif damage >= 1000000: - # Millions - millions = damage / 1000000 - if millions == int(millions): - return f"{int(millions)}M" - else: - return f"{millions:.1f}M" - elif damage >= 1000: - # Milliers - thousands = damage / 1000 - if thousands == int(thousands): - return f"{int(thousands)}K" - else: - return f"{thousands:.1f}K" - else: - # Moins de 1000 - return str(damage) - -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 - -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_username TEXT UNIQUE, - - -- 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 - ) - ''') - - # Table pour l'historique global - cursor.execute(''' - CREATE TABLE IF NOT EXISTS pb_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - 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, username, 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_username = ?", - (username.lower(),) - ) - result = cursor.fetchone() - conn.close() - - return result if result else (0, None, None) - - def update_user_pb(self, username, boss_type, damage, screenshot_filename, difficulty=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(username, 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}" - - # Créer l'utilisateur s'il n'existe pas, sinon mettre à jour - cursor.execute(f''' - INSERT INTO users (discord_username, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts) - VALUES (?, ?, ?, CURRENT_TIMESTAMP, 1) - ON CONFLICT(discord_username) - DO UPDATE SET - {column_prefix} = ?, - {column_prefix}_screenshot = ?, - {column_prefix}_date = CURRENT_TIMESTAMP, - total_attempts = total_attempts + 1 - ''', (username.lower(), damage, screenshot_filename, damage, screenshot_filename)) - - # Ajouter à l'historique - cursor.execute(''' - INSERT INTO pb_history (username, boss_type, difficulty, damage, screenshot_filename) - VALUES (?, ?, ?, ?, ?) - ''', (username.lower(), 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 - FROM users - WHERE {column_prefix} > 0 - ''' - - if clan: - base_query += ''' AND ( - discord_username LIKE '[''' + clan + '''] %' OR - discord_username LIKE '[''' + clan + ''']%' - )''' - - base_query += f' ORDER BY {column_prefix} DESC LIMIT ?' - - cursor.execute(base_query, (limit,)) - results = cursor.fetchall() - conn.close() - return results - - def get_user_all_pbs(self, username): - """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_username = ?', (username.lower(),)) - result = cursor.fetchone() - 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 - -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 la 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: - 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}") - return None - - def get_screenshot_path(self, filename, boss_type, difficulty=None): - """Retourne le chemin complet de la 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: {e}") - -# Fonctions utilitaires -def get_user_clan(username): - """Détermine le clan d'un utilisateur basé sur son pseudo - Version corrigée""" - username_upper = username.upper() - - # Chercher les 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 - 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): - """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()) - -# Initialisation des managers -db_manager = DatabaseManager() -screenshot_manager = ScreenshotManager() - -@bot.event -async def on_ready(): - print(f"Bot connected as {bot.user}") - -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: - # 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 - await show_user_pb(ctx, boss_type, None, arg1) - else: # Montrer son propre PB - await show_user_pb(ctx, boss_type, None, ctx.author.display_name) - return - - # 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"**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 - - # 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"**Available difficulties:** {difficulty_list}\n" - f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare" - ) - - except Exception as e: - await ctx.send(f"❌ Error: {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 - - 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) - - 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) - embed.set_image(url=attachment.url) - - await ctx.send(embed=embed) - else: - 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) - -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 - - boss_info = BOSS_CONFIG[boss_type] - difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" - - if pb_damage == 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 - ) - 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) - -# Commandes pour chaque boss -@bot.command() -async def pbhydra(ctx, arg1: str = None, arg2: str = None): - """Commande !pbhydra avec gestion des difficultés""" - await handle_pb_command(ctx, 'hydra', arg1, arg2) - -@bot.command() -async def pbchimera(ctx, arg1: str = None, arg2: str = None): - """Commande !pbchimera avec gestion des difficultés""" - await handle_pb_command(ctx, 'chimera', arg1, arg2) - -@bot.command() -async def pbcvc(ctx, target_user: str = None): - """Commande !pbcvc (sans difficultés)""" - await handle_pb_command(ctx, 'cvc', target_user) - -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) in enumerate(leaderboard): - date_text = "" - if date: - formatted_date = format_date_only(date) - if 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'] - 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}") - -# Commandes de classement global avec difficultés -@bot.command() -async def top10hydra(ctx, difficulty: str = None): - """Affiche le top 10 des PB Hydra pour une difficulté""" - 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") - -@bot.command() -async def top10chimera(ctx, difficulty: str = None): - """Affiche le top 10 des PB Chimera pour une difficulté""" - 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") - -@bot.command() -async def top10cvc(ctx): - """Affiche le top 10 des PB CvC""" - await show_leaderboard(ctx, 'cvc') - -# Commandes de classement par clan - RTF -@bot.command() -async def rtfhydra(ctx, difficulty: str = None): - """Affiche le top 10 Hydra du clan RTF""" - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['hydra']['difficulties']: - await show_leaderboard(ctx, 'hydra', difficulty, 'RTF') - else: - difficulties = " | ".join(BOSS_CONFIG['hydra']['difficulties']) - await ctx.send(f"❌ Please specify difficulty: `!rtfhydra `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare") - -@bot.command() -async def rtfchimera(ctx, difficulty: str = None): - """Affiche le top 10 Chimera du clan RTF""" - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['chimera']['difficulties']: - await show_leaderboard(ctx, 'chimera', difficulty, 'RTF') - else: - difficulties = " | ".join(BOSS_CONFIG['chimera']['difficulties']) - await ctx.send(f"❌ Please specify difficulty: `!rtfchimera `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra") - -@bot.command() -async def rtfcvc(ctx): - """Affiche le top 10 CvC du clan RTF""" - await show_leaderboard(ctx, 'cvc', None, 'RTF') - -# Commandes de classement par clan - RTFC -@bot.command() -async def rtfchydra(ctx, difficulty: str = None): - """Affiche le top 10 Hydra du clan RTFC""" - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['hydra']['difficulties']: - await show_leaderboard(ctx, 'hydra', difficulty, 'RTFC') - else: - difficulties = " | ".join(BOSS_CONFIG['hydra']['difficulties']) - await ctx.send(f"❌ Please specify difficulty: `!rtfchydra `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare") - -@bot.command() -async def rtfcchimera(ctx, difficulty: str = None): - """Affiche le top 10 Chimera du clan RTFC""" - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['chimera']['difficulties']: - await show_leaderboard(ctx, 'chimera', difficulty, 'RTFC') - else: - difficulties = " | ".join(BOSS_CONFIG['chimera']['difficulties']) - await ctx.send(f"❌ Please specify difficulty: `!rtfcchimera `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra") - -@bot.command() -async def rtfccvc(ctx): - """Affiche le top 10 CvC du clan RTFC""" - await show_leaderboard(ctx, 'cvc', None, 'RTFC') - -# Commandes de classement par clan - RTFR -@bot.command() -async def rtfrhydra(ctx, difficulty: str = None): - """Affiche le top 10 Hydra du clan RTFR""" - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['hydra']['difficulties']: - await show_leaderboard(ctx, 'hydra', difficulty, 'RTFR') - else: - difficulties = " | ".join(BOSS_CONFIG['hydra']['difficulties']) - await ctx.send(f"❌ Please specify difficulty: `!rtfrhydra `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare") - -@bot.command() -async def rtfrchimera(ctx, difficulty: str = None): - """Affiche le top 10 Chimera du clan RTFR""" - if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['chimera']['difficulties']: - await show_leaderboard(ctx, 'chimera', difficulty, 'RTFR') - else: - difficulties = " | ".join(BOSS_CONFIG['chimera']['difficulties']) - await ctx.send(f"❌ Please specify difficulty: `!rtfrchimera `\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra") - -@bot.command() -async def rtfrcvc(ctx): - """Affiche le top 10 CvC du clan RTFR""" - await show_leaderboard(ctx, 'cvc', None, 'RTFR') - -@bot.command() -async def mystats(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: - 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}**.") - return - - embed = discord.Embed( - title=f"📊 {username}'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 = 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) - 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}") - -@bot.command() -async def guide(ctx): - """Affiche la liste des commandes disponibles avec les nouvelles difficultés""" - if ctx.channel.id != AUTHORIZED_CHANNEL_ID: - return - - embed = discord.Embed( - 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 - 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 - ) - - # 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="**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" + - "`!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" + - "`!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!") - - await ctx.send(embed=embed) - -# Ajout du token bot (à remplacer par votre token) -# bot.run('YOUR_BOT_TOKEN_HERE') diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..b2c9655 --- /dev/null +++ b/bot.py @@ -0,0 +1,35 @@ +import os +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 + +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", + "cogs.pbchimera", + "cogs.pbcvc", + "cogs.top10", + "cogs.mystats", +] + +for cog in initial_cogs: + bot.load_extension(cog) + +@bot.event +async def on_ready(): + print(f"{bot.user.name} est connecté !") + +bot.run(DISCORD_TOKEN) diff --git a/cogs/guide.py b/cogs/guide.py new file mode 100644 index 0000000..d4a4199 --- /dev/null +++ b/cogs/guide.py @@ -0,0 +1,102 @@ +import discord +from discord.ext import commands +from config import AUTHORIZED_CHANNEL_ID + +class Guide(commands.Cog): + 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""" + if ctx.channel.id != AUTHORIZED_CHANNEL_ID: + return + + embed = discord.Embed( + 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 + 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 + ) + + # 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="**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" + + "`!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" + + "`!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!") + + await ctx.send(embed=embed) + +def setup(bot): + bot.add_cog(Guide(bot)) \ No newline at end of file diff --git a/cogs/mystats.py b/cogs/mystats.py new file mode 100644 index 0000000..bd459bc --- /dev/null +++ b/cogs/mystats.py @@ -0,0 +1,89 @@ +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 + +class MyStats(commands.Cog): + 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: + 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}**.") + return + + embed = discord.Embed( + title=f"📊 {username}'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 = 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) + 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}") + + +# Pour charger le Cog +def setup(bot): + bot.add_cog(MyStats(bot)) diff --git a/cogs/pbchimera.py b/cogs/pbchimera.py new file mode 100644 index 0000000..c4a81fd --- /dev/null +++ b/cogs/pbchimera.py @@ -0,0 +1,16 @@ +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): + 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) + +def setup(bot): + bot.add_cog(Pbchimera(bot)) \ No newline at end of file diff --git a/cogs/pbcvc.py b/cogs/pbcvc.py new file mode 100644 index 0000000..ae2e438 --- /dev/null +++ b/cogs/pbcvc.py @@ -0,0 +1,16 @@ +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): + 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)""" + await handle_pb_command(ctx, 'cvc', target_user) + +def setup(bot): + bot.add_cog(Pbcvc(bot)) \ No newline at end of file diff --git a/cogs/pbhydra.py b/cogs/pbhydra.py new file mode 100644 index 0000000..a1a5946 --- /dev/null +++ b/cogs/pbhydra.py @@ -0,0 +1,16 @@ +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): + 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) + +def setup(bot): + bot.add_cog(Pbhydra(bot)) \ No newline at end of file diff --git a/cogs/top10.py b/cogs/top10.py new file mode 100644 index 0000000..8d71eea --- /dev/null +++ b/cogs/top10.py @@ -0,0 +1,84 @@ +import discord +from discord.ext import commands +from utils.leaderboard_handler import show_leaderboard +from utils.helpers import normalize_difficulty, 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 RTF --- + @commands.command() + async def rtfhydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTF') + + @commands.command() + async def rtfchimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTF') + + @commands.command() + async def rtfcvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='RTF') + + # --- Commandes par clan RTFC --- + @commands.command() + async def rtfchydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTFC') + + @commands.command() + async def rtfcchimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTFC') + + @commands.command() + async def rtfccvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='RTFC') + + # --- Commandes par clan RTFR --- + @commands.command() + async def rtfrhydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTFR') + + @commands.command() + async def rtfrchimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTFR') + + @commands.command() + async def rtfrcvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='RTFR') + + # --- 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**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra") + else: + await show_leaderboard(ctx, boss_type, clan=clan) + +def setup(bot): + bot.add_cog(Top10(bot)) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..cfca3f9 --- /dev/null +++ b/config.py @@ -0,0 +1,34 @@ +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 = "/share/Container/discord-bot/screenshots" +DATABASE_PATH = "/share/Container/discord-bot/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} +} + +# 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' +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b5e4b95 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + discord-bot: + build: . + restart: unless-stopped + env_file: + - .env + volumes: + - ./screenshots:/app/screenshots + - ./bot_data.db:/app/bot_data.db + - ./logs:/app/logs + environment: + - TZ=Europe/Paris + container_name: rtf-discord-bot + + # Optional: Resource limits + deploy: + resources: + limits: + memory: 256M + reservations: + memory: 128M \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eb3451c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +discord.py>=2.3.0 +aiohttp>=3.8.0 +python-dotenv>=1.0.0 \ No newline at end of file diff --git a/utils/DatabaseManager_class.py b/utils/DatabaseManager_class.py new file mode 100644 index 0000000..fd45f60 --- /dev/null +++ b/utils/DatabaseManager_class.py @@ -0,0 +1,180 @@ +from config import DATABASE_PATH +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_username TEXT UNIQUE, + + -- 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 + ) + ''') + + # Table pour l'historique global + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pb_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + 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, username, 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_username = ?", + (username.lower(),) + ) + result = cursor.fetchone() + conn.close() + + return result if result else (0, None, None) + + def update_user_pb(self, username, boss_type, damage, screenshot_filename, difficulty=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(username, 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}" + + # Créer l'utilisateur s'il n'existe pas, sinon mettre à jour + cursor.execute(f''' + INSERT INTO users (discord_username, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts) + VALUES (?, ?, ?, CURRENT_TIMESTAMP, 1) + ON CONFLICT(discord_username) + DO UPDATE SET + {column_prefix} = ?, + {column_prefix}_screenshot = ?, + {column_prefix}_date = CURRENT_TIMESTAMP, + total_attempts = total_attempts + 1 + ''', (username.lower(), damage, screenshot_filename, damage, screenshot_filename)) + + # Ajouter à l'historique + cursor.execute(''' + INSERT INTO pb_history (username, boss_type, difficulty, damage, screenshot_filename) + VALUES (?, ?, ?, ?, ?) + ''', (username.lower(), 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 + FROM users + WHERE {column_prefix} > 0 + ''' + + if clan: + base_query += ''' AND ( + discord_username LIKE '[''' + clan + '''] %' OR + discord_username LIKE '[''' + clan + ''']%' + )''' + + base_query += f' ORDER BY {column_prefix} DESC LIMIT ?' + + cursor.execute(base_query, (limit,)) + results = cursor.fetchall() + conn.close() + return results + + def get_user_all_pbs(self, username): + """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_username = ?', (username.lower(),)) + result = cursor.fetchone() + 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 diff --git a/utils/ScreenshotManager_class.py b/utils/ScreenshotManager_class.py new file mode 100644 index 0000000..86028d9 --- /dev/null +++ b/utils/ScreenshotManager_class.py @@ -0,0 +1,63 @@ +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: + 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}") + 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: {e}") \ No newline at end of file diff --git a/utils/helpers.py b/utils/helpers.py new file mode 100644 index 0000000..80a82d6 --- /dev/null +++ b/utils/helpers.py @@ -0,0 +1,101 @@ +import re +from datetime import datetime +from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS + +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': 1000, '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() + + # 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""" + username_upper = username.upper() + + # Chercher les 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 + 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): + """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 + diff --git a/utils/leaderboard_handler.py b/utils/leaderboard_handler.py new file mode 100644 index 0000000..d99ac8b --- /dev/null +++ b/utils/leaderboard_handler.py @@ -0,0 +1,75 @@ +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, get_user_clan + +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) in enumerate(leaderboard): + date_text = "" + if date: + formatted_date = format_date_only(date) + if 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'] + 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}") \ No newline at end of file diff --git a/utils/pb_handler.py b/utils/pb_handler.py new file mode 100644 index 0000000..328dfa3 --- /dev/null +++ b/utils/pb_handler.py @@ -0,0 +1,190 @@ +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, +) + +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: + # 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 + await show_user_pb(ctx, boss_type, None, arg1) + else: # Montrer son propre PB + await show_user_pb(ctx, boss_type, None, ctx.author.display_name) + return + + # 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"**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 + + # 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"**Available difficulties:** {difficulty_list}\n" + f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare" + ) + + except Exception as e: + await ctx.send(f"❌ Error: {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 + + 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) + + 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) + embed.set_image(url=attachment.url) + + await ctx.send(embed=embed) + else: + 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) + + +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 + + boss_info = BOSS_CONFIG[boss_type] + difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" + + if pb_damage == 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 + ) + 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)