From acabc7d4090f4ab963c20ec91cc461e52bdaea64 Mon Sep 17 00:00:00 2001 From: LE BERRE Mickael Date: Mon, 18 May 2026 10:38:01 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20rebrand=20RTF=E2=86=92TEA,=20clan=20det?= =?UTF-8?q?ection=20via=20Discord=20roles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces display-name prefix parsing ([RTF]/[RTFC]/[RTFR]) with Discord role-based clan detection (TEAI/TEAF/TEAC/TEACO). - config: new CLAN_CONFIG with 4 TEA clans, CLAN_ROLE_IDS, CLAN_MIGRATION - helpers: get_user_clan() replaced by get_clan_from_member() - DatabaseManager: adds clan column on startup, auto-migrates existing records from old username prefixes, filters leaderboard by clan column - pb_handler: detects clan from roles on submission, passes it to DB - leaderboard_handler: reads clan from DB column instead of username - top10: new commands !teai*/!teaf*/!teac*/!teaco* (removes !rtf*) - guide: updated command list and bot title Co-Authored-By: Claude Sonnet 4.6 --- cogs/guide.py | 11 ++++--- cogs/top10.py | 55 ++++++++++++++++++++------------ config.py | 24 +++++++++++--- utils/DatabaseManager_class.py | 58 ++++++++++++++++++++-------------- utils/helpers.py | 24 ++++---------- utils/leaderboard_handler.py | 16 ++++------ utils/pb_handler.py | 8 +++-- 7 files changed, 112 insertions(+), 84 deletions(-) diff --git a/cogs/guide.py b/cogs/guide.py index d6853aa..320e8fa 100644 --- a/cogs/guide.py +++ b/cogs/guide.py @@ -16,7 +16,7 @@ class Guide(commands.Cog): return embed = discord.Embed( - title="🧐 RTF Bot - Commands Guide", + title="🧐 TEA Bot - Commands Guide", description="Here are all available commands for tracking your Personal Bests!", color=0x00bfff ) @@ -81,9 +81,10 @@ class Guide(commands.Cog): # Classements par clan embed.add_field( name="🏆 Clan Leaderboards", - value="**RTF:** `!rtfhydra ` `!rtfchimera ` `!rtfcvc`\n" - "**RTFC:** `!rtfchydra ` `!rtfcchimera ` `!rtfccvc`\n" - "**RTFR:** `!rtfrhydra ` `!rtfrchimera ` `!rtfrcvc`", + value="**🔥 TEAI (Inferno):** `!teaihydra ` `!teaichimera ` `!teaicvc`\n" + "**🛡️ TEAF (Flame):** `!teafhydra ` `!teafchimera ` `!teafcvc`\n" + "**⚔️ TEAC (Cinder):** `!teachydra ` `!teachimera ` `!teaccvc`\n" + "**👑 TEACO (Corrupted Olympians):** `!teacohydra ` `!teacochimera ` `!teacocvc`", inline=False ) @@ -104,7 +105,7 @@ class Guide(commands.Cog): "`!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" + "`!teaihydra nm` - TEAI clan Nightmare rankings\n" "**Always attach screenshot when submitting PBs!**", inline=False ) diff --git a/cogs/top10.py b/cogs/top10.py index 15ba344..515a43f 100644 --- a/cogs/top10.py +++ b/cogs/top10.py @@ -32,44 +32,57 @@ class Top10(commands.Cog): async def top10cvc(self, ctx): await show_leaderboard(ctx, 'cvc') - # --- Commandes par clan RTF --- + # --- Commandes par clan TEAI (Inferno) --- @commands.command() - async def rtfhydra(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTF') + async def teaihydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAI') @commands.command() - async def rtfchimera(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTF') + async def teaichimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAI') @commands.command() - async def rtfcvc(self, ctx): - await show_leaderboard(ctx, 'cvc', clan='RTF') + async def teaicvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='TEAI') - # --- Commandes par clan RTFC --- + # --- Commandes par clan TEAF (Flame) --- @commands.command() - async def rtfchydra(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTFC') + async def teafhydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAF') @commands.command() - async def rtfcchimera(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTFC') + async def teafchimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAF') @commands.command() - async def rtfccvc(self, ctx): - await show_leaderboard(ctx, 'cvc', clan='RTFC') + async def teafcvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='TEAF') - # --- Commandes par clan RTFR --- + # --- Commandes par clan TEAC (Cinder) --- @commands.command() - async def rtfrhydra(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTFR') + async def teachydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAC') @commands.command() - async def rtfrchimera(self, ctx, difficulty: str = None): - await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTFR') + async def teachimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAC') @commands.command() - async def rtfrcvc(self, ctx): - await show_leaderboard(ctx, 'cvc', clan='RTFR') + async def teaccvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='TEAC') + + # --- Commandes par clan TEACO (Corrupted Olympians) --- + @commands.command() + async def teacohydra(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEACO') + + @commands.command() + async def teacochimera(self, ctx, difficulty: str = None): + await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEACO') + + @commands.command() + async def teacocvc(self, ctx): + await show_leaderboard(ctx, 'cvc', clan='TEACO') # --- Méthode interne pour éviter la répétition --- async def _show_clan_leaderboard(self, ctx, boss_type, difficulty, clan): diff --git a/config.py b/config.py index ee09cfb..592aee9 100644 --- a/config.py +++ b/config.py @@ -12,11 +12,27 @@ AUTHORIZED_CHANNEL_ID = int(os.getenv("AUTHORIZED_CHANNEL_ID")) SCREENSHOTS_BASE_PATH = "/app/screenshots" DATABASE_PATH = "/app/data/bot_data.db" -# Configuration des clans +# Configuration des clans TEA - The Ember Accord CLAN_CONFIG = { - 'RTF': {'name': 'RTF', 'emoji': '🛡️', 'color': 0x00ff00}, - 'RTFC': {'name': 'RTFC', 'emoji': '🔥', 'color': 0xff4500}, - 'RTFR': {'name': 'RTFR', 'emoji': '⚔️', 'color': 0x1e90ff} + 'TEAI': {'name': 'TEAI', 'full_name': 'Inferno', 'emoji': '🔥', 'color': 0xff4500}, + 'TEAF': {'name': 'TEAF', 'full_name': 'Flame', 'emoji': '🛡️', 'color': 0x00ff00}, + 'TEAC': {'name': 'TEAC', 'full_name': 'Cinder', 'emoji': '⚔️', 'color': 0x1e90ff}, + 'TEACO': {'name': 'TEACO', 'full_name': 'Corrupted Olympians', 'emoji': '👑', 'color': 0x9932cc}, +} + +# Mapping role Discord ID → clé de clan +CLAN_ROLE_IDS = { + 1190674529731747901: 'TEAI', + 1197646966599983185: 'TEAF', + 1220014404809261076: 'TEAC', + 1496965820868198550: 'TEACO', +} + +# Mapping anciens clans → nouveaux (migration base existante) +CLAN_MIGRATION = { + 'RTF': 'TEAI', + 'RTFC': 'TEAF', + 'RTFR': 'TEAC', } # Configuration des boss avec difficultés diff --git a/utils/DatabaseManager_class.py b/utils/DatabaseManager_class.py index 31271c3..4157df8 100644 --- a/utils/DatabaseManager_class.py +++ b/utils/DatabaseManager_class.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from config import DATABASE_PATH +from config import DATABASE_PATH, CLAN_MIGRATION import sqlite3, os class DatabaseManager: @@ -70,7 +70,16 @@ class DatabaseManager: if 'discord_id' not in columns: cursor.execute('ALTER TABLE users ADD COLUMN discord_id TEXT') - # Note: Vous devrez peut-être faire une migration manuelle pour les données existantes + + if 'clan' not in columns: + cursor.execute('ALTER TABLE users ADD COLUMN clan TEXT') + # Migration automatique : déduction du clan depuis l'ancien préfixe du pseudo + for old_tag, new_clan in CLAN_MIGRATION.items(): + cursor.execute( + "UPDATE users SET clan = ? WHERE clan IS NULL AND (" + "discord_username LIKE ? OR discord_username LIKE ?)", + (new_clan, f'[{old_tag}] %', f'[{old_tag}]%') + ) # Table pour l'historique global cursor.execute(''' @@ -108,32 +117,33 @@ class DatabaseManager: return result if result else (0, None, None) - def update_user_pb(self, user_id, username, boss_type, damage, screenshot_filename, difficulty=None): + def update_user_pb(self, user_id, username, boss_type, damage, screenshot_filename, difficulty=None, clan=None): """Met à jour le PB d'un utilisateur et supprime l'ancien screenshot""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() - + # Récupérer l'ancien screenshot pour le supprimer old_data = self.get_user_pb(user_id, boss_type, difficulty) old_screenshot = old_data[1] if old_data else None - + if difficulty: column_prefix = f"pb_{boss_type}_{difficulty}" else: column_prefix = f"pb_{boss_type}" - - # Créer l'utilisateur s'il n'existe pas, sinon mettre à jour + + # COALESCE(?, clan) : on n'écrase pas le clan existant si la détection retourne None cursor.execute(f''' - INSERT INTO users (discord_id, discord_username, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts) - VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, 1) - ON CONFLICT(discord_id) - DO UPDATE SET + INSERT INTO users (discord_id, discord_username, clan, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts) + VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, 1) + ON CONFLICT(discord_id) + DO UPDATE SET discord_username = ?, + clan = COALESCE(?, clan), {column_prefix} = ?, {column_prefix}_screenshot = ?, {column_prefix}_date = CURRENT_TIMESTAMP, total_attempts = total_attempts + 1 - ''', (str(user_id), username, damage, screenshot_filename, username, damage, screenshot_filename)) + ''', (str(user_id), username, clan, damage, screenshot_filename, username, clan, damage, screenshot_filename)) # Ajouter à l'historique cursor.execute(''' @@ -150,27 +160,27 @@ class DatabaseManager: """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 + SELECT discord_username, {column_prefix}, {column_prefix}_date, clan + FROM users WHERE {column_prefix} > 0 ''' - + + params = [] if clan: - base_query += ''' AND ( - discord_username LIKE '[''' + clan + '''] %' OR - discord_username LIKE '[''' + clan + ''']%' - )''' - + base_query += ' AND clan = ?' + params.append(clan) + base_query += f' ORDER BY {column_prefix} DESC LIMIT ?' - - cursor.execute(base_query, (limit,)) + params.append(limit) + + cursor.execute(base_query, params) results = cursor.fetchall() conn.close() return results diff --git a/utils/helpers.py b/utils/helpers.py index 6cd9787..e38489d 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import re from datetime import datetime -from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS +from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS, CLAN_ROLE_IDS def parse_damage_amount(damage_str): """Convertit les montants avec suffixes (K, M, B) en nombres entiers""" @@ -43,25 +43,13 @@ def normalize_difficulty(difficulty): return DIFFICULTY_SHORTCUTS[difficulty_lower] return difficulty_lower -def get_user_clan(username): - """Détermine le clan d'un utilisateur basé sur son pseudo""" - if not username: - return None - username_upper = username.upper() - # Tags avec crochets et espace - for clan_tag in ['[RTF] ', '[RTFC] ', '[RTFR] ']: - if username_upper.startswith(clan_tag): - return clan_tag.replace('[', '').replace(']', '').strip() - # Tags avec crochets sans espace - for clan_tag in ['[RTF]', '[RTFC]', '[RTFR]']: - if username_upper.startswith(clan_tag): - return clan_tag.replace('[', '').replace(']', '') +def get_clan_from_member(member) -> str | None: + """Détecte le clan d'un membre via ses rôles Discord""" + for role in member.roles: + if role.id in CLAN_ROLE_IDS: + return CLAN_ROLE_IDS[role.id] return None -def get_user_clan_from_ctx(ctx): - """Détermine le clan d'un utilisateur depuis le contexte Discord""" - return get_user_clan(ctx.author.display_name) - def format_datetime(date_str): """Formate une date en format AM/PM""" if not date_str: diff --git a/utils/leaderboard_handler.py b/utils/leaderboard_handler.py index b46b58c..9e0684c 100644 --- a/utils/leaderboard_handler.py +++ b/utils/leaderboard_handler.py @@ -3,7 +3,7 @@ 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 +from utils.helpers import normalize_difficulty, get_difficulty_display_name, format_damage_display, format_date_only db_manager = None @@ -49,20 +49,18 @@ async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None): medals = ["🥇", "🥈", "🥉"] + ["🏅"] * 7 - for i, (username, damage, date) in enumerate(leaderboard): + for i, (username, damage, date, row_clan) in enumerate(leaderboard): date_text = "" if date: formatted_date = format_date_only(date) if formatted_date: date_text = f" • {formatted_date}" - - # Afficher le clan dans le nom si pas de filtre par clan + + # Afficher l'emoji du clan si le leaderboard est global (pas filtré 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}" + if not clan and row_clan: + clan_emoji = CLAN_CONFIG.get(row_clan, {'emoji': '🏛️'})['emoji'] + display_name = f"{clan_emoji} {username}" embed.add_field( name=f"{medals[i]} #{i+1} {display_name}", diff --git a/utils/pb_handler.py b/utils/pb_handler.py index 50999eb..6d209a6 100644 --- a/utils/pb_handler.py +++ b/utils/pb_handler.py @@ -8,6 +8,7 @@ from utils.helpers import ( get_difficulty_display_name, format_damage_display, format_datetime, + get_clan_from_member, ) db_manager = None @@ -92,16 +93,17 @@ async def handle_pb_submission(ctx, boss_type, difficulty, damage): user_id = ctx.author.id username = ctx.author.display_name + clan = get_clan_from_member(ctx.author) current_pb, _, _ = db_manager.get_user_pb(user_id, boss_type, difficulty) - + if damage > current_pb: screenshot_filename = await screenshot_manager.save_screenshot( attachment, username, damage, boss_type, difficulty ) - + if screenshot_filename: old_screenshot = db_manager.update_user_pb( - user_id, username, boss_type, damage, screenshot_filename, difficulty + user_id, username, boss_type, damage, screenshot_filename, difficulty, clan ) if old_screenshot: