Compare commits
17 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e526a40bc | ||
|
|
64b60f1597 | ||
|
|
e364f354b0 | ||
|
|
314483e4f5 | ||
|
|
1a37ec8838 | ||
|
|
0d744c8cdc | ||
|
|
4ecac026b5 | ||
|
|
85942723ab | ||
| ec86776e42 | |||
| 01ef1cadcf | |||
| 14887625a2 | |||
| f4dbfba223 | |||
| bccf437192 | |||
| cd81d299ab | |||
| e4a38d7853 | |||
| cdc36dd42b | |||
| 8932696709 |
19 changed files with 1241 additions and 1209 deletions
|
|
@ -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
45
.github/workflows/deploy.yml
vendored
Normal 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
|
||||||
32
CLAUDE.md
32
CLAUDE.md
|
|
@ -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)
|
|
||||||
|
|
|
||||||
53
README.md
53
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
162
bot.py
162
bot.py
|
|
@ -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')
|
||||||
|
|
||||||
# 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",
|
||||||
"cogs.pbchimera",
|
"cogs.pbchimera",
|
||||||
"cogs.pbcvc",
|
"cogs.pbcvc",
|
||||||
"cogs.top10",
|
"cogs.top10",
|
||||||
"cogs.mystats",
|
"cogs.mystats",
|
||||||
"cogs.mercy",
|
"cogs.mercy",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Directory list
|
# Liste des dossiers
|
||||||
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",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
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} 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)
|
||||||
|
|
|
||||||
238
cogs/guide.py
238
cogs/guide.py
|
|
@ -1,119 +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):
|
||||||
"""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
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title="🧐 TEA 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
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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"
|
||||||
"**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
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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"
|
||||||
"`!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
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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"
|
||||||
"`!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
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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"
|
||||||
"`!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
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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"
|
||||||
"`!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
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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"
|
||||||
"`!top10chimera <difficulty>` - Global Chimera rankings\n"
|
"`!top10chimera <difficulty>` - Global Chimera rankings\n"
|
||||||
"`!top10cvc` - Global CvC rankings",
|
"`!top10cvc` - Global CvC rankings",
|
||||||
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"
|
||||||
"**🛡️ TEAF (Flame):** `!teafhydra <diff>` `!teafchimera <diff>` `!teafcvc`\n"
|
"**🛡️ TEAF (Flame):** `!teafhydra <diff>` `!teafchimera <diff>` `!teafcvc`\n"
|
||||||
"**⚔️ TEAC (Cinder):** `!teachydra <diff>` `!teachimera <diff>` `!teaccvc`\n"
|
"**⚔️ TEAC (Cinder):** `!teachydra <diff>` `!teachimera <diff>` `!teaccvc`\n"
|
||||||
"**👑 TEACO (Corrupted Olympians):** `!teacohydra <diff>` `!teacochimera <diff>` `!teacocvc`",
|
"**👑 TEACO (Corrupted Olympians):** `!teacohydra <diff>` `!teacochimera <diff>` `!teacocvc`",
|
||||||
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"
|
||||||
"`!mystats <username>` - View someone's PBs\n"
|
"`!mystats <username>` - View someone's PBs\n"
|
||||||
"`!guide` - Show this help message",
|
"`!guide` - Show this help message",
|
||||||
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"
|
||||||
"`!pbchimera unm 500K` - Submit Ultra Nightmare PB\n"
|
"`!pbchimera unm 500K` - Submit Ultra Nightmare PB\n"
|
||||||
"`!pbcvc 2.3M` - Submit CvC PB\n"
|
"`!pbcvc 2.3M` - Submit CvC PB\n"
|
||||||
"`!mercy add 50 primal` - Add 50 pulls to Primal shard\n"
|
"`!mercy add 50 primal` - Add 50 pulls to Primal shard\n"
|
||||||
"`!mercy show` - Show your mercy pulls\n"
|
"`!mercy show` - Show your mercy pulls\n"
|
||||||
"`!teaihydra nm` - TEAI clan Nightmare rankings\n"
|
"`!teaihydra nm` - TEAI clan Nightmare rankings\n"
|
||||||
"**Always attach screenshot when submitting PBs!**",
|
"**Always attach screenshot when submitting PBs!**",
|
||||||
inline=False
|
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):
|
async def setup(bot):
|
||||||
await bot.add_cog(Guide(bot))
|
await bot.add_cog(Guide(bot))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
196
cogs/mystats.py
196
cogs/mystats.py
|
|
@ -1,98 +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 # 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
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if target_user:
|
if target_user:
|
||||||
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"❌ No data found for **{target_user}**.")
|
await ctx.send(f"❌ No data found for **{target_user}**.")
|
||||||
return
|
return
|
||||||
if len(matches) > 1:
|
if len(matches) > 1:
|
||||||
await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.")
|
await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.")
|
||||||
return
|
return
|
||||||
user_id, display_name = matches[0]
|
user_id, display_name = matches[0]
|
||||||
else:
|
else:
|
||||||
user_id = ctx.author.id
|
user_id = ctx.author.id
|
||||||
display_name = ctx.author.display_name
|
display_name = ctx.author.display_name
|
||||||
|
|
||||||
user_data = db_manager.get_user_all_pbs(user_id)
|
user_data = db_manager.get_user_all_pbs(user_id)
|
||||||
|
|
||||||
if not user_data:
|
if not user_data:
|
||||||
await ctx.send(f"❌ No data found for **{display_name}**.")
|
await ctx.send(f"❌ No data found for **{display_name}**.")
|
||||||
return
|
return
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"📊 {display_name}'s Complete Stats",
|
title=f"📊 {display_name}'s Complete Stats",
|
||||||
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}'
|
||||||
date_key = f'pb_hydra_{difficulty}_date'
|
date_key = f'pb_hydra_{difficulty}_date'
|
||||||
|
|
||||||
if pb_key in user_data and user_data[pb_key] > 0:
|
if pb_key in user_data and user_data[pb_key] > 0:
|
||||||
pb_value = user_data[pb_key]
|
pb_value = user_data[pb_key]
|
||||||
pb_date = user_data.get(date_key)
|
pb_date = user_data.get(date_key)
|
||||||
date_text = f" • {format_date_only(pb_date)}" if pb_date else ""
|
date_text = f" • {format_date_only(pb_date)}" if pb_date else ""
|
||||||
hydra_stats.append(f"**{difficulty.title()}:** {format_damage_display(pb_value)}{date_text}")
|
hydra_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_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}'
|
||||||
date_key = f'pb_chimera_{difficulty}_date'
|
date_key = f'pb_chimera_{difficulty}_date'
|
||||||
|
|
||||||
if pb_key in user_data and user_data[pb_key] > 0:
|
if pb_key in user_data and user_data[pb_key] > 0:
|
||||||
pb_value = user_data[pb_key]
|
pb_value = user_data[pb_key]
|
||||||
pb_date = user_data.get(date_key)
|
pb_date = user_data.get(date_key)
|
||||||
date_text = f" • {format_date_only(pb_date)}" if pb_date else ""
|
date_text = f" • {format_date_only(pb_date)}" if pb_date else ""
|
||||||
display_name = "Ultra Nightmare" if difficulty == "ultra" else difficulty.title()
|
display_name = "Ultra Nightmare" if difficulty == "ultra" else difficulty.title()
|
||||||
chimera_stats.append(f"**{display_name}:** {format_damage_display(pb_value)}{date_text}")
|
chimera_stats.append(f"**{display_name}:** {format_damage_display(pb_value)}{date_text}")
|
||||||
|
|
||||||
chimera_text = "\n".join(chimera_stats) if chimera_stats else "No records"
|
chimera_text = "\n".join(chimera_stats) if chimera_stats else "No records"
|
||||||
embed.add_field(name="🛡️ Chimera PBs", value=chimera_text, inline=False)
|
embed.add_field(name="🛡️ Chimera PBs", value=chimera_text, inline=False)
|
||||||
|
|
||||||
# CvC
|
# CvC
|
||||||
cvc_pb = user_data.get('pb_cvc', 0)
|
cvc_pb = user_data.get('pb_cvc', 0)
|
||||||
cvc_date = user_data.get('pb_cvc_date')
|
cvc_date = user_data.get('pb_cvc_date')
|
||||||
cvc_text = f"**{format_damage_display(cvc_pb)} damage**" if cvc_pb > 0 else "No record"
|
cvc_text = f"**{format_damage_display(cvc_pb)} damage**" if cvc_pb > 0 else "No record"
|
||||||
if cvc_pb > 0 and cvc_date:
|
if cvc_pb > 0 and cvc_date:
|
||||||
formatted_date = format_date_only(cvc_date)
|
formatted_date = format_date_only(cvc_date)
|
||||||
if formatted_date:
|
if formatted_date:
|
||||||
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)
|
||||||
embed.add_field(name="💯 Total Combined Damage", value=f"**{format_damage_display(total_damage)}**", inline=False)
|
embed.add_field(name="💯 Total Combined Damage", value=f"**{format_damage_display(total_damage)}**", inline=False)
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(f"❌ Error: {e}")
|
await ctx.send(f"❌ Error: {e}")
|
||||||
|
|
||||||
async def setup(bot):
|
async def setup(bot):
|
||||||
await bot.add_cog(MyStats(bot))
|
await bot.add_cog(MyStats(bot))
|
||||||
|
|
|
||||||
|
|
@ -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 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):
|
||||||
await bot.add_cog(Pbchimera(bot))
|
await bot.add_cog(Pbchimera(bot))
|
||||||
|
|
|
||||||
|
|
@ -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 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):
|
||||||
await bot.add_cog(Pbcvc(bot))
|
await bot.add_cog(Pbcvc(bot))
|
||||||
|
|
|
||||||
|
|
@ -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 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):
|
||||||
await bot.add_cog(Pbhydra(bot))
|
await bot.add_cog(Pbhydra(bot))
|
||||||
|
|
|
||||||
206
cogs/top10.py
206
cogs/top10.py
|
|
@ -1,103 +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 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']:
|
||||||
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')
|
||||||
|
|
||||||
# --- 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')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def teaichimera(self, ctx, difficulty: str = None):
|
async def teaichimera(self, ctx, difficulty: str = None):
|
||||||
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAI')
|
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAI')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
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')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def teafchimera(self, ctx, difficulty: str = None):
|
async def teafchimera(self, ctx, difficulty: str = None):
|
||||||
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAF')
|
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAF')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
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')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def teachimera(self, ctx, difficulty: str = None):
|
async def teachimera(self, ctx, difficulty: str = None):
|
||||||
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAC')
|
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEAC')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
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')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
async def teacochimera(self, ctx, difficulty: str = None):
|
async def teacochimera(self, ctx, difficulty: str = None):
|
||||||
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEACO')
|
await self._show_clan_leaderboard(ctx, 'chimera', difficulty, 'TEACO')
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
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 n’a 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"
|
||||||
f"**Available:** {difficulties}\n"
|
f"**Available:** {difficulties}\n"
|
||||||
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra"
|
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await show_leaderboard(ctx, boss_type, clan=clan)
|
await show_leaderboard(ctx, boss_type, clan=clan)
|
||||||
|
|
||||||
async def setup(bot):
|
async def setup(bot):
|
||||||
await bot.add_cog(Top10(bot))
|
await bot.add_cog(Top10(bot))
|
||||||
|
|
|
||||||
102
config.py
102
config.py
|
|
@ -1,51 +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 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},
|
||||||
'TEAC': {'name': 'TEAC', 'full_name': 'Cinder', 'emoji': '⚔️', 'color': 0x1e90ff},
|
'TEAC': {'name': 'TEAC', 'full_name': 'Cinder', 'emoji': '⚔️', 'color': 0x1e90ff},
|
||||||
'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']},
|
||||||
'chimera': {'name': 'Chimera', 'emoji': '🦁', 'color': 0x9932cc,
|
'chimera': {'name': 'Chimera', 'emoji': '🦁', 'color': 0x9932cc,
|
||||||
'difficulties': ['easy', 'normal', 'hard', 'brutal', 'nightmare', 'ultra']},
|
'difficulties': ['easy', 'normal', 'hard', 'brutal', 'nightmare', 'ultra']},
|
||||||
'cvc': {'name': 'Clan vs Clan', 'emoji': '✔️', 'color': 0xff0000, 'difficulties': []}
|
'cvc': {'name': 'Clan vs Clan', 'emoji': '✔️', 'color': 0xff0000, 'difficulties': []}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Difficulty shortcut mappings
|
# Mappings pour diminutifs de difficultés
|
||||||
DIFFICULTY_SHORTCUTS = {
|
DIFFICULTY_SHORTCUTS = {
|
||||||
'nm': 'nightmare',
|
'nm': 'nightmare',
|
||||||
'unm': 'ultra'
|
'unm': 'ultra'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,213 +1,213 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from config import DATABASE_PATH, CLAN_MIGRATION
|
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):
|
||||||
"""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,
|
||||||
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 (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,
|
||||||
|
|
||||||
total_attempts INTEGER DEFAULT 0,
|
total_attempts INTEGER DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# 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()]
|
||||||
|
|
||||||
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')
|
||||||
|
|
||||||
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 ("
|
||||||
"discord_username LIKE ? OR discord_username LIKE ?)",
|
"discord_username LIKE ? OR discord_username LIKE ?)",
|
||||||
(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,
|
||||||
discord_id TEXT,
|
discord_id TEXT,
|
||||||
username TEXT,
|
username TEXT,
|
||||||
boss_type TEXT,
|
boss_type TEXT,
|
||||||
difficulty TEXT,
|
difficulty TEXT,
|
||||||
damage INTEGER,
|
damage INTEGER,
|
||||||
screenshot_filename TEXT,
|
screenshot_filename TEXT,
|
||||||
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
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()
|
||||||
|
|
||||||
if difficulty:
|
if difficulty:
|
||||||
column_prefix = f"pb_{boss_type}_{difficulty}"
|
column_prefix = f"pb_{boss_type}_{difficulty}"
|
||||||
else:
|
else:
|
||||||
column_prefix = f"pb_{boss_type}"
|
column_prefix = f"pb_{boss_type}"
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
f"SELECT {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date FROM users WHERE discord_id = ?",
|
f"SELECT {column_prefix}, {column_prefix}_screenshot, {column_prefix}_date FROM users WHERE discord_id = ?",
|
||||||
(str(user_id),)
|
(str(user_id),)
|
||||||
)
|
)
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
if difficulty:
|
if difficulty:
|
||||||
column_prefix = f"pb_{boss_type}_{difficulty}"
|
column_prefix = f"pb_{boss_type}_{difficulty}"
|
||||||
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)
|
||||||
ON CONFLICT(discord_id)
|
ON CONFLICT(discord_id)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
discord_username = ?,
|
discord_username = ?,
|
||||||
clan = COALESCE(?, clan),
|
clan = COALESCE(?, clan),
|
||||||
{column_prefix} = ?,
|
{column_prefix} = ?,
|
||||||
{column_prefix}_screenshot = ?,
|
{column_prefix}_screenshot = ?,
|
||||||
{column_prefix}_date = CURRENT_TIMESTAMP,
|
{column_prefix}_date = CURRENT_TIMESTAMP,
|
||||||
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 (?, ?, ?, ?, ?, ?)
|
||||||
''', (str(user_id), username, boss_type, difficulty or 'none', damage, screenshot_filename))
|
''', (str(user_id), username, boss_type, difficulty or 'none', damage, screenshot_filename))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
if difficulty:
|
if difficulty:
|
||||||
column_prefix = f"pb_{boss_type}_{difficulty}"
|
column_prefix = f"pb_{boss_type}_{difficulty}"
|
||||||
else:
|
else:
|
||||||
column_prefix = f"pb_{boss_type}"
|
column_prefix = f"pb_{boss_type}"
|
||||||
|
|
||||||
base_query = f'''
|
base_query = f'''
|
||||||
SELECT discord_username, {column_prefix}, {column_prefix}_date, clan
|
SELECT discord_username, {column_prefix}, {column_prefix}_date, clan
|
||||||
FROM users
|
FROM users
|
||||||
WHERE {column_prefix} > 0
|
WHERE {column_prefix} > 0
|
||||||
'''
|
'''
|
||||||
|
|
||||||
params = []
|
params = []
|
||||||
if clan:
|
if clan:
|
||||||
base_query += ' AND clan = ?'
|
base_query += ' AND clan = ?'
|
||||||
params.append(clan)
|
params.append(clan)
|
||||||
|
|
||||||
base_query += f' ORDER BY {column_prefix} DESC LIMIT ?'
|
base_query += f' ORDER BY {column_prefix} DESC LIMIT ?'
|
||||||
params.append(limit)
|
params.append(limit)
|
||||||
|
|
||||||
cursor.execute(base_query, params)
|
cursor.execute(base_query, params)
|
||||||
results = cursor.fetchall()
|
results = cursor.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
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]
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
cursor.execute('SELECT discord_id, discord_username FROM users WHERE discord_username LIKE ?', (f'%{username}%',))
|
cursor.execute('SELECT discord_id, discord_username FROM users WHERE discord_username LIKE ?', (f'%{username}%',))
|
||||||
results = cursor.fetchall()
|
results = cursor.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
@ -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},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
# 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()
|
||||||
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:
|
||||||
# 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)
|
||||||
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):
|
||||||
"""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)}")
|
||||||
|
|
|
||||||
212
utils/helpers.py
212
utils/helpers.py
|
|
@ -1,106 +1,106 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
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()
|
||||||
if damage_str.isdigit():
|
if damage_str.isdigit():
|
||||||
return int(damage_str)
|
return int(damage_str)
|
||||||
match = re.match(r'^([0-9]*\.?[0-9]+)([KMB]?)$', damage_str)
|
match = re.match(r'^([0-9]*\.?[0-9]+)([KMB]?)$', damage_str)
|
||||||
if not match:
|
if not match:
|
||||||
return None
|
return None
|
||||||
number_str, suffix = match.groups()
|
number_str, suffix = match.groups()
|
||||||
try:
|
try:
|
||||||
number = float(number_str)
|
number = float(number_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
multipliers = {'K': 1_000, 'M': 1_000_000, 'B': 1_000_000_000, '': 1}
|
multipliers = {'K': 1_000, 'M': 1_000_000, 'B': 1_000_000_000, '': 1}
|
||||||
return int(number * multipliers[suffix])
|
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"
|
||||||
elif damage >= 1_000_000:
|
elif damage >= 1_000_000:
|
||||||
millions = damage / 1_000_000
|
millions = damage / 1_000_000
|
||||||
return f"{int(millions)}M" if millions == int(millions) else f"{millions:.1f}M"
|
return f"{int(millions)}M" if millions == int(millions) else f"{millions:.1f}M"
|
||||||
elif damage >= 1_000:
|
elif damage >= 1_000:
|
||||||
thousands = damage / 1_000
|
thousands = damage / 1_000
|
||||||
return f"{int(thousands)}K" if thousands == int(thousands) else f"{thousands:.1f}K"
|
return f"{int(thousands)}K" if thousands == int(thousands) else f"{thousands:.1f}K"
|
||||||
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()
|
||||||
if difficulty_lower in DIFFICULTY_SHORTCUTS:
|
if difficulty_lower in DIFFICULTY_SHORTCUTS:
|
||||||
return DIFFICULTY_SHORTCUTS[difficulty_lower]
|
return DIFFICULTY_SHORTCUTS[difficulty_lower]
|
||||||
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:
|
||||||
dt = datetime.fromisoformat(date_str)
|
dt = datetime.fromisoformat(date_str)
|
||||||
return dt.strftime("%m/%d/%Y at %I:%M %p")
|
return dt.strftime("%m/%d/%Y at %I:%M %p")
|
||||||
except:
|
except:
|
||||||
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:
|
||||||
dt = datetime.fromisoformat(date_str)
|
dt = datetime.fromisoformat(date_str)
|
||||||
return dt.strftime("%m/%d/%Y")
|
return dt.strftime("%m/%d/%Y")
|
||||||
except:
|
except:
|
||||||
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',
|
||||||
'brutal': 'Brutal',
|
'brutal': 'Brutal',
|
||||||
'hard': 'Hard',
|
'hard': 'Hard',
|
||||||
'normal': 'Normal',
|
'normal': 'Normal',
|
||||||
'easy': 'Easy'
|
'easy': 'Easy'
|
||||||
}
|
}
|
||||||
return difficulty_names.get(difficulty, difficulty.title())
|
return difficulty_names.get(difficulty, difficulty.title())
|
||||||
|
|
||||||
def is_authorized_channel(ctx):
|
def is_authorized_channel(ctx):
|
||||||
return ctx.channel.id == AUTHORIZED_CHANNEL_ID
|
return ctx.channel.id == AUTHORIZED_CHANNEL_ID
|
||||||
|
|
||||||
MERCY_RULES = {
|
MERCY_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},
|
||||||
"sacred": {"start": 12, "increment": 2, "base": 6},
|
"sacred": {"start": 12, "increment": 2, "base": 6},
|
||||||
"primal_legendary": {"start": 75, "increment": 1, "base": 1},
|
"primal_legendary": {"start": 75, "increment": 1, "base": 1},
|
||||||
"primal_mythical": {"start": 200, "increment": 10, "base": 0.5},
|
"primal_mythical": {"start": 200, "increment": 10, "base": 0.5},
|
||||||
"remnant": {"start": 24, "increment": 1, "base": 0},
|
"remnant": {"start": 24, "increment": 1, "base": 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
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]
|
||||||
chance = rule["base"] if pulls < rule["start"] else rule["base"] + (pulls - rule["start"]) * rule["increment"]
|
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"])
|
guaranteed_at = int(rule["start"] + (100 - rule["base"]) / rule["increment"])
|
||||||
remaining = max(0, guaranteed_at - pulls)
|
remaining = max(0, guaranteed_at - pulls)
|
||||||
return chance, guaranteed_at, remaining
|
return chance, guaranteed_at, remaining
|
||||||
|
|
@ -1,74 +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
|
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):
|
||||||
"""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']:
|
||||||
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
|
||||||
|
|
||||||
# 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"
|
||||||
|
|
||||||
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, row_clan) 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}"
|
||||||
|
|
||||||
# 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']
|
||||||
display_name = f"{clan_emoji} {username}"
|
display_name = f"{clan_emoji} {username}"
|
||||||
|
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name=f"{medals[i]} #{i+1} {display_name}",
|
name=f"{medals[i]} #{i+1} {display_name}",
|
||||||
value=f"**{format_damage_display(damage)} damage**{date_text}",
|
value=f"**{format_damage_display(damage)} damage**{date_text}",
|
||||||
inline=False
|
inline=False
|
||||||
)
|
)
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(f"⚠️ Error: {e}")
|
await ctx.send(f"⚠️ Error: {e}")
|
||||||
|
|
|
||||||
|
|
@ -1,185 +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,
|
get_clan_from_member,
|
||||||
)
|
)
|
||||||
|
|
||||||
db_manager = None
|
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
|
||||||
|
|
||||||
boss_info = BOSS_CONFIG[boss_type]
|
boss_info = BOSS_CONFIG[boss_type]
|
||||||
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)
|
||||||
if damage is not None:
|
if damage is not None:
|
||||||
await handle_pb_submission(ctx, boss_type, None, damage)
|
await handle_pb_submission(ctx, boss_type, None, damage)
|
||||||
else:
|
else:
|
||||||
await show_user_pb(ctx, boss_type, None, arg1)
|
await show_user_pb(ctx, boss_type, None, arg1)
|
||||||
else:
|
else:
|
||||||
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(
|
||||||
f"⚠️ Please specify difficulty and damage!\n"
|
f"⚠️ Please specify difficulty and damage!\n"
|
||||||
f"**Available difficulties:** {difficulty_list}\n"
|
f"**Available difficulties:** {difficulty_list}\n"
|
||||||
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare\n"
|
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare\n"
|
||||||
f"**Examples:**\n"
|
f"**Examples:**\n"
|
||||||
f"`!pb{boss_type} normal 1.5M` - Submit PB with screenshot\n"
|
f"`!pb{boss_type} normal 1.5M` - Submit PB with screenshot\n"
|
||||||
f"`!pb{boss_type} nm 500K` - Submit Nightmare PB\n"
|
f"`!pb{boss_type} nm 500K` - Submit Nightmare PB\n"
|
||||||
f"`!pb{boss_type} hard` - Show your Hard PB\n"
|
f"`!pb{boss_type} hard` - Show your Hard PB\n"
|
||||||
f"`!pb{boss_type} brutal username` - Show user's Brutal PB"
|
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:
|
if normalized_difficulty in difficulties:
|
||||||
difficulty = normalized_difficulty
|
difficulty = normalized_difficulty
|
||||||
|
|
||||||
if arg2:
|
if arg2:
|
||||||
damage = parse_damage_amount(arg2)
|
damage = parse_damage_amount(arg2)
|
||||||
if damage is not None:
|
if damage is not None:
|
||||||
await handle_pb_submission(ctx, boss_type, difficulty, damage)
|
await handle_pb_submission(ctx, boss_type, difficulty, damage)
|
||||||
else:
|
else:
|
||||||
await show_user_pb(ctx, boss_type, difficulty, arg2)
|
await show_user_pb(ctx, boss_type, difficulty, arg2)
|
||||||
else:
|
else:
|
||||||
await show_user_pb(ctx, boss_type, difficulty, ctx.author.display_name)
|
await show_user_pb(ctx, boss_type, difficulty, ctx.author.display_name)
|
||||||
else:
|
else:
|
||||||
difficulty_list = " | ".join([d.title() for d in difficulties])
|
difficulty_list = " | ".join([d.title() for d in difficulties])
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
f"⚠️ Invalid difficulty: `{arg1}`\n"
|
f"⚠️ Invalid difficulty: `{arg1}`\n"
|
||||||
f"**Available difficulties:** {difficulty_list}\n"
|
f"**Available difficulties:** {difficulty_list}\n"
|
||||||
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare"
|
f"**Shortcuts:** `nm` = Nightmare, `unm` = Ultra Nightmare"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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
|
||||||
|
|
||||||
attachment = ctx.message.attachments[0]
|
attachment = ctx.message.attachments[0]
|
||||||
if not any(attachment.filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']):
|
if not any(attachment.filename.lower().endswith(ext) for ext in ['.png', '.jpg', '.jpeg', '.gif', '.webp']):
|
||||||
await ctx.send("⚠️ Please attach a valid image file!")
|
await ctx.send("⚠️ Please attach a valid image file!")
|
||||||
return
|
return
|
||||||
|
|
||||||
user_id = ctx.author.id
|
user_id = ctx.author.id
|
||||||
username = ctx.author.display_name
|
username = ctx.author.display_name
|
||||||
clan = get_clan_from_member(ctx.author)
|
clan = get_clan_from_member(ctx.author)
|
||||||
current_pb, _, _ = db_manager.get_user_pb(user_id, boss_type, difficulty)
|
current_pb, _, _ = db_manager.get_user_pb(user_id, boss_type, difficulty)
|
||||||
|
|
||||||
if damage > current_pb:
|
if damage > current_pb:
|
||||||
screenshot_filename = await screenshot_manager.save_screenshot(
|
screenshot_filename = await screenshot_manager.save_screenshot(
|
||||||
attachment, username, damage, boss_type, difficulty
|
attachment, username, damage, boss_type, difficulty
|
||||||
)
|
)
|
||||||
|
|
||||||
if screenshot_filename:
|
if screenshot_filename:
|
||||||
old_screenshot = db_manager.update_user_pb(
|
old_screenshot = db_manager.update_user_pb(
|
||||||
user_id, username, boss_type, damage, screenshot_filename, difficulty, clan
|
user_id, username, boss_type, damage, screenshot_filename, difficulty, clan
|
||||||
)
|
)
|
||||||
|
|
||||||
if old_screenshot:
|
if old_screenshot:
|
||||||
screenshot_manager.delete_old_screenshot(old_screenshot, boss_type, difficulty)
|
screenshot_manager.delete_old_screenshot(old_screenshot, boss_type, difficulty)
|
||||||
|
|
||||||
improvement = damage - current_pb if current_pb > 0 else damage
|
improvement = damage - current_pb if current_pb > 0 else damage
|
||||||
boss_info = BOSS_CONFIG[boss_type]
|
boss_info = BOSS_CONFIG[boss_type]
|
||||||
difficulty_name = get_difficulty_display_name(difficulty) if difficulty else ""
|
difficulty_name = get_difficulty_display_name(difficulty) if difficulty else ""
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"🎉 NEW {boss_info['name'].upper()} PB! 🎉",
|
title=f"🎉 NEW {boss_info['name'].upper()} PB! 🎉",
|
||||||
description=f"**{username}** just hit **{format_damage_display(damage)} damage** on {difficulty_name} {boss_info['name']}!",
|
description=f"**{username}** just hit **{format_damage_display(damage)} damage** on {difficulty_name} {boss_info['name']}!",
|
||||||
color=0x00ff00
|
color=0x00ff00
|
||||||
)
|
)
|
||||||
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)
|
||||||
embed.set_image(url=f"attachment://{screenshot_filename}")
|
embed.set_image(url=f"attachment://{screenshot_filename}")
|
||||||
await ctx.send(embed=embed, file=file)
|
await ctx.send(embed=embed, file=file)
|
||||||
else:
|
else:
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
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.")
|
||||||
return
|
return
|
||||||
elif len(matches) > 1:
|
elif len(matches) > 1:
|
||||||
await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.")
|
await ctx.send(f"⚠️ Multiple users found for **{target_user}**. Please be more specific.")
|
||||||
return
|
return
|
||||||
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
|
||||||
|
|
||||||
current_pb, screenshot, date = db_manager.get_user_pb(user_id, boss_type, difficulty)
|
current_pb, screenshot, date = db_manager.get_user_pb(user_id, boss_type, difficulty)
|
||||||
boss_info = BOSS_CONFIG[boss_type]
|
boss_info = BOSS_CONFIG[boss_type]
|
||||||
difficulty_name = get_difficulty_display_name(difficulty) if difficulty else ""
|
difficulty_name = get_difficulty_display_name(difficulty) if difficulty else ""
|
||||||
|
|
||||||
if current_pb > 0:
|
if current_pb > 0:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"📊 {display_name}'s {difficulty_name} {boss_info['name']} PB",
|
title=f"📊 {display_name}'s {difficulty_name} {boss_info['name']} PB",
|
||||||
description=f"**{format_damage_display(current_pb)} damage**",
|
description=f"**{format_damage_display(current_pb)} damage**",
|
||||||
color=0x00bfff
|
color=0x00bfff
|
||||||
)
|
)
|
||||||
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):
|
||||||
file = discord.File(screenshot_path, filename=screenshot)
|
file = discord.File(screenshot_path, filename=screenshot)
|
||||||
embed.set_image(url=f"attachment://{screenshot}")
|
embed.set_image(url=f"attachment://{screenshot}")
|
||||||
await ctx.send(embed=embed, file=file)
|
await ctx.send(embed=embed, file=file)
|
||||||
return
|
return
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
else:
|
else:
|
||||||
await ctx.send(f"⚠️ No PB found for **{display_name}** on {difficulty_name} {boss_info['name']}.")
|
await ctx.send(f"⚠️ No PB found for **{display_name}** on {difficulty_name} {boss_info['name']}.")
|
||||||
Loading…
Reference in a new issue