Compare commits

...

12 commits
dev ... main

Author SHA1 Message Date
LE BERRE Mickael
6621599fc6 i18n: translate all French comments, docstrings and logs to English
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 29s
discord.py's built-in !help command exposes cog docstrings directly to
Discord members — leaving them in French made the output partially French.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:56:36 +02:00
LE BERRE Mickael
ca8d761293 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:25:09 +02:00
LE BERRE Mickael
5cfa8db270 docs: explicit comment on StrictHostKeyChecking=no in deploy workflow
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 26s
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:30 +02:00
LE BERRE Mickael
20b86144b4 fix: mystats username lookup and multi-word name support
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 28s
- 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 12:06:59 +02:00
LE BERRE Mickael
57cd2517ad 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 12:06:59 +02:00
LE BERRE Mickael
cebd1f2e0a fix: replace str | None with Optional[str] for Python 3.9 compat
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 16s
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:47:20 +02:00
LE BERRE Mickael
acabc7d409 feat: rebrand RTF→TEA, clan detection via Discord roles
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 26s
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:38:01 +02:00
0cc1a4139d chore: add .gitignore (was only on dev)
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 23s
2026-04-30 16:54:05 +02:00
49ffd17528 chore: remove old GitHub Actions workflow
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 26s
2026-04-30 16:47:49 +02:00
0c46ef5a81 docs: update CLAUDE.md with CI/CD lessons, update README deployment section
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 28s
2026-04-30 16:46:57 +02:00
66cabe2fec fix: exclude __pycache__ and .github from rsync
All checks were successful
Deploy Bot on NAS / deploy (push) Successful in 24s
2026-04-30 16:44:36 +02:00
cf6f68c31c fix: sync CI/CD fixes from dev to main
Some checks failed
Deploy Bot on NAS / deploy (push) Failing after 2s
2026-04-30 16:42:57 +02:00
21 changed files with 1236 additions and 1219 deletions

View file

@ -8,8 +8,13 @@ jobs:
deploy: deploy:
runs-on: self-hosted runs-on: self-hosted
steps: steps:
- name: Install dependencies
run: apk add --no-cache rsync openssh-client
- name: Checkout - name: Checkout
uses: actions/checkout@v4 run: |
git clone --depth 1 --branch ${{ github.ref_name }} \
${{ github.server_url }}/${{ github.repository }}.git .
- name: Set deployment path - name: Set deployment path
run: | run: |
@ -23,29 +28,29 @@ jobs:
- name: Configure SSH - name: Configure SSH
run: | run: |
mkdir -p ~/.ssh mkdir -p /root/.ssh
echo "${{ secrets.NAS_SSH_KEY }}" > ~/.ssh/id_deploy echo "${{ secrets.NAS_SSH_KEY }}" > /root/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy chmod 600 /root/.ssh/id_deploy
cat >> ~/.ssh/config << 'EOF'
Host nas
HostName 192.168.1.208
User Elewyn
IdentityFile ~/.ssh/id_deploy
StrictHostKeyChecking no
EOF
- name: Sync files to NAS - name: Sync files to NAS
run: | run: |
# StrictHostKeyChecking=no : runner Alpine stateless, pas de known_hosts persistant.
# Cible fixe sur LAN interne (192.168.1.208) — risque MITM inexistant.
rsync -av --delete \ rsync -av --delete \
-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/' \
./ nas:${{ env.DEPLOY_PATH }}/ --exclude='__pycache__/' \
./ Elewyn@192.168.1.208:${{ env.DEPLOY_PATH }}/
- name: Restart bot on NAS - name: Restart bot on NAS
run: | run: |
ssh nas "cd ${{ env.DEPLOY_PATH }} && \ # StrictHostKeyChecking=no : runner Alpine stateless, pas de known_hosts persistant.
docker compose down || true && \ # Cible fixe sur LAN interne (192.168.1.208) — risque MITM inexistant.
docker compose up --build -d" ssh -i /root/.ssh/id_deploy -o StrictHostKeyChecking=no \
Elewyn@192.168.1.208 \
"cd ${{ env.DEPLOY_PATH }} && /share/CACHEDEV1_DATA/.qpkg/container-station/usr/bin/docker compose down || true && /share/CACHEDEV1_DATA/.qpkg/container-station/usr/bin/docker compose up -d"

View file

@ -1,45 +0,0 @@
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

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.env
data/
screenshots/
logs/
__pycache__/
*.pyc
*.pyo
*.db

View file

@ -3,7 +3,7 @@
## Contexte ## Contexte
Bot Discord de tracking de Personal Best (PB) pour les clan bosses du jeu Raid Shadow Legends. Bot Discord de tracking de Personal Best (PB) pour les clan bosses du jeu Raid Shadow Legends.
Communauté RTF (3 clans : RTF, RTFC, RTFR). Communauté TEA — The Ember Accord (4 clans : TEAI, TEAF, TEAC, TEACO).
## Branches ## Branches
@ -12,13 +12,19 @@ Communauté RTF (3 clans : RTF, RTFC, RTFR).
| `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) → rsync vers NAS QNAP (192.168.1.208). CI/CD via Forgejo Actions → runner `vm-runner` (192.168.1.53, Alpine) → 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` (clé privée ed25519 pour SSH Elewyn@NAS). Secret requis dans Forgejo : `NAS_SSH_KEY`**doit être encodé en base64** :
```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)
@ -50,14 +56,28 @@ 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 ## Pièges connus — CI/CD
- Le `.env` ne doit jamais être commité — il contient le token Discord - **NAS_SSH_KEY doit être en base64**`echo "..." > fichier` dans Alpine sh casse les sauts de ligne de la clé brute
- `data/`, `screenshots/`, `logs/` sont gitignorés — données persistantes sur le NAS - **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`
- Le rsync exclut ces dossiers pour ne pas écraser les données en prod - **Chemin Docker sur QNAP** : `/share/CACHEDEV1_DATA/.qpkg/container-station/usr/bin/docker``docker` n'est pas dans le `$PATH` SSH
- **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,8 +107,6 @@ 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: .
@ -116,14 +114,12 @@ services:
env_file: env_file:
- .env - .env
volumes: volumes:
- ./:/app
- ./screenshots:/app/screenshots - ./screenshots:/app/screenshots
- ./bot_data.db:/app/bot_data.db - ./data:/app/data
- ./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:
@ -163,36 +159,31 @@ 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 sur QNAP ## 🚀 Déploiement
### Via Container Station (Recommandé) Le déploiement est automatisé via Forgejo Actions.
1. Install Container Station via App Center Un push sur `main` déploie en production, un push sur `dev` déploie en développement.
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
### Via SSH (Alternative) ### Prérequis (une seule fois)
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
# Connect to QNAP ssh Elewyn@192.168.1.208
ssh admin@YOUR_QNAP_IP cd /share/CACHEDEV1_DATA/discord-bot-prod # ou discord-bot-dev
/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

162
bot.py
View file

@ -1,81 +1,81 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import discord import discord
import os import os
import sys import sys
from discord.ext import commands from discord.ext import commands
from config import DISCORD_TOKEN from config import DISCORD_TOKEN
from utils.DatabaseManager_class import DatabaseManager from utils.DatabaseManager_class import DatabaseManager
from utils.ScreenshotManager_class import ScreenshotManager from utils.ScreenshotManager_class import ScreenshotManager
from utils.MercyManager_class import MercyManager from utils.MercyManager_class import MercyManager
from utils.pb_handler import set_managers from utils.pb_handler import set_managers
from utils.leaderboard_handler import set_db_manager from utils.leaderboard_handler import set_db_manager
# Force UTF-8 # Force UTF-8
os.environ["PYTHONIOENCODING"] = "utf-8" os.environ["PYTHONIOENCODING"] = "utf-8"
sys.stdout.reconfigure(encoding='utf-8') sys.stdout.reconfigure(encoding='utf-8')
# Définir les intents # Define intents
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
# Initialisation des managers # Initialize managers
db_manager = DatabaseManager() db_manager = DatabaseManager()
screenshot_manager = ScreenshotManager() screenshot_manager = ScreenshotManager()
mercy_manager = MercyManager() mercy_manager = MercyManager()
# Injection des managers dans les handlers # Inject managers into 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
# Liste des cogs # Cog list
initial_cogs = [ initial_cogs = [
"cogs.guide", "cogs.guide",
"cogs.pbhydra", "cogs.pbhydra",
"cogs.pbchimera", "cogs.pbchimera",
"cogs.pbcvc", "cogs.pbcvc",
"cogs.top10", "cogs.top10",
"cogs.mystats", "cogs.mystats",
"cogs.mercy", "cogs.mercy",
] ]
# Liste des dossiers # Directory list
folders = [ folders = [
"screenshots/hydra/normal", "screenshots/hydra/normal",
"screenshots/hydra/hard", "screenshots/hydra/hard",
"screenshots/hydra/brutal", "screenshots/hydra/brutal",
"screenshots/hydra/nightmare", "screenshots/hydra/nightmare",
"screenshots/chimera/easy", "screenshots/chimera/easy",
"screenshots/chimera/normal", "screenshots/chimera/normal",
"screenshots/chimera/hard", "screenshots/chimera/hard",
"screenshots/chimera/brutal", "screenshots/chimera/brutal",
"screenshots/chimera/nightmare", "screenshots/chimera/nightmare",
"screenshots/chimera/ultra", "screenshots/chimera/ultra",
"screenshots/cvc", "screenshots/cvc",
] ]
# Création des dossiers si nécessaire (exist_ok=True évite d'écraser) # Create directories if needed (exist_ok=True avoids overwriting)
for f in folders: for f in folders:
os.makedirs(f, exist_ok=True) os.makedirs(f, exist_ok=True)
class MyBot(commands.Bot): class MyBot(commands.Bot):
def __init__(self): def __init__(self):
super().__init__(command_prefix="!", intents=intents) super().__init__(command_prefix="!", intents=intents)
self.db_manager = db_manager self.db_manager = db_manager
self.screenshot_manager = screenshot_manager self.screenshot_manager = screenshot_manager
self.mercy_manager = mercy_manager self.mercy_manager = mercy_manager
async def setup_hook(self): async def setup_hook(self):
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} chargé") print(f"[OK] Cog {cog} loaded")
except Exception as e: except Exception as e:
print(f"[ERREUR] Impossible de charger {cog}: {e}") print(f"[ERROR] Failed to load {cog}: {e}")
async def on_ready(self): async def on_ready(self):
print(f"{self.user.name} est connecté !") print(f"{self.user.name} connected!")
bot = MyBot() bot = MyBot()
bot.run(DISCORD_TOKEN) bot.run(DISCORD_TOKEN)

