Compare commits

..

17 commits
main ... dev

Author SHA1 Message Date
LE BERRE Mickael
0e526a40bc docs: update CLAUDE.md community description to TEA rebrand
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 30s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 16:27:11 +02:00
LE BERRE Mickael
64b60f1597 docs: explicit comment on StrictHostKeyChecking=no in deploy workflow
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 34s
Runner Alpine stateless, pas de known_hosts persistant.
Cible fixe sur LAN interne (192.168.1.208) — risque MITM inexistant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 16:17:56 +02:00
LE BERRE Mickael
e364f354b0 fix: mystats username lookup and multi-word name support
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 25s
- Résolution du nom via find_user_by_name avant get_user_all_pbs
  (l'ancien code passait le nom en guise de discord_id → aucun résultat)
- Affichage du nom de la cible dans le titre de l'embed
- * pour capturer les noms avec espaces

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:56:28 +02:00
LE BERRE Mickael
314483e4f5 fix: support multi-word usernames in pb lookup commands
Sans le *, discord.py tronquait les noms avec espaces au premier mot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:52:04 +02:00
LE BERRE Mickael
1a37ec8838 fix: set dev role IDs for TEA clans + fix leading-zero syntax error
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 12s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:01:35 +02:00
LE BERRE Mickael
0d744c8cdc chore: placeholder role IDs for dev environment
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 25s
IDs serveur dev à renseigner avant déploiement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:51:59 +02:00
LE BERRE Mickael
4ecac026b5 fix: replace str | None with Optional[str] for Python 3.9 compat
str | None union syntax requires Python 3.10+, bot runs on 3.9-slim.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:51:26 +02:00
LE BERRE Mickael
85942723ab feat: rebrand RTF→TEA, clan detection via Discord roles
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 <noreply@anthropic.com>
2026-05-18 10:51:26 +02:00
ec86776e42 fix: remove container_name conflict between dev and prod, drop obsolete version
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 17s
2026-04-30 16:30:37 +02:00
01ef1cadcf fix: remove --build, code is volume-mounted
Some checks failed
Deploy Bot on NAS / deploy (push) Failing after 10s
2026-04-30 16:27:11 +02:00
14887625a2 fix: use full docker path for QNAP Container Station
Some checks failed
Deploy Bot on NAS / deploy (push) Failing after 17s
2026-04-30 16:22:25 +02:00
f4dbfba223 ci: re-trigger pipeline
Some checks failed
Deploy Bot on NAS / deploy (push) Failing after 2s
2026-04-30 16:19:26 +02:00
bccf437192 fix: use absolute path /root/.ssh/id_deploy in CI
Some checks failed
Deploy Bot on NAS / deploy (push) Has been cancelled
2026-04-30 16:15:39 +02:00
cd81d299ab swith do ip
Some checks failed
Deploy Bot on NAS / deploy (push) Has been cancelled
2026-04-30 16:12:11 +02:00
e4a38d7853 fix: install rsync et openssh dans le job (runner Alpine)
Some checks failed
Deploy Bot on NAS / deploy (push) Failing after 2s
2026-04-30 16:02:19 +02:00
cdc36dd42b test: trigger deploy pipeline
Some checks failed
Deploy Bot on NAS / deploy (push) Failing after 1s
2026-04-30 16:00:44 +02:00
8932696709 fix: remplace actions/checkout par git clone natif (pas de node.js)
Some checks failed
Deploy Bot on NAS / deploy (push) Failing after 1s
2026-04-30 15:54:16 +02:00
19 changed files with 1241 additions and 1209 deletions

View file

@ -39,12 +39,10 @@ jobs:
rsync -av --delete \ rsync -av --delete \
-e "ssh -i /root/.ssh/id_deploy -o StrictHostKeyChecking=no" \ -e "ssh -i /root/.ssh/id_deploy -o StrictHostKeyChecking=no" \
--exclude='.git' \ --exclude='.git' \
--exclude='.github' \
--exclude='.env' \ --exclude='.env' \
--exclude='data/' \ --exclude='data/' \
--exclude='screenshots/' \ --exclude='screenshots/' \
--exclude='logs/' \ --exclude='logs/' \
--exclude='__pycache__/' \
./ Elewyn@192.168.1.208:${{ env.DEPLOY_PATH }}/ ./ Elewyn@192.168.1.208:${{ env.DEPLOY_PATH }}/
- name: Restart bot on NAS - name: Restart bot on NAS

45
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: Deploy Bot on NAS
on:
push:
branches: [ main, dev ]
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Set deployment path
id: set-path
run: |
if [ "${GITHUB_REF_NAME}" = "main" ]; then
echo "DEPLOY_PATH=/share/CACHEDEV1_DATA/discord-bot-prod" >> $GITHUB_ENV
elif [ "${GITHUB_REF_NAME}" = "dev" ]; then
echo "DEPLOY_PATH=/share/CACHEDEV1_DATA/discord-bot-dev" >> $GITHUB_ENV
else
echo "Unsupported branch"
exit 1
fi
- name: Update bot files
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd $DEPLOY_PATH
git config --global --add safe.directory $DEPLOY_PATH
if [ ! -d ".git" ]; then
git init
git remote add origin https://x-access-token:$GITHUB_TOKEN@github.com/ArcElewyn/Discord.git
else
git remote remove origin || true
git remote add origin https://x-access-token:$GITHUB_TOKEN@github.com/ArcElewyn/Discord.git
fi
git fetch origin $GITHUB_REF_NAME
git reset --hard origin/$GITHUB_REF_NAME
- name: Restart bot
run: |
cd $DEPLOY_PATH
docker compose -f $DEPLOY_PATH/docker-compose.yml down || true
docker compose -f $DEPLOY_PATH/docker-compose.yml up --build -d

View file

@ -12,19 +12,13 @@ Communauté TEA — The Ember Accord (4 clans : TEAI, TEAF, TEAC, TEACO).
| `main` | Production | `/share/CACHEDEV1_DATA/discord-bot-prod` | | `main` | Production | `/share/CACHEDEV1_DATA/discord-bot-prod` |
| `dev` | Développement | `/share/CACHEDEV1_DATA/discord-bot-dev` | | `dev` | Développement | `/share/CACHEDEV1_DATA/discord-bot-dev` |
`dev` est une branche permanente — ne jamais merger dans `main`.
## Déploiement ## Déploiement
CI/CD via Forgejo Actions → runner `vm-runner` (192.168.1.53, Alpine) → rsync vers NAS QNAP (192.168.1.208). CI/CD via Forgejo Actions → runner `vm-runner` (192.168.1.53) → rsync vers NAS QNAP (192.168.1.208).
Push sur `main` ou `dev` déclenche le déploiement automatique. Push sur `main` ou `dev` déclenche le déploiement automatique.
Secret requis dans Forgejo : `NAS_SSH_KEY`**doit être encodé en base64** : Secret requis dans Forgejo : `NAS_SSH_KEY` (clé privée ed25519 pour SSH Elewyn@NAS).
```bash
base64 -w 0 ~/.ssh/runner-nas
```
Coller le résultat dans le secret (pas le contenu brut du fichier).
## Persistance des données (NE PAS écraser) ## Persistance des données (NE PAS écraser)
@ -56,28 +50,14 @@ Dockerfile # Image Python 3.9-slim
docker-compose.yml # Déploiement container docker-compose.yml # Déploiement container
``` ```
## Langue du code
Tout le code source doit être **exclusivement en anglais** :
- Docstrings et commentaires
- Messages Discord affichés aux utilisateurs
- Logs (`print`, `logging`)
Le `!help` de discord.py affiche les docstrings des cogs directement — les laisser en français les rend visibles aux membres.
## Boss supportés ## Boss supportés
- **Hydra** : normal, hard, brutal, nightmare - **Hydra** : normal, hard, brutal, nightmare
- **Chimera** : easy, normal, hard, brutal, nightmare, ultra - **Chimera** : easy, normal, hard, brutal, nightmare, ultra
- **CvC** : Clan vs Clan - **CvC** : Clan vs Clan
## Pièges connus — CI/CD ## Pièges connus
- **NAS_SSH_KEY doit être en base64**`echo "..." > fichier` dans Alpine sh casse les sauts de ligne de la clé brute - Le `.env` ne doit jamais être commité — il contient le token Discord
- **Runner tourne dans Alpine**`actions/checkout@v4` ne fonctionne pas (pas de Node.js) → utiliser `git clone`; `rsync` et `openssh-client` doivent être installés via `apk add` - `data/`, `screenshots/`, `logs/` sont gitignorés — données persistantes sur le NAS
- **Chemin Docker sur QNAP** : `/share/CACHEDEV1_DATA/.qpkg/container-station/usr/bin/docker``docker` n'est pas dans le `$PATH` SSH - Le rsync exclut ces dossiers pour ne pas écraser les données en prod
- **Pas de `--build`** — le code est monté en volume (`./:/app`), un simple redémarrage suffit
- **Pas de `container_name`** — les envs prod et dev partagent le même NAS, un nom hardcodé crée un conflit ; Docker Compose dérive le nom depuis le dossier
- **rsync exclut `__pycache__/`** — créés par Docker (root), l'user Elewyn ne peut pas les supprimer via rsync `--delete`
- **rsync exclut `.github/`** — vestige de l'ancien CI GitHub Actions, ne pas envoyer sur le NAS
- **Clé SSH workstation** : `~/.ssh/runner-nas` (ed25519, autorisée dans `~/.ssh/authorized_keys` sur le NAS pour l'user Elewyn)

View file

@ -107,6 +107,8 @@ CMD ["python", "bot.py"]
### 4. Create `docker-compose.yml` ### 4. Create `docker-compose.yml`
```yaml ```yaml
version: '3.8'
services: services:
discord-bot: discord-bot:
build: . build: .
@ -114,12 +116,14 @@ services:
env_file: env_file:
- .env - .env
volumes: volumes:
- ./:/app
- ./screenshots:/app/screenshots - ./screenshots:/app/screenshots
- ./data:/app/data - ./bot_data.db:/app/bot_data.db
- ./logs:/app/logs - ./logs:/app/logs
environment: environment:
- TZ=Europe/Paris - TZ=Europe/Paris
container_name: rtf-discord-bot
# Optional: Resource limits
deploy: deploy:
resources: resources:
limits: limits:
@ -159,31 +163,36 @@ Add Reactions
URL : `https://discord.com/api/oauth2/authorize?client_id=YOUR_BOT_ID&permissions=378944&scope=bot` URL : `https://discord.com/api/oauth2/authorize?client_id=YOUR_BOT_ID&permissions=378944&scope=bot`
## 🚀 Déploiement ## 🚀 Déploiement sur QNAP
Le déploiement est automatisé via Forgejo Actions. ### Via Container Station (Recommandé)
Un push sur `main` déploie en production, un push sur `dev` déploie en développement. 1. Install Container Station via App Center
2. Create /share/Container/discord-bot/
3. Copy all files into this folder
4. Edit .env with your tokens
5. Container Station → "Create" → "Create Application via docker-compose"
6. Select your docker-compose.yml
7. Start the container
### Prérequis (une seule fois) ### Via SSH (Alternative)
1. Créer le `.env` sur le NAS dans chaque `DEPLOY_PATH` :
```env
DISCORD_TOKEN=your_bot_token_here
AUTHORIZED_CHANNEL_ID=your_channel_id_here
```
2. Ajouter le secret `NAS_SSH_KEY` dans Forgejo (clé ed25519 encodée en base64) :
```bash
base64 -w 0 ~/.ssh/runner-nas
```
### Démarrage initial (première fois sur le NAS)
```bash ```bash
ssh Elewyn@192.168.1.208 # Connect to QNAP
cd /share/CACHEDEV1_DATA/discord-bot-prod # ou discord-bot-dev ssh admin@YOUR_QNAP_IP
/share/CACHEDEV1_DATA/.qpkg/container-station/usr/bin/docker compose up -d
# Navigate to folder
cd /share/Container/discord-bot/
# Install dependencies (if Python is installed)
pip3 install -r requirements.txt
# Test run
python3 bot.py
# Create auto-start service via QNAP interface
# Control Panel → Applications → Autorun
# Add: cd /share/Container/discord-bot && python3 bot.py &
``` ```
## 📊 SQLite Database ## 📊 SQLite Database

18
bot.py
View file

@ -16,20 +16,20 @@ from utils.leaderboard_handler import set_db_manager
os.environ["PYTHONIOENCODING"] = "utf-8" os.environ["PYTHONIOENCODING"] = "utf-8"
sys.stdout.reconfigure(encoding='utf-8') sys.stdout.reconfigure(encoding='utf-8')
# Define intents # Définir les intents
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
# Initialize managers # Initialisation des managers
db_manager = DatabaseManager() db_manager = DatabaseManager()
screenshot_manager = ScreenshotManager() screenshot_manager = ScreenshotManager()
mercy_manager = MercyManager() mercy_manager = MercyManager()
# Inject managers into handlers # Injection des managers dans les handlers
set_managers(db_manager, screenshot_manager) # pb_handler set_managers(db_manager, screenshot_manager) # pb_handler
set_db_manager(db_manager) # leaderboard_handler set_db_manager(db_manager) # leaderboard_handler
# Cog list # Liste des cogs
initial_cogs = [ initial_cogs = [
"cogs.guide", "cogs.guide",
"cogs.pbhydra", "cogs.pbhydra",
@ -40,7 +40,7 @@ initial_cogs = [
"cogs.mercy", "cogs.mercy",
] ]
# Directory list # Liste des dossiers
folders = [ folders = [
"screenshots/hydra/normal", "screenshots/hydra/normal",
"screenshots/hydra/hard", "screenshots/hydra/hard",
@ -55,7 +55,7 @@ folders = [
"screenshots/cvc", "screenshots/cvc",
] ]
# Create directories if needed (exist_ok=True avoids overwriting) # Création des dossiers si nécessaire (exist_ok=True évite d'écraser)
for f in folders: for f in folders:
os.makedirs(f, exist_ok=True) os.makedirs(f, exist_ok=True)
@ -70,12 +70,12 @@ class MyBot(commands.Bot):
for cog in initial_cogs: for cog in initial_cogs:
try: try:
await self.load_extension(cog) await self.load_extension(cog)
print(f"[OK] Cog {cog} loaded") print(f"[OK] Cog {cog} chargé")
except Exception as e: except Exception as e:
print(f"[ERROR] Failed to load {cog}: {e}") print(f"[ERREUR] Impossible de charger {cog}: {e}")
async def on_ready(self): async def on_ready(self):
print(f"{self.user.name} connected!") print(f"{self.user.name} est connecté !")
bot = MyBot() bot = MyBot()
bot.run(DISCORD_TOKEN) bot.run(DISCORD_TOKEN)

View file

@ -4,14 +4,14 @@ from discord.ext import commands
from config import AUTHORIZED_CHANNEL_ID from config import AUTHORIZED_CHANNEL_ID
class Guide(commands.Cog): class Guide(commands.Cog):
"""Shows the list of available commands""" """Affiche la liste des commandes disponibles"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@commands.command(name="guide") @commands.command(name="guide")
async def guide(self, ctx): async def guide(self, ctx):
"""Shows all available commands with difficulties""" """Affiche toutes les commandes disponibles avec les difficultés"""
if ctx.channel.id != AUTHORIZED_CHANNEL_ID: if ctx.channel.id != AUTHORIZED_CHANNEL_ID:
return return
@ -21,7 +21,7 @@ class Guide(commands.Cog):
color=0x00bfff color=0x00bfff
) )
# Damage format info # Info sur les formats de dégâts
embed.add_field( embed.add_field(
name="💠 Damage Formats", name="💠 Damage Formats",
value="**Accepted formats:** `1500000`, `1.5M`, `500K`, `2B`\n" value="**Accepted formats:** `1500000`, `1.5M`, `500K`, `2B`\n"
@ -30,7 +30,7 @@ class Guide(commands.Cog):
inline=False inline=False
) )
# Hydra PB commands # Commandes PB Hydra
embed.add_field( embed.add_field(
name="🐍 Hydra Commands", name="🐍 Hydra Commands",
value="**Difficulties:** Normal | Hard | Brutal | Nightmare (nm)\n" value="**Difficulties:** Normal | Hard | Brutal | Nightmare (nm)\n"
@ -40,7 +40,7 @@ class Guide(commands.Cog):
inline=False inline=False
) )
# Chimera PB commands # Commandes PB Chimera
embed.add_field( embed.add_field(
name="🦁 Chimera Commands", name="🦁 Chimera Commands",
value="**Difficulties:** Easy | Normal | Hard | Brutal | Nightmare (nm) | Ultra (unm)\n" value="**Difficulties:** Easy | Normal | Hard | Brutal | Nightmare (nm) | Ultra (unm)\n"
@ -50,7 +50,7 @@ class Guide(commands.Cog):
inline=False inline=False
) )
# CvC PB commands # Commandes PB CvC
embed.add_field( embed.add_field(
name="⚔️ CvC Commands", name="⚔️ CvC Commands",
value="`!pbcvc <damage>` - Submit PB + screenshot\n" value="`!pbcvc <damage>` - Submit PB + screenshot\n"
@ -59,7 +59,7 @@ class Guide(commands.Cog):
inline=False inline=False
) )
# Mercy commands # Commandes Mercy
embed.add_field( embed.add_field(
name="🎲 Mercy Commands", name="🎲 Mercy Commands",
value="`!mercy show` - Show your current mercy pulls\n" value="`!mercy show` - Show your current mercy pulls\n"
@ -69,7 +69,7 @@ class Guide(commands.Cog):
inline=False inline=False
) )
# Global leaderboards # Classements globaux
embed.add_field( embed.add_field(
name="🌍 Global Leaderboards", name="🌍 Global Leaderboards",
value="`!top10hydra <difficulty>` - Global Hydra rankings\n" value="`!top10hydra <difficulty>` - Global Hydra rankings\n"
@ -78,7 +78,7 @@ class Guide(commands.Cog):
inline=False inline=False
) )
# Clan leaderboards # Classements par clan
embed.add_field( embed.add_field(
name="🏆 Clan Leaderboards", name="🏆 Clan Leaderboards",
value="**🔥 TEAI (Inferno):** `!teaihydra <diff>` `!teaichimera <diff>` `!teaicvc`\n" value="**🔥 TEAI (Inferno):** `!teaihydra <diff>` `!teaichimera <diff>` `!teaicvc`\n"
@ -88,7 +88,7 @@ class Guide(commands.Cog):
inline=False inline=False
) )
# Stats and help # Stats et aide
embed.add_field( embed.add_field(
name="📈 Stats & Info", name="📈 Stats & Info",
value="`!mystats` - View all your PBs\n" value="`!mystats` - View all your PBs\n"
@ -97,7 +97,7 @@ class Guide(commands.Cog):
inline=False inline=False
) )
# Examples # Instructions
embed.add_field( embed.add_field(
name="⚡ Examples", name="⚡ Examples",
value="`!pbhydra brutal 1.5M` - Submit Brutal Hydra PB\n" value="`!pbhydra brutal 1.5M` - Submit Brutal Hydra PB\n"

View file

@ -8,7 +8,7 @@ from utils.helpers import calc_chance_and_guarantee
VALID_SHARDS = ["ancient", "void", "sacred", "primal", "remnant"] VALID_SHARDS = ["ancient", "void", "sacred", "primal", "remnant"]
class Mercy(commands.Cog): class Mercy(commands.Cog):
"""Cog for managing Mercy pulls""" """Cog pour gérer les pulls de Mercy"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot

View file

@ -3,17 +3,17 @@ import discord
from discord.ext import commands from discord.ext import commands
from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG
from utils.helpers import format_damage_display, format_date_only from utils.helpers import format_damage_display, format_date_only
from utils.pb_handler import db_manager # Make sure db_manager is initialized correctly from utils.pb_handler import db_manager # Assurez-vous que db_manager est initialisé correctement
class MyStats(commands.Cog): class MyStats(commands.Cog):
"""Cog for displaying all PBs for a user""" """Cog pour afficher tous les PB d'un utilisateur"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@commands.command(name="mystats") @commands.command(name="mystats")
async def mystats(self, ctx, *, target_user: str = None): async def mystats(self, ctx, *, target_user: str = None):
"""Shows all PBs for a user across all difficulties""" """Affiche tous les PB d'un utilisateur avec les nouvelles difficultés"""
if ctx.channel.id != AUTHORIZED_CHANNEL_ID: if ctx.channel.id != AUTHORIZED_CHANNEL_ID:
return return
@ -42,7 +42,7 @@ class MyStats(commands.Cog):
color=0x00bfff color=0x00bfff
) )
# Hydra - all difficulties # Hydra - toutes les difficultés
hydra_stats = [] hydra_stats = []
for difficulty in BOSS_CONFIG['hydra']['difficulties']: for difficulty in BOSS_CONFIG['hydra']['difficulties']:
pb_key = f'pb_hydra_{difficulty}' pb_key = f'pb_hydra_{difficulty}'
@ -57,7 +57,7 @@ class MyStats(commands.Cog):
hydra_text = "\n".join(hydra_stats) if hydra_stats else "No records" hydra_text = "\n".join(hydra_stats) if hydra_stats else "No records"
embed.add_field(name="⚔️ Hydra PBs", value=hydra_text, inline=False) embed.add_field(name="⚔️ Hydra PBs", value=hydra_text, inline=False)
# Chimera - all difficulties # Chimera - toutes les difficultés
chimera_stats = [] chimera_stats = []
for difficulty in BOSS_CONFIG['chimera']['difficulties']: for difficulty in BOSS_CONFIG['chimera']['difficulties']:
pb_key = f'pb_chimera_{difficulty}' pb_key = f'pb_chimera_{difficulty}'
@ -83,7 +83,7 @@ class MyStats(commands.Cog):
cvc_text += f"{formatted_date}" cvc_text += f"{formatted_date}"
embed.add_field(name="🗡️ CvC PB", value=cvc_text, inline=False) embed.add_field(name="🗡️ CvC PB", value=cvc_text, inline=False)
# Combined total # 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_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 += sum(user_data.get(f'pb_chimera_{d}', 0) for d in BOSS_CONFIG['chimera']['difficulties'])
total_damage += user_data.get('pb_cvc', 0) total_damage += user_data.get('pb_cvc', 0)

View file

@ -4,14 +4,14 @@ from discord.ext import commands
from utils.pb_handler import handle_pb_command from utils.pb_handler import handle_pb_command
class Pbchimera(commands.Cog): class Pbchimera(commands.Cog):
"""Cog for managing Chimera Personal Bests""" """Cog pour gérer les Personal Bests Chimera"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@commands.command(name="pbchimera") @commands.command(name="pbchimera")
async def pbchimera(self, ctx, arg1: str = None, *, arg2: str = None): async def pbchimera(self, ctx, arg1: str = None, *, arg2: str = None):
"""!pbchimera command with difficulty handling""" """Commande !pbchimera avec gestion des difficultés"""
await handle_pb_command(ctx, 'chimera', arg1, arg2) await handle_pb_command(ctx, 'chimera', arg1, arg2)
async def setup(bot): async def setup(bot):

View file

@ -4,14 +4,14 @@ from discord.ext import commands
from utils.pb_handler import handle_pb_command from utils.pb_handler import handle_pb_command
class Pbcvc(commands.Cog): class Pbcvc(commands.Cog):
"""Cog for managing CvC Personal Bests (no difficulties)""" """Cog pour gérer les Personal Bests CvC (sans difficultés)"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@commands.command(name="pbcvc") @commands.command(name="pbcvc")
async def pbcvc(self, ctx, *, target_user: str = None): async def pbcvc(self, ctx, *, target_user: str = None):
"""!pbcvc command""" """Commande !pbcvc"""
await handle_pb_command(ctx, 'cvc', target_user) await handle_pb_command(ctx, 'cvc', target_user)
async def setup(bot): async def setup(bot):

View file

@ -4,14 +4,14 @@ from discord.ext import commands
from utils.pb_handler import handle_pb_command from utils.pb_handler import handle_pb_command
class Pbhydra(commands.Cog): class Pbhydra(commands.Cog):
"""Cog for managing Hydra Personal Bests""" """Cog pour gérer les Personal Bests Hydra"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@commands.command(name="pbhydra") @commands.command(name="pbhydra")
async def pbhydra(self, ctx, arg1: str = None, *, arg2: str = None): async def pbhydra(self, ctx, arg1: str = None, *, arg2: str = None):
"""!pbhydra command with difficulty handling""" """Commande !pbhydra avec gestion des difficultés"""
await handle_pb_command(ctx, 'hydra', arg1, arg2) await handle_pb_command(ctx, 'hydra', arg1, arg2)
async def setup(bot): async def setup(bot):

View file

@ -6,12 +6,12 @@ from utils.helpers import normalize_difficulty
from config import BOSS_CONFIG from config import BOSS_CONFIG
class Top10(commands.Cog): class Top10(commands.Cog):
"""Cog grouping all global and per-clan leaderboard commands""" """Cog regroupant toutes les commandes de leaderboard globales et par clan"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
# --- Global commands --- # --- Commandes globales ---
@commands.command() @commands.command()
async def top10hydra(self, ctx, difficulty: str = None): async def top10hydra(self, ctx, difficulty: str = None):
if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['hydra']['difficulties']: if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['hydra']['difficulties']:
@ -32,7 +32,7 @@ class Top10(commands.Cog):
async def top10cvc(self, ctx): async def top10cvc(self, ctx):
await show_leaderboard(ctx, 'cvc') await show_leaderboard(ctx, 'cvc')
# --- TEAI clan commands (Inferno) --- # --- Commandes par clan TEAI (Inferno) ---
@commands.command() @commands.command()
async def teaihydra(self, ctx, difficulty: str = None): async def teaihydra(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAI') await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAI')
@ -45,7 +45,7 @@ class Top10(commands.Cog):
async def teaicvc(self, ctx): async def teaicvc(self, ctx):
await show_leaderboard(ctx, 'cvc', clan='TEAI') await show_leaderboard(ctx, 'cvc', clan='TEAI')
# --- TEAF clan commands (Flame) --- # --- Commandes par clan TEAF (Flame) ---
@commands.command() @commands.command()
async def teafhydra(self, ctx, difficulty: str = None): async def teafhydra(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAF') await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAF')
@ -58,7 +58,7 @@ class Top10(commands.Cog):
async def teafcvc(self, ctx): async def teafcvc(self, ctx):
await show_leaderboard(ctx, 'cvc', clan='TEAF') await show_leaderboard(ctx, 'cvc', clan='TEAF')
# --- TEAC clan commands (Cinder) --- # --- Commandes par clan TEAC (Cinder) ---
@commands.command() @commands.command()
async def teachydra(self, ctx, difficulty: str = None): async def teachydra(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAC') await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAC')
@ -71,7 +71,7 @@ class Top10(commands.Cog):
async def teaccvc(self, ctx): async def teaccvc(self, ctx):
await show_leaderboard(ctx, 'cvc', clan='TEAC') await show_leaderboard(ctx, 'cvc', clan='TEAC')
# --- TEACO clan commands (Corrupted Olympians) --- # --- Commandes par clan TEACO (Corrupted Olympians) ---
@commands.command() @commands.command()
async def teacohydra(self, ctx, difficulty: str = None): async def teacohydra(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEACO') await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEACO')
@ -84,12 +84,12 @@ class Top10(commands.Cog):
async def teacocvc(self, ctx): async def teacocvc(self, ctx):
await show_leaderboard(ctx, 'cvc', clan='TEACO') await show_leaderboard(ctx, 'cvc', clan='TEACO')
# --- Internal helper method --- # --- Méthode interne pour éviter la répétition ---
async def _show_clan_leaderboard(self, ctx, boss_type, difficulty, clan): async def _show_clan_leaderboard(self, ctx, boss_type, difficulty, clan):
"""Shows the leaderboard for a specific boss and clan""" """Affiche le leaderboard pour un boss et un clan spécifique"""
if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG[boss_type]['difficulties']: if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG[boss_type]['difficulties']:
await show_leaderboard(ctx, boss_type, difficulty, clan) await show_leaderboard(ctx, boss_type, difficulty, clan)
elif boss_type != 'cvc': # CvC has no difficulties elif boss_type != 'cvc': # CvC na pas de difficultés
difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties']) difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties'])
await ctx.send( await ctx.send(
f"❌ Please specify difficulty: `!{ctx.command.name} <difficulty>`\n" f"❌ Please specify difficulty: `!{ctx.command.name} <difficulty>`\n"

View file

@ -4,15 +4,15 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
# Token and authorized channel # Token et channel autorisé
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
AUTHORIZED_CHANNEL_ID = int(os.getenv("AUTHORIZED_CHANNEL_ID")) AUTHORIZED_CHANNEL_ID = int(os.getenv("AUTHORIZED_CHANNEL_ID"))
# Paths # Chemins
SCREENSHOTS_BASE_PATH = "/app/screenshots" SCREENSHOTS_BASE_PATH = "/app/screenshots"
DATABASE_PATH = "/app/data/bot_data.db" DATABASE_PATH = "/app/data/bot_data.db"
# TEA clan configuration - The Ember Accord # Configuration des clans TEA - The Ember Accord
CLAN_CONFIG = { CLAN_CONFIG = {
'TEAI': {'name': 'TEAI', 'full_name': 'Inferno', 'emoji': '🔥', 'color': 0xff4500}, 'TEAI': {'name': 'TEAI', 'full_name': 'Inferno', 'emoji': '🔥', 'color': 0xff4500},
'TEAF': {'name': 'TEAF', 'full_name': 'Flame', 'emoji': '🛡️', 'color': 0x00ff00}, 'TEAF': {'name': 'TEAF', 'full_name': 'Flame', 'emoji': '🛡️', 'color': 0x00ff00},
@ -20,22 +20,22 @@ CLAN_CONFIG = {
'TEACO': {'name': 'TEACO', 'full_name': 'Corrupted Olympians', 'emoji': '👑', 'color': 0x9932cc}, 'TEACO': {'name': 'TEACO', 'full_name': 'Corrupted Olympians', 'emoji': '👑', 'color': 0x9932cc},
} }
# Discord role ID to clan key mapping # Mapping role Discord ID → clé de clan
CLAN_ROLE_IDS = { CLAN_ROLE_IDS = {
1190674529731747901: 'TEAI', 1505856647744979004: 'TEAI',
1197646966599983185: 'TEAF', 1505856725490733056: 'TEAF',
1220014404809261076: 'TEAC', 1505856808361656370: 'TEAC',
1496965820868198550: 'TEACO', 1505856914439929856: 'TEACO',
} }
# Old clan to new clan mapping (existing database migration) # Mapping anciens clans → nouveaux (migration base existante)
CLAN_MIGRATION = { CLAN_MIGRATION = {
'RTF': 'TEAI', 'RTF': 'TEAI',
'RTFC': 'TEAF', 'RTFC': 'TEAF',
'RTFR': 'TEAC', 'RTFR': 'TEAC',
} }
# Boss configuration with difficulties # Configuration des boss avec difficultés
BOSS_CONFIG = { BOSS_CONFIG = {
'hydra': {'name': 'Hydra', 'emoji': '📍', 'color': 0xff6b35, 'hydra': {'name': 'Hydra', 'emoji': '📍', 'color': 0xff6b35,
'difficulties': ['normal', 'hard', 'brutal', 'nightmare']}, 'difficulties': ['normal', 'hard', 'brutal', 'nightmare']},
@ -44,7 +44,7 @@ BOSS_CONFIG = {
'cvc': {'name': 'Clan vs Clan', 'emoji': '✔️', 'color': 0xff0000, 'difficulties': []} 'cvc': {'name': 'Clan vs Clan', 'emoji': '✔️', 'color': 0xff0000, 'difficulties': []}
} }
# Difficulty shortcut mappings # Mappings pour diminutifs de difficultés
DIFFICULTY_SHORTCUTS = { DIFFICULTY_SHORTCUTS = {
'nm': 'nightmare', 'nm': 'nightmare',
'unm': 'ultra' 'unm': 'ultra'

View file

@ -8,12 +8,12 @@ class DatabaseManager:
self.init_database() self.init_database()
def init_database(self): def init_database(self):
"""Initializes the database with all columns for difficulties""" """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) os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
# Main table with all difficulties # Table principale avec toutes les difficultés
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -54,7 +54,7 @@ class DatabaseManager:
pb_chimera_ultra_screenshot TEXT, pb_chimera_ultra_screenshot TEXT,
pb_chimera_ultra_date TIMESTAMP, pb_chimera_ultra_date TIMESTAMP,
-- CvC (unchanged) -- CvC (inchangé)
pb_cvc INTEGER DEFAULT 0, pb_cvc INTEGER DEFAULT 0,
pb_cvc_screenshot TEXT, pb_cvc_screenshot TEXT,
pb_cvc_date TIMESTAMP, pb_cvc_date TIMESTAMP,
@ -64,7 +64,7 @@ class DatabaseManager:
) )
''') ''')
# Existing data migration (if needed) # Migration des données existantes (si nécessaire)
cursor.execute("PRAGMA table_info(users)") cursor.execute("PRAGMA table_info(users)")
columns = [row[1] for row in cursor.fetchall()] columns = [row[1] for row in cursor.fetchall()]
@ -73,7 +73,7 @@ class DatabaseManager:
if 'clan' not in columns: if 'clan' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN clan TEXT') cursor.execute('ALTER TABLE users ADD COLUMN clan TEXT')
# Auto-migration: derive clan from old username prefix # Migration automatique : déduction du clan depuis l'ancien préfixe du pseudo
for old_tag, new_clan in CLAN_MIGRATION.items(): for old_tag, new_clan in CLAN_MIGRATION.items():
cursor.execute( cursor.execute(
"UPDATE users SET clan = ? WHERE clan IS NULL AND (" "UPDATE users SET clan = ? WHERE clan IS NULL AND ("
@ -81,7 +81,7 @@ class DatabaseManager:
(new_clan, f'[{old_tag}] %', f'[{old_tag}]%') (new_clan, f'[{old_tag}] %', f'[{old_tag}]%')
) )
# Global history table # Table pour l'historique global
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS pb_history ( CREATE TABLE IF NOT EXISTS pb_history (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -99,7 +99,7 @@ class DatabaseManager:
conn.close() conn.close()
def get_user_pb(self, user_id, boss_type, difficulty=None): def get_user_pb(self, user_id, boss_type, difficulty=None):
"""Returns PB for a user on a specific boss and difficulty""" """Récupère le PB d'un utilisateur pour un boss et difficulté spécifique"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
@ -118,11 +118,11 @@ class DatabaseManager:
return result if result else (0, None, None) return result if result else (0, None, None)
def update_user_pb(self, user_id, username, boss_type, damage, screenshot_filename, difficulty=None, clan=None): def update_user_pb(self, user_id, username, boss_type, damage, screenshot_filename, difficulty=None, clan=None):
"""Updates a user's PB and deletes the previous screenshot""" """Met à jour le PB d'un utilisateur et supprime l'ancien screenshot"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
# Get old screenshot for deletion # Récupérer l'ancien screenshot pour le supprimer
old_data = self.get_user_pb(user_id, boss_type, difficulty) old_data = self.get_user_pb(user_id, boss_type, difficulty)
old_screenshot = old_data[1] if old_data else None old_screenshot = old_data[1] if old_data else None
@ -131,7 +131,7 @@ class DatabaseManager:
else: else:
column_prefix = f"pb_{boss_type}" column_prefix = f"pb_{boss_type}"
# COALESCE(?, clan): keeps existing clan if detection returns None # COALESCE(?, clan) : on n'écrase pas le clan existant si la détection retourne None
cursor.execute(f''' cursor.execute(f'''
INSERT INTO users (discord_id, discord_username, clan, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts) INSERT INTO users (discord_id, discord_username, clan, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, 1) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, 1)
@ -145,7 +145,7 @@ class DatabaseManager:
total_attempts = total_attempts + 1 total_attempts = total_attempts + 1
''', (str(user_id), username, clan, damage, screenshot_filename, username, clan, damage, screenshot_filename)) ''', (str(user_id), username, clan, damage, screenshot_filename, username, clan, damage, screenshot_filename))
# Add to history # Ajouter à l'historique
cursor.execute(''' cursor.execute('''
INSERT INTO pb_history (discord_id, username, boss_type, difficulty, damage, screenshot_filename) INSERT INTO pb_history (discord_id, username, boss_type, difficulty, damage, screenshot_filename)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
@ -157,7 +157,7 @@ class DatabaseManager:
return old_screenshot return old_screenshot
def get_leaderboard(self, boss_type, difficulty=None, limit=10, clan=None): def get_leaderboard(self, boss_type, difficulty=None, limit=10, clan=None):
"""Returns the leaderboard for a specific boss and difficulty""" """Récupère le classement pour un boss et difficulté spécifique"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
@ -186,11 +186,11 @@ class DatabaseManager:
return results return results
def get_user_all_pbs(self, user_id): def get_user_all_pbs(self, user_id):
"""Returns all PBs for a user""" """Récupère tous les PB d'un utilisateur"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
# Retrieve all PB columns # Récupérer toutes les colonnes de PB
cursor.execute('SELECT * FROM users WHERE discord_id = ?', (str(user_id),)) cursor.execute('SELECT * FROM users WHERE discord_id = ?', (str(user_id),))
result = cursor.fetchone() result = cursor.fetchone()
columns = [desc[0] for desc in cursor.description] columns = [desc[0] for desc in cursor.description]
@ -202,7 +202,7 @@ class DatabaseManager:
return dict(zip(columns, result)) return dict(zip(columns, result))
def find_user_by_name(self, username): def find_user_by_name(self, username):
"""Finds a user by name (for backwards compatibility)""" """Trouve un utilisateur par son nom (pour rétrocompatibilité)"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()

View file

@ -3,7 +3,7 @@ import sqlite3
from datetime import datetime from datetime import datetime
from config import DATABASE_PATH from config import DATABASE_PATH
# Mercy rules for storage # Règles de mercy pour stockage
MERCY_RULES = { MERCY_RULES = {
"ancient": {"threshold": 200, "increment": 0.5, "base": 0}, "ancient": {"threshold": 200, "increment": 0.5, "base": 0},
"void": {"threshold": 200, "increment": 0.5, "base": 0}, "void": {"threshold": 200, "increment": 0.5, "base": 0},
@ -19,7 +19,7 @@ class MercyManager:
self.init_table() self.init_table()
def init_table(self): def init_table(self):
"""Initializes the mercy counters table""" """Initialise la table des compteurs de mercy"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
@ -35,7 +35,7 @@ class MercyManager:
conn.close() conn.close()
def get_pulls(self, user_id, shard_type): def get_pulls(self, user_id, shard_type):
"""Returns current pull count for a user and shard type""" """Retourne le nombre de pulls actuels pour un utilisateur et un type de shard"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
@ -47,11 +47,11 @@ class MercyManager:
return row[0] if row else 0 return row[0] if row else 0
def add_pulls(self, user_id, shard_type, pulls): def add_pulls(self, user_id, shard_type, pulls):
"""Adds pulls for a user, handling INSERT/UPDATE correctly""" """Ajoute des pulls pour un utilisateur en gérant correctement l'INSERT/UPDATE"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
# Check if record exists # Vérifie si l'enregistrement existe
cursor.execute( cursor.execute(
"SELECT pulls FROM mercy_counters WHERE user_id = ? AND shard_type = ?", "SELECT pulls FROM mercy_counters WHERE user_id = ? AND shard_type = ?",
(user_id, shard_type) (user_id, shard_type)
@ -76,7 +76,7 @@ class MercyManager:
return new_pulls return new_pulls
def reset_pulls(self, user_id, shard_type): def reset_pulls(self, user_id, shard_type):
"""Resets pull count for a user on a shard""" """Réinitialise les pulls d'un utilisateur pour un shard"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
@ -87,7 +87,7 @@ class MercyManager:
conn.close() conn.close()
def get_all_pulls(self, user_id): def get_all_pulls(self, user_id):
"""Returns all pull counts for a user across all shards""" """Retourne tous les pulls d'un utilisateur pour tous les shards"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
@ -99,14 +99,14 @@ class MercyManager:
return {shard_type: pulls for shard_type, pulls in rows} return {shard_type: pulls for shard_type, pulls in rows}
def get_mercy_chance(self, shard_type, pulls): def get_mercy_chance(self, shard_type, pulls):
"""Calculates mercy probability based on pull count""" """Calcule la probabilité de mercy selon le nombre de pulls"""
rule = MERCY_RULES[shard_type] rule = MERCY_RULES[shard_type]
if pulls <= rule["threshold"]: if pulls <= rule["threshold"]:
return rule["base"] return rule["base"]
return rule["base"] + (pulls - rule["threshold"]) * rule["increment"] return rule["base"] + (pulls - rule["threshold"]) * rule["increment"]
def pulls_until_guaranteed(self, shard_type, pulls): def pulls_until_guaranteed(self, shard_type, pulls):
"""Returns pulls remaining until guaranteed loot""" """Retourne combien de pulls restent avant un loot garanti"""
rules = { rules = {
"ancient": {"start": 200, "increment": 5, "base": 0.5}, "ancient": {"start": 200, "increment": 5, "base": 0.5},
"void": {"start": 200, "increment": 5, "base": 0.5}, "void": {"start": 200, "increment": 5, "base": 0.5},

View file

@ -7,18 +7,18 @@ from config import SCREENSHOTS_BASE_PATH, BOSS_CONFIG
class ScreenshotManager: class ScreenshotManager:
def __init__(self, base_path=SCREENSHOTS_BASE_PATH): def __init__(self, base_path=SCREENSHOTS_BASE_PATH):
self.base_path = base_path self.base_path = base_path
# Create directories for each boss and difficulty # Créer les dossiers pour chaque boss et difficulté
for boss_type in BOSS_CONFIG.keys(): for boss_type in BOSS_CONFIG.keys():
boss_path = os.path.join(base_path, boss_type) boss_path = os.path.join(base_path, boss_type)
os.makedirs(boss_path, exist_ok=True) os.makedirs(boss_path, exist_ok=True)
# Create subdirectories for difficulties # Créer sous-dossiers pour les difficultés
for difficulty in BOSS_CONFIG[boss_type]['difficulties']: for difficulty in BOSS_CONFIG[boss_type]['difficulties']:
difficulty_path = os.path.join(boss_path, difficulty) difficulty_path = os.path.join(boss_path, difficulty)
os.makedirs(difficulty_path, exist_ok=True) os.makedirs(difficulty_path, exist_ok=True)
async def save_screenshot(self, attachment, username, damage, boss_type, difficulty=None): async def save_screenshot(self, attachment, username, damage, boss_type, difficulty=None):
"""Saves the screenshot locally""" """Sauvegarde le screenshot localement"""
try: try:
timestamp = int(datetime.now().timestamp()) timestamp = int(datetime.now().timestamp())
file_extension = attachment.filename.split('.')[-1].lower() file_extension = attachment.filename.split('.')[-1].lower()
@ -34,18 +34,18 @@ class ScreenshotManager:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(attachment.url) as resp: async with session.get(attachment.url) as resp:
if resp.status == 200: if resp.status == 200:
# Binary mode, no encoding issues # Ouverture en binaire, pas de problème d'encodage
with open(filepath, 'wb') as f: with open(filepath, 'wb') as f:
f.write(await resp.read()) f.write(await resp.read())
return filename return filename
return None return None
except Exception as e: except Exception as e:
print(f"Screenshot save error: {str(e)}") print(f"Erreur sauvegarde screenshot: {str(e)}")
return None return None
def get_screenshot_path(self, filename, boss_type, difficulty=None): def get_screenshot_path(self, filename, boss_type, difficulty=None):
"""Returns the full path to the screenshot""" """Retourne le chemin complet du screenshot"""
if filename: if filename:
if difficulty: if difficulty:
return os.path.join(self.base_path, boss_type, difficulty, filename) return os.path.join(self.base_path, boss_type, difficulty, filename)
@ -54,12 +54,12 @@ class ScreenshotManager:
return None return None
def delete_old_screenshot(self, filename, boss_type, difficulty=None): def delete_old_screenshot(self, filename, boss_type, difficulty=None):
"""Deletes the old screenshot""" """Supprime l'ancien screenshot"""
if filename: if filename:
old_path = self.get_screenshot_path(filename, boss_type, difficulty) old_path = self.get_screenshot_path(filename, boss_type, difficulty)
if old_path and os.path.exists(old_path): if old_path and os.path.exists(old_path):
try: try:
os.remove(old_path) os.remove(old_path)
print(f"Old screenshot deleted: {filename}") print(f"Ancien screenshot supprimé: {filename}")
except Exception as e: except Exception as e:
print(f"Screenshot deletion error: {str(e)}") print(f"Erreur suppression screenshot: {str(e)}")

View file

@ -5,7 +5,7 @@ from typing import Optional
from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS, CLAN_ROLE_IDS from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS, CLAN_ROLE_IDS
def parse_damage_amount(damage_str): def parse_damage_amount(damage_str):
"""Converts amounts with suffixes (K, M, B) to integers""" """Convertit les montants avec suffixes (K, M, B) en nombres entiers"""
if not damage_str: if not damage_str:
return None return None
damage_str = damage_str.strip().upper() damage_str = damage_str.strip().upper()
@ -23,7 +23,7 @@ def parse_damage_amount(damage_str):
return int(number * multipliers[suffix]) return int(number * multipliers[suffix])
def format_damage_display(damage): def format_damage_display(damage):
"""Formats a damage amount with the appropriate suffix""" """Formate un montant de dégâts avec le suffixe approprié"""
if damage >= 1_000_000_000: if damage >= 1_000_000_000:
billions = 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" return f"{int(billions)}B" if billions == int(billions) else f"{billions:.1f}B"
@ -36,7 +36,7 @@ def format_damage_display(damage):
return str(damage) return str(damage)
def normalize_difficulty(difficulty): def normalize_difficulty(difficulty):
"""Normalizes a difficulty string, handling shortcuts""" """Normalise une difficulté en gérant les diminutifs"""
if not difficulty: if not difficulty:
return None return None
difficulty_lower = difficulty.lower() difficulty_lower = difficulty.lower()
@ -45,14 +45,14 @@ def normalize_difficulty(difficulty):
return difficulty_lower return difficulty_lower
def get_clan_from_member(member) -> Optional[str]: def get_clan_from_member(member) -> Optional[str]:
"""Detects a member's clan via their Discord roles""" """Détecte le clan d'un membre via ses rôles Discord"""
for role in member.roles: for role in member.roles:
if role.id in CLAN_ROLE_IDS: if role.id in CLAN_ROLE_IDS:
return CLAN_ROLE_IDS[role.id] return CLAN_ROLE_IDS[role.id]
return None return None
def format_datetime(date_str): def format_datetime(date_str):
"""Formats a date in AM/PM format""" """Formate une date en format AM/PM"""
if not date_str: if not date_str:
return None return None
try: try:
@ -62,7 +62,7 @@ def format_datetime(date_str):
return None return None
def format_date_only(date_str): def format_date_only(date_str):
"""Formats a date without the time component""" """Formate une date sans l'heure"""
if not date_str: if not date_str:
return None return None
try: try:
@ -72,7 +72,7 @@ def format_date_only(date_str):
return None return None
def get_difficulty_display_name(difficulty): def get_difficulty_display_name(difficulty):
"""Converts an internal difficulty name to display name""" """Convertit le nom de difficulté en nom d'affichage"""
difficulty_names = { difficulty_names = {
'ultra': 'Ultra Nightmare', 'ultra': 'Ultra Nightmare',
'nightmare': 'Nightmare', 'nightmare': 'Nightmare',
@ -96,7 +96,7 @@ MERCY_RULES = {
} }
def calc_chance_and_guarantee(shard_type, pulls): def calc_chance_and_guarantee(shard_type, pulls):
"""Returns chance, guaranteed pull count, and remaining pulls""" """Retourne chance, pull garanti et pulls restants"""
if shard_type not in MERCY_RULES: if shard_type not in MERCY_RULES:
return 0, None, None return 0, None, None
rule = MERCY_RULES[shard_type] rule = MERCY_RULES[shard_type]

View file

@ -12,12 +12,12 @@ def set_db_manager(db):
db_manager = db db_manager = db
async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None): async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None):
"""Generic function to display leaderboards""" """Fonction générique pour afficher les classements"""
if ctx.channel.id != AUTHORIZED_CHANNEL_ID: if ctx.channel.id != AUTHORIZED_CHANNEL_ID:
return return
try: try:
# Normalize difficulty if specified # Normaliser la difficulté si spécifiée
if difficulty: if difficulty:
difficulty = normalize_difficulty(difficulty) difficulty = normalize_difficulty(difficulty)
if difficulty not in BOSS_CONFIG[boss_type]['difficulties']: if difficulty not in BOSS_CONFIG[boss_type]['difficulties']:
@ -34,7 +34,7 @@ async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None):
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 return
# Title with clan and difficulty if specified # Titre avec clan et difficulté si spécifiés
difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" 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"
@ -56,7 +56,7 @@ async def show_leaderboard(ctx, boss_type, difficulty=None, clan=None):
if formatted_date: if formatted_date:
date_text = f"{formatted_date}" date_text = f"{formatted_date}"
# Show clan emoji if leaderboard is global (not filtered by clan) # Afficher l'emoji du clan si le leaderboard est global (pas filtré par clan)
display_name = username display_name = username
if not clan and row_clan: if not clan and row_clan:
clan_emoji = CLAN_CONFIG.get(row_clan, {'emoji': '🏛️'})['emoji'] clan_emoji = CLAN_CONFIG.get(row_clan, {'emoji': '🏛️'})['emoji']

View file

@ -15,13 +15,13 @@ db_manager = None
screenshot_manager = None screenshot_manager = None
def set_managers(db, ss): def set_managers(db, ss):
"""Injects managers (called once from bot.py)""" """Injection des managers (appelée une seule fois depuis bot.py)"""
global db_manager, screenshot_manager global db_manager, screenshot_manager
db_manager = db db_manager = db
screenshot_manager = ss screenshot_manager = ss
async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None): async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None):
"""Generic handler for all PB commands""" """Fonction générique pour gérer toutes les commandes PB avec difficultés"""
if ctx.channel.id != AUTHORIZED_CHANNEL_ID: if ctx.channel.id != AUTHORIZED_CHANNEL_ID:
return return
@ -29,7 +29,7 @@ async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None):
difficulties = boss_info['difficulties'] difficulties = boss_info['difficulties']
try: try:
# For CvC (no difficulties) # Pour CvC (pas de difficultés)
if not difficulties: if not difficulties:
if arg1: if arg1:
damage = parse_damage_amount(arg1) damage = parse_damage_amount(arg1)
@ -41,7 +41,7 @@ async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None):
await show_user_pb(ctx, boss_type, None, ctx.author.display_name) await show_user_pb(ctx, boss_type, None, ctx.author.display_name)
return return
# For Hydra and Chimera (with difficulties) # Pour Hydra et Chimera (avec difficultés)
if not arg1: if not arg1:
difficulty_list = " | ".join([d.title() for d in difficulties]) difficulty_list = " | ".join([d.title() for d in difficulties])
await ctx.send( await ctx.send(
@ -81,7 +81,7 @@ async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None):
await ctx.send(f"⚠️ Error: {str(e)}") await ctx.send(f"⚠️ Error: {str(e)}")
async def handle_pb_submission(ctx, boss_type, difficulty, damage): async def handle_pb_submission(ctx, boss_type, difficulty, damage):
"""Handles submission of a new PB""" """Gère la soumission d'un nouveau PB"""
if not ctx.message.attachments: 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 return
@ -120,7 +120,7 @@ async def handle_pb_submission(ctx, boss_type, difficulty, damage):
) )
embed.add_field(name="📈 Improvement", value=f"+{format_damage_display(improvement)} damage", inline=True) embed.add_field(name="📈 Improvement", value=f"+{format_damage_display(improvement)} damage", inline=True)
# Send screenshot to Discord # Envoi du screenshot correctement pour Discord
screenshot_path = screenshot_manager.get_screenshot_path(screenshot_filename, boss_type, difficulty) screenshot_path = screenshot_manager.get_screenshot_path(screenshot_filename, boss_type, difficulty)
if screenshot_path and os.path.exists(screenshot_path): if screenshot_path and os.path.exists(screenshot_path):
file = discord.File(screenshot_path, filename=screenshot_filename) file = discord.File(screenshot_path, filename=screenshot_filename)
@ -131,19 +131,19 @@ async def handle_pb_submission(ctx, boss_type, difficulty, damage):
else: else:
await ctx.send("⚠️ Failed to save screenshot. Please try again.") await ctx.send("⚠️ Failed to save screenshot. Please try again.")
else: else:
# PB not beaten, show current PB # Si le PB n'est pas battu, on montre le PB existant
await show_user_pb(ctx, boss_type, difficulty, username) await show_user_pb(ctx, boss_type, difficulty, username)
async def show_user_pb(ctx, boss_type, difficulty, target_user): async def show_user_pb(ctx, boss_type, difficulty, target_user):
"""Displays the current PB for a user""" """Affiche le PB actuel d'un utilisateur"""
# If target_user is a username, try to find them # Si target_user est un nom d'utilisateur, on essaie de le trouver
if isinstance(target_user, str) and not target_user.isdigit(): if isinstance(target_user, str) and not target_user.isdigit():
# First check if it's the current user # D'abord, vérifier si c'est l'utilisateur actuel
if target_user.lower() == ctx.author.display_name.lower(): if target_user.lower() == ctx.author.display_name.lower():
user_id = ctx.author.id user_id = ctx.author.id
display_name = ctx.author.display_name display_name = ctx.author.display_name
else: else:
# Search in database # Chercher dans la base de données
matches = db_manager.find_user_by_name(target_user) matches = db_manager.find_user_by_name(target_user)
if not matches: if not matches:
await ctx.send(f"⚠️ User **{target_user}** not found in database.") await ctx.send(f"⚠️ User **{target_user}** not found in database.")
@ -154,7 +154,7 @@ async def show_user_pb(ctx, boss_type, difficulty, target_user):
else: else:
user_id, display_name = matches[0] user_id, display_name = matches[0]
else: else:
# Current user # Si c'est l'utilisateur actuel
user_id = ctx.author.id user_id = ctx.author.id
display_name = ctx.author.display_name display_name = ctx.author.display_name
@ -171,7 +171,7 @@ async def show_user_pb(ctx, boss_type, difficulty, target_user):
if date: if date:
embed.add_field(name="📅 Date", value=format_datetime(date), inline=True) embed.add_field(name="📅 Date", value=format_datetime(date), inline=True)
# Send local screenshot to Discord # Envoi du screenshot local correctement
if screenshot: if screenshot:
screenshot_path = screenshot_manager.get_screenshot_path(screenshot, boss_type, difficulty) screenshot_path = screenshot_manager.get_screenshot_path(screenshot, boss_type, difficulty)
if screenshot_path and os.path.exists(screenshot_path): if screenshot_path and os.path.exists(screenshot_path):