View file

@ -1,118 +1,119 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import discord import discord
from discord.ext import commands 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):
"""Affiche la liste des commandes disponibles""" """Shows the list of available commands"""
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):
"""Affiche toutes les commandes disponibles avec les difficultés""" """Shows all available commands with difficulties"""
if ctx.channel.id != AUTHORIZED_CHANNEL_ID: if ctx.channel.id != AUTHORIZED_CHANNEL_ID:
return return
embed = discord.Embed( embed = discord.Embed(
title="🧐 RTF Bot - Commands Guide", title="🧐 TEA Bot - Commands Guide",
description="Here are all available commands for tracking your Personal Bests!", description="Here are all available commands for tracking your Personal Bests!",
color=0x00bfff color=0x00bfff
) )
# Info sur les formats de dégâts # Damage format info
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"
"**Suffixes:** K = thousands, M = millions, B = billions\n" "**Suffixes:** K = thousands, M = millions, B = billions\n"
"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare", "**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare",
inline=False inline=False
) )
# Commandes PB Hydra # Hydra PB commands
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"
"`!pbhydra <difficulty> <damage>` - Submit PB + screenshot\n" "`!pbhydra <difficulty> <damage>` - Submit PB + screenshot\n"
"`!pbhydra <difficulty>` - Show your PB\n" "`!pbhydra <difficulty>` - Show your PB\n"
"`!pbhydra <difficulty> <user>` - Show user's PB", "`!pbhydra <difficulty> <user>` - Show user's PB",
inline=False inline=False
) )
# Commandes PB Chimera # Chimera PB commands
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"
"`!pbchimera <difficulty> <damage>` - Submit PB + screenshot\n" "`!pbchimera <difficulty> <damage>` - Submit PB + screenshot\n"
"`!pbchimera <difficulty>` - Show your PB\n" "`!pbchimera <difficulty>` - Show your PB\n"
"`!pbchimera <difficulty> <user>` - Show user's PB", "`!pbchimera <difficulty> <user>` - Show user's PB",
inline=False inline=False
) )
# Commandes PB CvC # CvC PB commands
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"
"`!pbcvc` - Show your PB\n" "`!pbcvc` - Show your PB\n"
"`!pbcvc <username>` - Show user's PB", "`!pbcvc <username>` - Show user's PB",
inline=False inline=False
) )
# Commandes Mercy # Mercy commands
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"
"`!mercy add <nb> <type>` - Add pulls to a shard type\n" "`!mercy add <nb> <type>` - Add pulls to a shard type\n"
"`!mercy reset <type>` - Reset pulls for a shard type\n" "`!mercy reset <type>` - Reset pulls for a shard type\n"
"**Available types:** ancient, void, sacred, primal, remnant", "**Available types:** ancient, void, sacred, primal, remnant",
inline=False inline=False
) )
# Classements globaux # Global leaderboards
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"
"`!top10chimera <difficulty>` - Global Chimera rankings\n" "`!top10chimera <difficulty>` - Global Chimera rankings\n"
"`!top10cvc` - Global CvC rankings", "`!top10cvc` - Global CvC rankings",
inline=False inline=False
) )
# Classements par clan # Clan leaderboards
embed.add_field( embed.add_field(
name="🏆 Clan Leaderboards", name="🏆 Clan Leaderboards",
value="**RTF:** `!rtfhydra <diff>` `!rtfchimera <diff>` `!rtfcvc`\n" value="**🔥 TEAI (Inferno):** `!teaihydra <diff>` `!teaichimera <diff>` `!teaicvc`\n"
"**RTFC:** `!rtfchydra <diff>` `!rtfcchimera <diff>` `!rtfccvc`\n" "**🛡️ TEAF (Flame):** `!teafhydra <diff>` `!teafchimera <diff>` `!teafcvc`\n"
"**RTFR:** `!rtfrhydra <diff>` `!rtfrchimera <diff>` `!rtfrcvc`", "**⚔️ TEAC (Cinder):** `!teachydra <diff>` `!teachimera <diff>` `!teaccvc`\n"
inline=False "**👑 TEACO (Corrupted Olympians):** `!teacohydra <diff>` `!teacochimera <diff>` `!teacocvc`",
) inline=False
)
# Stats et aide
embed.add_field( # Stats and help
name="📈 Stats & Info", embed.add_field(
value="`!mystats` - View all your PBs\n" name="📈 Stats & Info",
"`!mystats <username>` - View someone's PBs\n" value="`!mystats` - View all your PBs\n"
"`!guide` - Show this help message", "`!mystats <username>` - View someone's PBs\n"
inline=False "`!guide` - Show this help message",
) inline=False
)
# Instructions
embed.add_field( # Examples
name="⚡ Examples", embed.add_field(
value="`!pbhydra brutal 1.5M` - Submit Brutal Hydra PB\n" name="⚡ Examples",
"`!pbchimera unm 500K` - Submit Ultra Nightmare PB\n" value="`!pbhydra brutal 1.5M` - Submit Brutal Hydra PB\n"
"`!pbcvc 2.3M` - Submit CvC PB\n" "`!pbchimera unm 500K` - Submit Ultra Nightmare PB\n"
"`!mercy add 50 primal` - Add 50 pulls to Primal shard\n" "`!pbcvc 2.3M` - Submit CvC PB\n"
"`!mercy show` - Show your mercy pulls\n" "`!mercy add 50 primal` - Add 50 pulls to Primal shard\n"
"`!rtfhydra nm` - RTF clan Nightmare rankings\n" "`!mercy show` - Show your mercy pulls\n"
"**Always attach screenshot when submitting PBs!**", "`!teaihydra nm` - TEAI clan Nightmare rankings\n"
inline=False "**Always attach screenshot when submitting PBs!**",
) inline=False
)
embed.set_footer(text="🎮 Old screenshots are automatically deleted when you set new PBs!")
embed.set_footer(text="🎮 Old screenshots are automatically deleted when you set new PBs!")
await ctx.send(embed=embed)
await ctx.send(embed=embed)
async def setup(bot):
await bot.add_cog(Guide(bot)) async def setup(bot):
await bot.add_cog(Guide(bot))

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 pour gérer les pulls de Mercy""" """Cog for managing Mercy pulls"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot

View file

@ -1,86 +1,98 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import discord 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 # Assurez-vous que db_manager est initialisé correctement from utils.pb_handler import db_manager # Make sure db_manager is initialized correctly
class MyStats(commands.Cog): class MyStats(commands.Cog):
"""Cog pour afficher tous les PB d'un utilisateur""" """Cog for displaying all PBs for a user"""
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):
"""Affiche tous les PB d'un utilisateur avec les nouvelles difficultés""" """Shows all PBs for a user across all difficulties"""
if ctx.channel.id != AUTHORIZED_CHANNEL_ID: if ctx.channel.id != AUTHORIZED_CHANNEL_ID:
return return
try: try:
username = target_user if target_user else ctx.author.id if target_user:
user_data = db_manager.get_user_all_pbs(username) matches = db_manager.find_user_by_name(target_user)
if not matches:
if not user_data: await ctx.send(f"❌ No data found for **{target_user}**.")
await ctx.send(f"❌ No data found for **{ctx.author.display_name}**.") return
return if len(matches) > 1:
await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.")
embed = discord.Embed( return
title=f"📊 {ctx.author.display_name}'s Complete Stats", user_id, display_name = matches[0]
color=0x00bfff else:
) user_id = ctx.author.id
display_name = ctx.author.display_name
# Hydra - toutes les difficultés
hydra_stats = [] user_data = db_manager.get_user_all_pbs(user_id)
for difficulty in BOSS_CONFIG['hydra']['difficulties']:
pb_key = f'pb_hydra_{difficulty}' if not user_data:
date_key = f'pb_hydra_{difficulty}_date' await ctx.send(f"❌ No data found for **{display_name}**.")
return
if pb_key in user_data and user_data[pb_key] > 0:
pb_value = user_data[pb_key] embed = discord.Embed(
pb_date = user_data.get(date_key) title=f"📊 {display_name}'s Complete Stats",
date_text = f"{format_date_only(pb_date)}" if pb_date else "" color=0x00bfff
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" # Hydra - all difficulties
embed.add_field(name="⚔️ Hydra PBs", value=hydra_text, inline=False) hydra_stats = []
for difficulty in BOSS_CONFIG['hydra']['difficulties']:
# Chimera - toutes les difficultés pb_key = f'pb_hydra_{difficulty}'
chimera_stats = [] date_key = f'pb_hydra_{difficulty}_date'
for difficulty in BOSS_CONFIG['chimera']['difficulties']:
pb_key = f'pb_chimera_{difficulty}' if pb_key in user_data and user_data[pb_key] > 0:
date_key = f'pb_chimera_{difficulty}_date' pb_value = user_data[pb_key]
pb_date = user_data.get(date_key)
if pb_key in user_data and user_data[pb_key] > 0: date_text = f"{format_date_only(pb_date)}" if pb_date else ""
pb_value = user_data[pb_key] hydra_stats.append(f"**{difficulty.title()}:** {format_damage_display(pb_value)}{date_text}")
pb_date = user_data.get(date_key)
date_text = f"{format_date_only(pb_date)}" if pb_date else "" hydra_text = "\n".join(hydra_stats) if hydra_stats else "No records"
display_name = "Ultra Nightmare" if difficulty == "ultra" else difficulty.title() embed.add_field(name="⚔️ Hydra PBs", value=hydra_text, inline=False)
chimera_stats.append(f"**{display_name}:** {format_damage_display(pb_value)}{date_text}")
# Chimera - all difficulties
chimera_text = "\n".join(chimera_stats) if chimera_stats else "No records" chimera_stats = []
embed.add_field(name="🛡️ Chimera PBs", value=chimera_text, inline=False) for difficulty in BOSS_CONFIG['chimera']['difficulties']:
pb_key = f'pb_chimera_{difficulty}'
# CvC date_key = f'pb_chimera_{difficulty}_date'
cvc_pb = user_data.get('pb_cvc', 0)
cvc_date = user_data.get('pb_cvc_date') if pb_key in user_data and user_data[pb_key] > 0:
cvc_text = f"**{format_damage_display(cvc_pb)} damage**" if cvc_pb > 0 else "No record" pb_value = user_data[pb_key]
if cvc_pb > 0 and cvc_date: pb_date = user_data.get(date_key)
formatted_date = format_date_only(cvc_date) date_text = f"{format_date_only(pb_date)}" if pb_date else ""
if formatted_date: display_name = "Ultra Nightmare" if difficulty == "ultra" else difficulty.title()
cvc_text += f"{formatted_date}" chimera_stats.append(f"**{display_name}:** {format_damage_display(pb_value)}{date_text}")
embed.add_field(name="🗡️ CvC PB", value=cvc_text, inline=False)
chimera_text = "\n".join(chimera_stats) if chimera_stats else "No records"
# Total combiné embed.add_field(name="🛡️ Chimera PBs", value=chimera_text, inline=False)
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']) # CvC
total_damage += user_data.get('pb_cvc', 0) cvc_pb = user_data.get('pb_cvc', 0)
embed.add_field(name="💯 Total Combined Damage", value=f"**{format_damage_display(total_damage)}**", inline=False) cvc_date = user_data.get('pb_cvc_date')
cvc_text = f"**{format_damage_display(cvc_pb)} damage**" if cvc_pb > 0 else "No record"
await ctx.send(embed=embed) if cvc_pb > 0 and cvc_date:
formatted_date = format_date_only(cvc_date)
except Exception as e: if formatted_date:
await ctx.send(f"❌ Error: {e}") cvc_text += f"{formatted_date}"
embed.add_field(name="🗡️ CvC PB", value=cvc_text, inline=False)
async def setup(bot):
await bot.add_cog(MyStats(bot)) # Combined total
total_damage = sum(user_data.get(f'pb_hydra_{d}', 0) for d in BOSS_CONFIG['hydra']['difficulties'])
total_damage += sum(user_data.get(f'pb_chimera_{d}', 0) for d in BOSS_CONFIG['chimera']['difficulties'])
total_damage += user_data.get('pb_cvc', 0)
embed.add_field(name="💯 Total Combined Damage", value=f"**{format_damage_display(total_damage)}**", inline=False)
await ctx.send(embed=embed)
except Exception as e:
await ctx.send(f"❌ Error: {e}")
async def setup(bot):
await bot.add_cog(MyStats(bot))

View file

@ -1,18 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import discord import discord
from discord.ext import commands 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 pour gérer les Personal Bests Chimera""" """Cog for managing Chimera Personal Bests"""
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):
"""Commande !pbchimera avec gestion des difficultés""" """!pbchimera command with difficulty handling"""
await handle_pb_command(ctx, 'chimera', arg1, arg2) await handle_pb_command(ctx, 'chimera', arg1, arg2)
async def setup(bot): async def setup(bot):
await bot.add_cog(Pbchimera(bot)) await bot.add_cog(Pbchimera(bot))

View file

@ -1,18 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import discord import discord
from discord.ext import commands 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 pour gérer les Personal Bests CvC (sans difficultés)""" """Cog for managing CvC Personal Bests (no difficulties)"""
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):
"""Commande !pbcvc""" """!pbcvc command"""
await handle_pb_command(ctx, 'cvc', target_user) await handle_pb_command(ctx, 'cvc', target_user)
async def setup(bot): async def setup(bot):
await bot.add_cog(Pbcvc(bot)) await bot.add_cog(Pbcvc(bot))

View file

@ -1,18 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import discord import discord
from discord.ext import commands 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 pour gérer les Personal Bests Hydra""" """Cog for managing Hydra Personal Bests"""
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):
"""Commande !pbhydra avec gestion des difficultés""" """!pbhydra command with difficulty handling"""
await handle_pb_command(ctx, 'hydra', arg1, arg2) await handle_pb_command(ctx, 'hydra', arg1, arg2)
async def setup(bot): async def setup(bot):
await bot.add_cog(Pbhydra(bot)) await bot.add_cog(Pbhydra(bot))

View file

@ -1,90 +1,103 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import discord import discord
from discord.ext import commands from discord.ext import commands
from utils.leaderboard_handler import show_leaderboard from utils.leaderboard_handler import show_leaderboard
from utils.helpers import normalize_difficulty 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 regroupant toutes les commandes de leaderboard globales et par clan""" """Cog grouping all global and per-clan leaderboard commands"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
# --- Commandes globales --- # --- Global commands ---
@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']:
await show_leaderboard(ctx, 'hydra', difficulty) await show_leaderboard(ctx, 'hydra', difficulty)
else: else:
difficulties = " | ".join(BOSS_CONFIG['hydra']['difficulties']) difficulties = " | ".join(BOSS_CONFIG['hydra']['difficulties'])
await ctx.send(f"❌ Please specify difficulty: `!top10hydra <difficulty>`\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare") await ctx.send(f"❌ Please specify difficulty: `!top10hydra <difficulty>`\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare")
@commands.command() @commands.command()
async def top10chimera(self, ctx, difficulty: str = None): async def top10chimera(self, ctx, difficulty: str = None):
if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['chimera']['difficulties']: if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG['chimera']['difficulties']:
await show_leaderboard(ctx, 'chimera', difficulty) await show_leaderboard(ctx, 'chimera', difficulty)
else: else:
difficulties = " | ".join(BOSS_CONFIG['chimera']['difficulties']) difficulties = " | ".join(BOSS_CONFIG['chimera']['difficulties'])
await ctx.send(f"❌ Please specify difficulty: `!top10chimera <difficulty>`\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra") await ctx.send(f"❌ Please specify difficulty: `!top10chimera <difficulty>`\n**Available:** {difficulties}\n**Shortcuts:** `nm` = Nightmare, `unm` = Ultra")
@commands.command() @commands.command()
async def top10cvc(self, ctx): async def top10cvc(self, ctx):
await show_leaderboard(ctx, 'cvc') await show_leaderboard(ctx, 'cvc')
# --- Commandes par clan RTF --- # --- TEAI clan commands (Inferno) ---
@commands.command() @commands.command()
async def rtfhydra(self, ctx, difficulty: str = None): async def teaihydra(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTF') await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAI')
@commands.command() @commands.command()
async def rtfchimera(self, ctx, difficulty: str = None): async def teaichimera(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTF') await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAI')
@commands.command() @commands.command()
async def rtfcvc(self, ctx): async def teaicvc(self, ctx):
await show_leaderboard(ctx, 'cvc', clan='RTF') await show_leaderboard(ctx, 'cvc', clan='TEAI')
# --- Commandes par clan RTFC --- # --- TEAF clan commands (Flame) ---
@commands.command() @commands.command()
async def rtfchydra(self, ctx, difficulty: str = None): async def teafhydra(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTFC') await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAF')
@commands.command() @commands.command()
async def rtfcchimera(self, ctx, difficulty: str = None): async def teafchimera(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTFC') await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAF')
@commands.command() @commands.command()
async def rtfccvc(self, ctx): async def teafcvc(self, ctx):
await show_leaderboard(ctx, 'cvc', clan='RTFC') await show_leaderboard(ctx, 'cvc', clan='TEAF')
# --- Commandes par clan RTFR --- # --- TEAC clan commands (Cinder) ---
@commands.command() @commands.command()
async def rtfrhydra(self, ctx, difficulty: str = None): async def teachydra(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'RTFR') await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEAC')
@commands.command() @commands.command()
async def rtfrchimera(self, ctx, difficulty: str = None): async def teachimera(self, ctx, difficulty: str = None):
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'RTFR') await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAC')
@commands.command() @commands.command()
async def rtfrcvc(self, ctx): async def teaccvc(self, ctx):
await show_leaderboard(ctx, 'cvc', clan='RTFR') await show_leaderboard(ctx, 'cvc', clan='TEAC')
# --- Méthode interne pour éviter la répétition --- # --- TEACO clan commands (Corrupted Olympians) ---
async def _show_clan_leaderboard(self, ctx, boss_type, difficulty, clan): @commands.command()
"""Affiche le leaderboard pour un boss et un clan spécifique""" async def teacohydra(self, ctx, difficulty: str = None):
if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG[boss_type]['difficulties']: await self._show_clan_leaderboard(ctx, 'hydra', difficulty, 'TEACO')
await show_leaderboard(ctx, boss_type, difficulty, clan)
elif boss_type != 'cvc': # CvC na pas de difficultés @commands.command()
difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties']) async def teacochimera(self, ctx, difficulty: str = None):
await ctx.send( await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEACO')
f"❌ Please specify difficulty: `!{ctx.command.name} <difficulty>`\n"
f"**Available:** {difficulties}\n" @commands.command()
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra" async def teacocvc(self, ctx):
) await show_leaderboard(ctx, 'cvc', clan='TEACO')
else:
await show_leaderboard(ctx, boss_type, clan=clan) # --- Internal helper method ---
async def _show_clan_leaderboard(self, ctx, boss_type, difficulty, clan):
async def setup(bot): """Shows the leaderboard for a specific boss and clan"""
await bot.add_cog(Top10(bot)) if difficulty and normalize_difficulty(difficulty) in BOSS_CONFIG[boss_type]['difficulties']:
await show_leaderboard(ctx, boss_type, difficulty, clan)
elif boss_type != 'cvc': # CvC has no difficulties
difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties'])
await ctx.send(
f"❌ Please specify difficulty: `!{ctx.command.name} <difficulty>`\n"
f"**Available:** {difficulties}\n"
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra"
)
else:
await show_leaderboard(ctx, boss_type, clan=clan)
async def setup(bot):
await bot.add_cog(Top10(bot))

View file

@ -1,35 +1,51 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
# Token et channel autorisé # Token and authorized channel
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"))
# Chemins # Paths
SCREENSHOTS_BASE_PATH = "/app/screenshots" SCREENSHOTS_BASE_PATH = "/app/screenshots"
DATABASE_PATH = "/app/data/bot_data.db" DATABASE_PATH = "/app/data/bot_data.db"
# Configuration des clans # TEA clan configuration - The Ember Accord
CLAN_CONFIG = { CLAN_CONFIG = {
'RTF': {'name': 'RTF', 'emoji': '🛡️', 'color': 0x00ff00}, 'TEAI': {'name': 'TEAI', 'full_name': 'Inferno', 'emoji': '🔥', 'color': 0xff4500},
'RTFC': {'name': 'RTFC', 'emoji': '🔥', 'color': 0xff4500}, 'TEAF': {'name': 'TEAF', 'full_name': 'Flame', 'emoji': '🛡️', 'color': 0x00ff00},
'RTFR': {'name': 'RTFR', 'emoji': '⚔️', 'color': 0x1e90ff} 'TEAC': {'name': 'TEAC', 'full_name': 'Cinder', 'emoji': '⚔️', 'color': 0x1e90ff},
} 'TEACO': {'name': 'TEACO', 'full_name': 'Corrupted Olympians', 'emoji': '👑', 'color': 0x9932cc},
}
# Configuration des boss avec difficultés
BOSS_CONFIG = { # Discord role ID to clan key mapping
'hydra': {'name': 'Hydra', 'emoji': '📍', 'color': 0xff6b35, CLAN_ROLE_IDS = {
'difficulties': ['normal', 'hard', 'brutal', 'nightmare']}, 1190674529731747901: 'TEAI',
'chimera': {'name': 'Chimera', 'emoji': '🦁', 'color': 0x9932cc, 1197646966599983185: 'TEAF',
'difficulties': ['easy', 'normal', 'hard', 'brutal', 'nightmare', 'ultra']}, 1220014404809261076: 'TEAC',
'cvc': {'name': 'Clan vs Clan', 'emoji': '✔️', 'color': 0xff0000, 'difficulties': []} 1496965820868198550: 'TEACO',
} }
# Mappings pour diminutifs de difficultés # Old clan to new clan mapping (existing database migration)
DIFFICULTY_SHORTCUTS = { CLAN_MIGRATION = {
'nm': 'nightmare', 'RTF': 'TEAI',
'unm': 'ultra' 'RTFC': 'TEAF',
} 'RTFR': 'TEAC',
}
# Boss configuration with difficulties
BOSS_CONFIG = {
'hydra': {'name': 'Hydra', 'emoji': '📍', 'color': 0xff6b35,
'difficulties': ['normal', 'hard', 'brutal', 'nightmare']},
'chimera': {'name': 'Chimera', 'emoji': '🦁', 'color': 0x9932cc,
'difficulties': ['easy', 'normal', 'hard', 'brutal', 'nightmare', 'ultra']},
'cvc': {'name': 'Clan vs Clan', 'emoji': '✔️', 'color': 0xff0000, 'difficulties': []}
}
# Difficulty shortcut mappings
DIFFICULTY_SHORTCUTS = {
'nm': 'nightmare',
'unm': 'ultra'
}

View file

@ -1,5 +1,3 @@
version: '3.8'
services: services:
discord-bot: discord-bot:
build: . build: .
@ -15,7 +13,6 @@ services:
- ./logs:/app/logs - ./logs:/app/logs
environment: environment:
- TZ=Europe/Paris - TZ=Europe/Paris
container_name: rtf-discord-bot
deploy: deploy:
resources: resources:
limits: limits:

View file

@ -1,203 +1,213 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from config import DATABASE_PATH from config import DATABASE_PATH, CLAN_MIGRATION
import sqlite3, os import sqlite3, os
class DatabaseManager: class DatabaseManager:
def __init__(self, db_path=DATABASE_PATH): def __init__(self, db_path=DATABASE_PATH):
self.db_path = db_path self.db_path = db_path
self.init_database() self.init_database()
def init_database(self): def init_database(self):
"""Initialise la base de données avec les nouvelles colonnes pour les difficultés""" """Initializes the database with all columns for difficulties"""
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()
# Table principale avec toutes les difficultés # Main table with all difficulties
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,
discord_id TEXT UNIQUE, discord_id TEXT UNIQUE,
discord_username TEXT, discord_username TEXT,
-- Hydra difficulties -- Hydra difficulties
pb_hydra_normal INTEGER DEFAULT 0, pb_hydra_normal INTEGER DEFAULT 0,
pb_hydra_normal_screenshot TEXT, pb_hydra_normal_screenshot TEXT,
pb_hydra_normal_date TIMESTAMP, pb_hydra_normal_date TIMESTAMP,
pb_hydra_hard INTEGER DEFAULT 0, pb_hydra_hard INTEGER DEFAULT 0,
pb_hydra_hard_screenshot TEXT, pb_hydra_hard_screenshot TEXT,
pb_hydra_hard_date TIMESTAMP, pb_hydra_hard_date TIMESTAMP,
pb_hydra_brutal INTEGER DEFAULT 0, pb_hydra_brutal INTEGER DEFAULT 0,
pb_hydra_brutal_screenshot TEXT, pb_hydra_brutal_screenshot TEXT,
pb_hydra_brutal_date TIMESTAMP, pb_hydra_brutal_date TIMESTAMP,
pb_hydra_nightmare INTEGER DEFAULT 0, pb_hydra_nightmare INTEGER DEFAULT 0,
pb_hydra_nightmare_screenshot TEXT, pb_hydra_nightmare_screenshot TEXT,
pb_hydra_nightmare_date TIMESTAMP, pb_hydra_nightmare_date TIMESTAMP,
-- Chimera difficulties -- Chimera difficulties
pb_chimera_easy INTEGER DEFAULT 0, pb_chimera_easy INTEGER DEFAULT 0,
pb_chimera_easy_screenshot TEXT, pb_chimera_easy_screenshot TEXT,
pb_chimera_easy_date TIMESTAMP, pb_chimera_easy_date TIMESTAMP,
pb_chimera_normal INTEGER DEFAULT 0, pb_chimera_normal INTEGER DEFAULT 0,
pb_chimera_normal_screenshot TEXT, pb_chimera_normal_screenshot TEXT,
pb_chimera_normal_date TIMESTAMP, pb_chimera_normal_date TIMESTAMP,
pb_chimera_hard INTEGER DEFAULT 0, pb_chimera_hard INTEGER DEFAULT 0,
pb_chimera_hard_screenshot TEXT, pb_chimera_hard_screenshot TEXT,
pb_chimera_hard_date TIMESTAMP, pb_chimera_hard_date TIMESTAMP,
pb_chimera_brutal INTEGER DEFAULT 0, pb_chimera_brutal INTEGER DEFAULT 0,
pb_chimera_brutal_screenshot TEXT, pb_chimera_brutal_screenshot TEXT,
pb_chimera_brutal_date TIMESTAMP, pb_chimera_brutal_date TIMESTAMP,
pb_chimera_nightmare INTEGER DEFAULT 0, pb_chimera_nightmare INTEGER DEFAULT 0,
pb_chimera_nightmare_screenshot TEXT, pb_chimera_nightmare_screenshot TEXT,
pb_chimera_nightmare_date TIMESTAMP, pb_chimera_nightmare_date TIMESTAMP,
pb_chimera_ultra INTEGER DEFAULT 0, pb_chimera_ultra INTEGER DEFAULT 0,
pb_chimera_ultra_screenshot TEXT, pb_chimera_ultra_screenshot TEXT,
pb_chimera_ultra_date TIMESTAMP, pb_chimera_ultra_date TIMESTAMP,
-- CvC (inchangé) -- CvC (unchanged)
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,
total_attempts INTEGER DEFAULT 0, total_attempts INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
''') ''')
# Migration des données existantes (si nécessaire) # Existing data migration (if needed)
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()]
if 'discord_id' not in columns: if 'discord_id' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN discord_id TEXT') 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:
# Table pour l'historique global cursor.execute('ALTER TABLE users ADD COLUMN clan TEXT')
cursor.execute(''' # Auto-migration: derive clan from old username prefix
CREATE TABLE IF NOT EXISTS pb_history ( for old_tag, new_clan in CLAN_MIGRATION.items():
id INTEGER PRIMARY KEY AUTOINCREMENT, cursor.execute(
discord_id TEXT, "UPDATE users SET clan = ? WHERE clan IS NULL AND ("
username TEXT, "discord_username LIKE ? OR discord_username LIKE ?)",
boss_type TEXT, (new_clan, f'[{old_tag}] %', f'[{old_tag}]%')
difficulty TEXT, )
damage INTEGER,
screenshot_filename TEXT, # Global history table
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP cursor.execute('''
) CREATE TABLE IF NOT EXISTS pb_history (
''') id INTEGER PRIMARY KEY AUTOINCREMENT,
discord_id TEXT,
conn.commit() username TEXT,
conn.close() boss_type TEXT,
difficulty TEXT,
def get_user_pb(self, user_id, boss_type, difficulty=None): damage INTEGER,
"""Récupère le PB d'un utilisateur pour un boss et difficulté spécifique""" screenshot_filename TEXT,
conn = sqlite3.connect(self.db_path) date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
cursor = conn.cursor() )
''')
if difficulty:
column_prefix = f"pb_{boss_type}_{difficulty}" conn.commit()
else: conn.close()
column_prefix = f"pb_{boss_type}"
def get_user_pb(self, user_id, boss_type, difficulty=None):
cursor.execute( """Returns PB for a user on a specific boss and difficulty"""
f"SELECT {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date FROM users WHERE discord_id = ?", conn = sqlite3.connect(self.db_path)
(str(user_id),) cursor = conn.cursor()
)
result = cursor.fetchone() if difficulty:
conn.close() column_prefix = f"pb_{boss_type}_{difficulty}"
else:
return result if result else (0, None, None) column_prefix = f"pb_{boss_type}"
def update_user_pb(self, user_id, username, boss_type, damage, screenshot_filename, difficulty=None): cursor.execute(
"""Met à jour le PB d'un utilisateur et supprime l'ancien screenshot""" f"SELECT {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date FROM users WHERE discord_id = ?",
conn = sqlite3.connect(self.db_path) (str(user_id),)
cursor = conn.cursor() )
result = cursor.fetchone()
# Récupérer l'ancien screenshot pour le supprimer conn.close()
old_data = self.get_user_pb(user_id, boss_type, difficulty)
old_screenshot = old_data[1] if old_data else None return result if result else (0, None, None)
if difficulty: def update_user_pb(self, user_id, username, boss_type, damage, screenshot_filename, difficulty=None, clan=None):
column_prefix = f"pb_{boss_type}_{difficulty}" """Updates a user's PB and deletes the previous screenshot"""
else: conn = sqlite3.connect(self.db_path)
column_prefix = f"pb_{boss_type}" cursor = conn.cursor()
# Créer l'utilisateur s'il n'existe pas, sinon mettre à jour # Get old screenshot for deletion
cursor.execute(f''' old_data = self.get_user_pb(user_id, boss_type, difficulty)
INSERT INTO users (discord_id, discord_username, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts) old_screenshot = old_data[1] if old_data else None
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, 1)
ON CONFLICT(discord_id) if difficulty:
DO UPDATE SET column_prefix = f"pb_{boss_type}_{difficulty}"
discord_username = ?, else:
{column_prefix} = ?, column_prefix = f"pb_{boss_type}"
{column_prefix}_screenshot = ?,
{column_prefix}_date = CURRENT_TIMESTAMP, # COALESCE(?, clan): keeps existing clan if detection returns None
total_attempts = total_attempts + 1 cursor.execute(f'''
''', (str(user_id), username, damage, screenshot_filename, username, damage, screenshot_filename)) INSERT INTO users (discord_id, discord_username, clan, {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date, total_attempts)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, 1)
# Ajouter à l'historique ON CONFLICT(discord_id)
cursor.execute(''' DO UPDATE SET
INSERT INTO pb_history (discord_id, username, boss_type, difficulty, damage, screenshot_filename) discord_username = ?,
VALUES (?, ?, ?, ?, ?, ?) clan = COALESCE(?, clan),
''', (str(user_id), username, boss_type, difficulty or 'none', damage, screenshot_filename)) {column_prefix} = ?,
{column_prefix}_screenshot = ?,
conn.commit() {column_prefix}_date = CURRENT_TIMESTAMP,
conn.close() total_attempts = total_attempts + 1
''', (str(user_id), username, clan, damage, screenshot_filename, username, clan, damage, screenshot_filename))
return old_screenshot
# Add to history
def get_leaderboard(self, boss_type, difficulty=None, limit=10, clan=None): cursor.execute('''
"""Récupère le classement pour un boss et difficulté spécifique""" INSERT INTO pb_history (discord_id, username, boss_type, difficulty, damage, screenshot_filename)
conn = sqlite3.connect(self.db_path) VALUES (?, ?, ?, ?, ?, ?)
cursor = conn.cursor() ''', (str(user_id), username, boss_type, difficulty or 'none', damage, screenshot_filename))
if difficulty: conn.commit()
column_prefix = f"pb_{boss_type}_{difficulty}" conn.close()
else:
column_prefix = f"pb_{boss_type}" return old_screenshot
base_query = f''' def get_leaderboard(self, boss_type, difficulty=None, limit=10, clan=None):
SELECT discord_username, {column_prefix}, {column_prefix}_date """Returns the leaderboard for a specific boss and difficulty"""
FROM users conn = sqlite3.connect(self.db_path)
WHERE {column_prefix} > 0 cursor = conn.cursor()
'''
if difficulty:
if clan: column_prefix = f"pb_{boss_type}_{difficulty}"
base_query += ''' AND ( else:
discord_username LIKE '[''' + clan + '''] %' OR column_prefix = f"pb_{boss_type}"
discord_username LIKE '[''' + clan + ''']%'
)''' base_query = f'''
SELECT discord_username, {column_prefix}, {column_prefix}_date, clan
base_query += f' ORDER BY {column_prefix} DESC LIMIT ?' FROM users
WHERE {column_prefix} > 0
cursor.execute(base_query, (limit,)) '''
results = cursor.fetchall()
conn.close() params = []
return results if clan:
base_query += ' AND clan = ?'
def get_user_all_pbs(self, user_id): params.append(clan)
"""Récupère tous les PB d'un utilisateur"""
conn = sqlite3.connect(self.db_path) base_query += f' ORDER BY {column_prefix} DESC LIMIT ?'
cursor = conn.cursor() params.append(limit)
# Récupérer toutes les colonnes de PB cursor.execute(base_query, params)
cursor.execute('SELECT * FROM users WHERE discord_id = ?', (str(user_id),)) results = cursor.fetchall()
result = cursor.fetchone() conn.close()
columns = [desc[0] for desc in cursor.description] return results
conn.close()
def get_user_all_pbs(self, user_id):
if not result: """Returns all PBs for a user"""
return None conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
return dict(zip(columns, result))
# Retrieve all PB columns
def find_user_by_name(self, username): cursor.execute('SELECT * FROM users WHERE discord_id = ?', (str(user_id),))
"""Trouve un utilisateur par son nom (pour rétrocompatibilité)""" result = cursor.fetchone()
conn = sqlite3.connect(self.db_path) columns = [desc[0] for desc in cursor.description]
cursor = conn.cursor() conn.close()
cursor.execute('SELECT discord_id, discord_username FROM users WHERE discord_username LIKE ?', (f'%{username}%',)) if not result:
results = cursor.fetchall() return None
conn.close()
return dict(zip(columns, result))
return results
def find_user_by_name(self, username):
"""Finds a user by name (for backwards compatibility)"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT discord_id, discord_username FROM users WHERE discord_username LIKE ?', (f'%{username}%',))
results = cursor.fetchall()
conn.close()
return results

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
# Règles de mercy pour stockage # Mercy rules for storage
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):
"""Initialise la table des compteurs de mercy""" """Initializes the mercy counters table"""
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):
"""Retourne le nombre de pulls actuels pour un utilisateur et un type de shard""" """Returns current pull count for a user and shard type"""
conn = sqlite3.connect(self.db_path) 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):
"""Ajoute des pulls pour un utilisateur en gérant correctement l'INSERT/UPDATE""" """Adds pulls for a user, handling INSERT/UPDATE correctly"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
cursor = conn.cursor() cursor = conn.cursor()
# Vérifie si l'enregistrement existe # Check if record exists
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):
"""Réinitialise les pulls d'un utilisateur pour un shard""" """Resets pull count for a user on a shard"""
conn = sqlite3.connect(self.db_path) 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):
"""Retourne tous les pulls d'un utilisateur pour tous les shards""" """Returns all pull counts for a user across all shards"""
conn = sqlite3.connect(self.db_path) 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):
"""Calcule la probabilité de mercy selon le nombre de pulls""" """Calculates mercy probability based on pull count"""
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):
"""Retourne combien de pulls restent avant un loot garanti""" """Returns pulls remaining until guaranteed loot"""
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

@ -1,65 +1,65 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sqlite3, os import sqlite3, os
import aiohttp import aiohttp
from datetime import datetime from datetime import datetime
from config import SCREENSHOTS_BASE_PATH, BOSS_CONFIG 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
# Créer les dossiers pour chaque boss et difficulté # Create directories for each boss and difficulty
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)
# Créer sous-dossiers pour les difficultés # Create subdirectories for difficulties
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):
"""Sauvegarde le screenshot localement""" """Saves the screenshot locally"""
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()
filename = f"{username.lower()}_{damage}_{timestamp}.{file_extension}" filename = f"{username.lower()}_{damage}_{timestamp}.{file_extension}"
if difficulty: if difficulty:
boss_path = os.path.join(self.base_path, boss_type, difficulty) boss_path = os.path.join(self.base_path, boss_type, difficulty)
else: else:
boss_path = os.path.join(self.base_path, boss_type) boss_path = os.path.join(self.base_path, boss_type)
filepath = os.path.join(boss_path, filename) filepath = os.path.join(boss_path, filename)
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:
# Ouverture en binaire, pas de problème d'encodage # Binary mode, no encoding issues
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"Erreur sauvegarde screenshot: {str(e)}") print(f"Screenshot save error: {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):
"""Retourne le chemin complet du screenshot""" """Returns the full path to the 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)
else: else:
return os.path.join(self.base_path, boss_type, filename) return os.path.join(self.base_path, boss_type, filename)
return None return None
def delete_old_screenshot(self, filename, boss_type, difficulty=None): def delete_old_screenshot(self, filename, boss_type, difficulty=None):
"""Supprime l'ancien screenshot""" """Deletes the old 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"Ancien screenshot supprimé: {filename}") print(f"Old screenshot deleted: {filename}")
except Exception as e: except Exception as e:
print(f"Erreur suppression screenshot: {str(e)}") print(f"Screenshot deletion error: {str(e)}")

View file

@ -1,117 +1,106 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
from datetime import datetime from datetime import datetime
from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS from typing import Optional
from config import AUTHORIZED_CHANNEL_ID, DIFFICULTY_SHORTCUTS, CLAN_ROLE_IDS
def parse_damage_amount(damage_str):
"""Convertit les montants avec suffixes (K, M, B) en nombres entiers""" def parse_damage_amount(damage_str):
if not damage_str: """Converts amounts with suffixes (K, M, B) to integers"""
return None if not damage_str:
damage_str = damage_str.strip().upper() return None
if damage_str.isdigit(): damage_str = damage_str.strip().upper()
return int(damage_str) if damage_str.isdigit():
match = re.match(r'^([0-9]*\.?[0-9]+)([KMB]?)$', damage_str) return int(damage_str)
if not match: match = re.match(r'^([0-9]*\.?[0-9]+)([KMB]?)$', damage_str)
return None if not match:
number_str, suffix = match.groups() return None
try: number_str, suffix = match.groups()
number = float(number_str) try:
except ValueError: number = float(number_str)
return None except ValueError:
multipliers = {'K': 1_000, 'M': 1_000_000, 'B': 1_000_000_000, '': 1} return None
return int(number * multipliers[suffix]) multipliers = {'K': 1_000, 'M': 1_000_000, 'B': 1_000_000_000, '': 1}
return int(number * multipliers[suffix])
def format_damage_display(damage):
"""Formate un montant de dégâts avec le suffixe approprié""" def format_damage_display(damage):
if damage >= 1_000_000_000: """Formats a damage amount with the appropriate suffix"""
billions = damage / 1_000_000_000 if damage >= 1_000_000_000:
return f"{int(billions)}B" if billions == int(billions) else f"{billions:.1f}B" billions = damage / 1_000_000_000
elif damage >= 1_000_000: return f"{int(billions)}B" if billions == int(billions) else f"{billions:.1f}B"
millions = damage / 1_000_000 elif damage >= 1_000_000:
return f"{int(millions)}M" if millions == int(millions) else f"{millions:.1f}M" millions = damage / 1_000_000
elif damage >= 1_000: return f"{int(millions)}M" if millions == int(millions) else f"{millions:.1f}M"
thousands = damage / 1_000 elif damage >= 1_000:
return f"{int(thousands)}K" if thousands == int(thousands) else f"{thousands:.1f}K" thousands = damage / 1_000
return str(damage) 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""" def normalize_difficulty(difficulty):
if not difficulty: """Normalizes a difficulty string, handling shortcuts"""
return None if not difficulty:
difficulty_lower = difficulty.lower() return None
if difficulty_lower in DIFFICULTY_SHORTCUTS: difficulty_lower = difficulty.lower()
return DIFFICULTY_SHORTCUTS[difficulty_lower] if difficulty_lower in DIFFICULTY_SHORTCUTS:
return difficulty_lower return DIFFICULTY_SHORTCUTS[difficulty_lower]
return difficulty_lower
def get_user_clan(username):
"""Détermine le clan d'un utilisateur basé sur son pseudo""" def get_clan_from_member(member) -> Optional[str]:
if not username: """Detects a member's clan via their Discord roles"""
return None for role in member.roles:
username_upper = username.upper() if role.id in CLAN_ROLE_IDS:
# Tags avec crochets et espace return CLAN_ROLE_IDS[role.id]
for clan_tag in ['[RTF] ', '[RTFC] ', '[RTFR] ']: return None
if username_upper.startswith(clan_tag):
return clan_tag.replace('[', '').replace(']', '').strip() def format_datetime(date_str):
# Tags avec crochets sans espace """Formats a date in AM/PM format"""
for clan_tag in ['[RTF]', '[RTFC]', '[RTFR]']: if not date_str:
if username_upper.startswith(clan_tag): return None
return clan_tag.replace('[', '').replace(']', '') try:
return None dt = datetime.fromisoformat(date_str)
return dt.strftime("%m/%d/%Y at %I:%M %p")
def get_user_clan_from_ctx(ctx): except:
"""Détermine le clan d'un utilisateur depuis le contexte Discord""" return None
return get_user_clan(ctx.author.display_name)
def format_date_only(date_str):
def format_datetime(date_str): """Formats a date without the time component"""
"""Formate une date en format AM/PM""" if not date_str:
if not date_str: return None
return None try:
try: dt = datetime.fromisoformat(date_str)
dt = datetime.fromisoformat(date_str) return dt.strftime("%m/%d/%Y")
return dt.strftime("%m/%d/%Y at %I:%M %p") except:
except: return None
return None
def get_difficulty_display_name(difficulty):
def format_date_only(date_str): """Converts an internal difficulty name to display name"""
"""Formate une date sans l'heure""" difficulty_names = {
if not date_str: 'ultra': 'Ultra Nightmare',
return None 'nightmare': 'Nightmare',
try: 'brutal': 'Brutal',
dt = datetime.fromisoformat(date_str) 'hard': 'Hard',
return dt.strftime("%m/%d/%Y") 'normal': 'Normal',
except: 'easy': 'Easy'
return None }
return difficulty_names.get(difficulty, difficulty.title())
def get_difficulty_display_name(difficulty):
"""Convertit le nom de difficulté en nom d'affichage""" def is_authorized_channel(ctx):
difficulty_names = { return ctx.channel.id == AUTHORIZED_CHANNEL_ID
'ultra': 'Ultra Nightmare',
'nightmare': 'Nightmare', MERCY_RULES = {
'brutal': 'Brutal', "ancient": {"start": 200, "increment": 5, "base": 0.5},
'hard': 'Hard', "void": {"start": 200, "increment": 5, "base": 0.5},
'normal': 'Normal', "sacred": {"start": 12, "increment": 2, "base": 6},
'easy': 'Easy' "primal_legendary": {"start": 75, "increment": 1, "base": 1},
} "primal_mythical": {"start": 200, "increment": 10, "base": 0.5},
return difficulty_names.get(difficulty, difficulty.title()) "remnant": {"start": 24, "increment": 1, "base": 0},
}
def is_authorized_channel(ctx):
return ctx.channel.id == AUTHORIZED_CHANNEL_ID def calc_chance_and_guarantee(shard_type, pulls):
"""Returns chance, guaranteed pull count, and remaining pulls"""
MERCY_RULES = { if shard_type not in MERCY_RULES:
"ancient": {"start": 200, "increment": 5, "base": 0.5}, return 0, None, None
"void": {"start": 200, "increment": 5, "base": 0.5}, rule = MERCY_RULES[shard_type]
"sacred": {"start": 12, "increment": 2, "base": 6}, chance = rule["base"] if pulls < rule["start"] else rule["base"] + (pulls - rule["start"]) * rule["increment"]
"primal_legendary": {"start": 75, "increment": 1, "base": 1}, guaranteed_at = int(rule["start"] + (100 - rule["base"]) / rule["increment"])
"primal_mythical": {"start": 200, "increment": 10, "base": 0.5}, remaining = max(0, guaranteed_at - pulls)
"remnant": {"start": 24, "increment": 1, "base": 0}, return chance, guaranteed_at, remaining
}
def calc_chance_and_guarantee(shard_type, pulls):
"""Retourne chance, pull garanti et pulls restants"""
if shard_type not in MERCY_RULES:
return 0, None, None
rule = MERCY_RULES[shard_type]
chance = rule["base"] if pulls < rule["start"] else rule["base"] + (pulls - rule["start"]) * rule["increment"]
guaranteed_at = int(rule["start"] + (100 - rule["base"]) / rule["increment"])
remaining = max(0, guaranteed_at - pulls)
return chance, guaranteed_at, remaining

View file

@ -1,76 +1,74 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import discord import discord
from discord.ext import commands from discord.ext import commands
from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG, CLAN_CONFIG 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 db_manager = None
def set_db_manager(db): def set_db_manager(db):
global db_manager global db_manager
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):
"""Fonction générique pour afficher les classements""" """Generic function to display leaderboards"""
if ctx.channel.id != AUTHORIZED_CHANNEL_ID: if ctx.channel.id != AUTHORIZED_CHANNEL_ID:
return return
try: try:
# Normaliser la difficulté si spécifiée # Normalize difficulty if specified
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']:
difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties']) difficulties = " | ".join(BOSS_CONFIG[boss_type]['difficulties'])
await ctx.send(f"⚠️ Invalid difficulty. Available: {difficulties}") await ctx.send(f"⚠️ Invalid difficulty. Available: {difficulties}")
return return
boss_info = BOSS_CONFIG[boss_type] boss_info = BOSS_CONFIG[boss_type]
leaderboard = db_manager.get_leaderboard(boss_type, difficulty, 10, clan) leaderboard = db_manager.get_leaderboard(boss_type, difficulty, 10, clan)
if not leaderboard: if not leaderboard:
clan_text = f" for clan {clan}" if clan else "" clan_text = f" for clan {clan}" if clan else ""
difficulty_text = f" {get_difficulty_display_name(difficulty)}" if difficulty else "" difficulty_text = f" {get_difficulty_display_name(difficulty)}" if difficulty else ""
await ctx.send(f"⚠️ No{difficulty_text} {boss_info['name']} records found{clan_text} yet!") await ctx.send(f"⚠️ No{difficulty_text} {boss_info['name']} records found{clan_text} yet!")
return return
# Titre avec clan et difficulté si spécifiés # Title with clan and difficulty if specified
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"
if clan: if clan:
clan_info = CLAN_CONFIG.get(clan, {'name': clan, 'emoji': '🏛️'}) clan_info = CLAN_CONFIG.get(clan, {'name': clan, 'emoji': '🏛️'})
title = f"{clan_info['emoji']} {clan_info['name']} - {difficulty_name} {boss_info['name']} Top 10" title = f"{clan_info['emoji']} {clan_info['name']} - {difficulty_name} {boss_info['name']} Top 10"
embed = discord.Embed( embed = discord.Embed(
title=title, title=title,
color=boss_info['color'] if not clan else CLAN_CONFIG.get(clan, {'color': boss_info['color']})['color'] color=boss_info['color'] if not clan else CLAN_CONFIG.get(clan, {'color': boss_info['color']})['color']
) )
medals = ["🥇", "🥈", "🥉"] + ["🏅"] * 7 medals = ["🥇", "🥈", "🥉"] + ["🏅"] * 7
for i, (username, damage, date) in enumerate(leaderboard): for i, (username, damage, date, row_clan) in enumerate(leaderboard):
date_text = "" date_text = ""
if date: if date:
formatted_date = format_date_only(date) formatted_date = format_date_only(date)
if formatted_date: if formatted_date:
date_text = f"{formatted_date}" date_text = f"{formatted_date}"
# Afficher le clan dans le nom si pas de filtre par clan # Show clan emoji if leaderboard is global (not filtered by clan)
display_name = username display_name = username
if not clan: if not clan and row_clan:
user_clan = get_user_clan(username) clan_emoji = CLAN_CONFIG.get(row_clan, {'emoji': '🏛️'})['emoji']
if user_clan: display_name = f"{clan_emoji} {username}"
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}",
embed.add_field( value=f"**{format_damage_display(damage)} damage**{date_text}",
name=f"{medals[i]} #{i+1} {display_name}", inline=False
value=f"**{format_damage_display(damage)} damage**{date_text}", )
inline=False
) await ctx.send(embed=embed)
await ctx.send(embed=embed) except Exception as e:
await ctx.send(f"⚠️ Error: {e}")
except Exception as e:
await ctx.send(f"⚠️ Error: {e}")

View file

@ -1,183 +1,185 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import discord import discord
from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG from config import AUTHORIZED_CHANNEL_ID, BOSS_CONFIG
from utils.helpers import ( from utils.helpers import (
parse_damage_amount, parse_damage_amount,
normalize_difficulty, normalize_difficulty,
get_difficulty_display_name, get_difficulty_display_name,
format_damage_display, format_damage_display,
format_datetime, format_datetime,
) get_clan_from_member,
)
db_manager = None
screenshot_manager = None db_manager = None
screenshot_manager = None
def set_managers(db, ss):
"""Injection des managers (appelée une seule fois depuis bot.py)""" def set_managers(db, ss):
global db_manager, screenshot_manager """Injects managers (called once from bot.py)"""
db_manager = db global db_manager, screenshot_manager
screenshot_manager = ss 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""" async def handle_pb_command(ctx, boss_type, arg1=None, arg2=None):
if ctx.channel.id != AUTHORIZED_CHANNEL_ID: """Generic handler for all PB commands"""
return if ctx.channel.id != AUTHORIZED_CHANNEL_ID:
return
boss_info = BOSS_CONFIG[boss_type]
difficulties = boss_info['difficulties'] boss_info = BOSS_CONFIG[boss_type]
difficulties = boss_info['difficulties']
try:
# Pour CvC (pas de difficultés) try:
if not difficulties: # For CvC (no difficulties)
if arg1: if not difficulties:
damage = parse_damage_amount(arg1) if arg1:
if damage is not None: damage = parse_damage_amount(arg1)
await handle_pb_submission(ctx, boss_type, None, damage) if damage is not None:
else: await handle_pb_submission(ctx, boss_type, None, damage)
await show_user_pb(ctx, boss_type, None, arg1) else:
else: await show_user_pb(ctx, boss_type, None, arg1)
await show_user_pb(ctx, boss_type, None, ctx.author.display_name) else:
return await show_user_pb(ctx, boss_type, None, ctx.author.display_name)
return
# Pour Hydra et Chimera (avec difficultés)
if not arg1: # For Hydra and Chimera (with difficulties)
difficulty_list = " | ".join([d.title() for d in difficulties]) if not arg1:
await ctx.send( difficulty_list = " | ".join([d.title() for d in difficulties])
f"⚠️ Please specify difficulty and damage!\n" await ctx.send(
f"**Available difficulties:** {difficulty_list}\n" f"⚠️ Please specify difficulty and damage!\n"
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare\n" f"**Available difficulties:** {difficulty_list}\n"
f"**Examples:**\n" f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare\n"
f"`!pb{boss_type} normal 1.5M` - Submit PB with screenshot\n" f"**Examples:**\n"
f"`!pb{boss_type} nm 500K` - Submit Nightmare PB\n" f"`!pb{boss_type} normal 1.5M` - Submit PB with screenshot\n"
f"`!pb{boss_type} hard` - Show your Hard PB\n" f"`!pb{boss_type} nm 500K` - Submit Nightmare PB\n"
f"`!pb{boss_type} brutal username` - Show user's Brutal PB" f"`!pb{boss_type} hard` - Show your Hard PB\n"
) f"`!pb{boss_type} brutal username` - Show user's Brutal PB"
return )
return
normalized_difficulty = normalize_difficulty(arg1)
normalized_difficulty = normalize_difficulty(arg1)
if normalized_difficulty in difficulties:
difficulty = normalized_difficulty if normalized_difficulty in difficulties:
difficulty = normalized_difficulty
if arg2:
damage = parse_damage_amount(arg2) if arg2:
if damage is not None: damage = parse_damage_amount(arg2)
await handle_pb_submission(ctx, boss_type, difficulty, damage) if damage is not None:
else: await handle_pb_submission(ctx, boss_type, difficulty, damage)
await show_user_pb(ctx, boss_type, difficulty, arg2) else:
else: await show_user_pb(ctx, boss_type, difficulty, arg2)
await show_user_pb(ctx, boss_type, difficulty, ctx.author.display_name) else:
else: await show_user_pb(ctx, boss_type, difficulty, ctx.author.display_name)
difficulty_list = " | ".join([d.title() for d in difficulties]) else:
await ctx.send( difficulty_list = " | ".join([d.title() for d in difficulties])
f"⚠️ Invalid difficulty: `{arg1}`\n" await ctx.send(
f"**Available difficulties:** {difficulty_list}\n" f"⚠️ Invalid difficulty: `{arg1}`\n"
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare" f"**Available difficulties:** {difficulty_list}\n"
) f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare"
)
except Exception as e:
await ctx.send(f"⚠️ Error: {str(e)}") except Exception as e:
await ctx.send(f"⚠️ Error: {str(e)}")
async def handle_pb_submission(ctx, boss_type, difficulty, damage):
"""Gère la soumission d'un nouveau PB""" async def handle_pb_submission(ctx, boss_type, difficulty, damage):
if not ctx.message.attachments: """Handles submission of a new PB"""
await ctx.send("⚠️ Please attach a screenshot to validate your PB!") if not ctx.message.attachments:
return 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']): attachment = ctx.message.attachments[0]
await ctx.send("⚠️ Please attach a valid image file!") if not any(attachment.filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']):
return await ctx.send("⚠️ Please attach a valid image file!")
return
user_id = ctx.author.id
username = ctx.author.display_name user_id = ctx.author.id
current_pb, _, _ = db_manager.get_user_pb(user_id, boss_type, difficulty) username = ctx.author.display_name
clan = get_clan_from_member(ctx.author)
if damage > current_pb: current_pb, _, _ = db_manager.get_user_pb(user_id, boss_type, difficulty)
screenshot_filename = await screenshot_manager.save_screenshot(
attachment, username, damage, 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 if screenshot_filename:
) old_screenshot = db_manager.update_user_pb(
user_id, username, boss_type, damage, screenshot_filename, difficulty, clan
if old_screenshot: )
screenshot_manager.delete_old_screenshot(old_screenshot, boss_type, difficulty)
if old_screenshot:
improvement = damage - current_pb if current_pb > 0 else damage screenshot_manager.delete_old_screenshot(old_screenshot, boss_type, difficulty)
boss_info = BOSS_CONFIG[boss_type]
difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" improvement = damage - current_pb if current_pb > 0 else damage
boss_info = BOSS_CONFIG[boss_type]
embed = discord.Embed( difficulty_name = get_difficulty_display_name(difficulty) if difficulty else ""
title=f"🎉 NEW {boss_info['name'].upper()} PB! 🎉",
description=f"**{username}** just hit **{format_damage_display(damage)} damage** on {difficulty_name} {boss_info['name']}!", embed = discord.Embed(
color=0x00ff00 title=f"🎉 NEW {boss_info['name'].upper()} PB! 🎉",
) description=f"**{username}** just hit **{format_damage_display(damage)} damage** on {difficulty_name} {boss_info['name']}!",
embed.add_field(name="📈 Improvement", value=f"+{format_damage_display(improvement)} damage", inline=True) color=0x00ff00
)
# Envoi du screenshot correctement pour Discord embed.add_field(name="📈 Improvement", value=f"+{format_damage_display(improvement)} damage", inline=True)
screenshot_path = screenshot_manager.get_screenshot_path(screenshot_filename, boss_type, difficulty)
if screenshot_path and os.path.exists(screenshot_path): # Send screenshot to Discord
file = discord.File(screenshot_path, filename=screenshot_filename) screenshot_path = screenshot_manager.get_screenshot_path(screenshot_filename, boss_type, difficulty)
embed.set_image(url=f"attachment://{screenshot_filename}") if screenshot_path and os.path.exists(screenshot_path):
await ctx.send(embed=embed, file=file) file = discord.File(screenshot_path, filename=screenshot_filename)
else: embed.set_image(url=f"attachment://{screenshot_filename}")
await ctx.send(embed=embed) await ctx.send(embed=embed, file=file)
else: else:
await ctx.send("⚠️ Failed to save screenshot. Please try again.") await ctx.send(embed=embed)
else: else:
# Si le PB n'est pas battu, on montre le PB existant await ctx.send("⚠️ Failed to save screenshot. Please try again.")
await show_user_pb(ctx, boss_type, difficulty, username) else:
# PB not beaten, show current PB
async def show_user_pb(ctx, boss_type, difficulty, target_user): await show_user_pb(ctx, boss_type, difficulty, username)
"""Affiche le PB actuel d'un utilisateur"""
# Si target_user est un nom d'utilisateur, on essaie de le trouver async def show_user_pb(ctx, boss_type, difficulty, target_user):
if isinstance(target_user, str) and not target_user.isdigit(): """Displays the current PB for a user"""
# D'abord, vérifier si c'est l'utilisateur actuel # If target_user is a username, try to find them
if target_user.lower() == ctx.author.display_name.lower(): if isinstance(target_user, str) and not target_user.isdigit():
user_id = ctx.author.id # First check if it's the current user
display_name = ctx.author.display_name if target_user.lower() == ctx.author.display_name.lower():
else: user_id = ctx.author.id
# Chercher dans la base de données display_name = ctx.author.display_name
matches = db_manager.find_user_by_name(target_user) else:
if not matches: # Search in database
await ctx.send(f"⚠️ User **{target_user}** not found in database.") matches = db_manager.find_user_by_name(target_user)
return if not matches:
elif len(matches) > 1: await ctx.send(f"⚠️ User **{target_user}** not found in database.")
await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.") return
return elif len(matches) > 1:
else: await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.")
user_id, display_name = matches[0] return
else: else:
# Si c'est l'utilisateur actuel user_id, display_name = matches[0]
user_id = ctx.author.id else:
display_name = ctx.author.display_name # Current user
user_id = ctx.author.id
current_pb, screenshot, date = db_manager.get_user_pb(user_id, boss_type, difficulty) display_name = ctx.author.display_name
boss_info = BOSS_CONFIG[boss_type]
difficulty_name = get_difficulty_display_name(difficulty) if difficulty else "" current_pb, screenshot, date = db_manager.get_user_pb(user_id, boss_type, difficulty)
boss_info = BOSS_CONFIG[boss_type]
if current_pb > 0: difficulty_name = get_difficulty_display_name(difficulty) if difficulty else ""
embed = discord.Embed(
title=f"📊 {display_name}'s {difficulty_name} {boss_info['name']} PB", if current_pb > 0:
description=f"**{format_damage_display(current_pb)} damage**", embed = discord.Embed(
color=0x00bfff title=f"📊 {display_name}'s {difficulty_name} {boss_info['name']} PB",
) description=f"**{format_damage_display(current_pb)} damage**",
if date: color=0x00bfff
embed.add_field(name="📅 Date", value=format_datetime(date), inline=True) )
if date:
# Envoi du screenshot local correctement embed.add_field(name="📅 Date", value=format_datetime(date), inline=True)
if screenshot:
screenshot_path = screenshot_manager.get_screenshot_path(screenshot, boss_type, difficulty) # Send local screenshot to Discord
if screenshot_path and os.path.exists(screenshot_path): if screenshot:
file = discord.File(screenshot_path, filename=screenshot) screenshot_path = screenshot_manager.get_screenshot_path(screenshot, boss_type, difficulty)
embed.set_image(url=f"attachment://{screenshot}") if screenshot_path and os.path.exists(screenshot_path):
await ctx.send(embed=embed, file=file) file = discord.File(screenshot_path, filename=screenshot)
return embed.set_image(url=f"attachment://{screenshot}")
await ctx.send(embed=embed, file=file)
await ctx.send(embed=embed) return
else:
await ctx.send(f"⚠️ No PB found for **{display_name}** on {difficulty_name} {boss_info['name']}.") await ctx.send(embed=embed)
else:
await ctx.send(f"⚠️ No PB found for **{display_name}** on {difficulty_name} {boss_info['name']}.")