First Commit
This commit is contained in:
Executable
+245
@@ -0,0 +1,245 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from utils.sql_commands import DatabaseManager
|
||||
from utils.bank_functions import *
|
||||
from random import randint
|
||||
from math import log2
|
||||
from datetime import datetime
|
||||
import io
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
def calculate_xp_needed_for_next_level(level: int) -> int:
|
||||
"""Calculates the XP needed for the next level."""
|
||||
return int(35 * (level**2) + 35 * level)
|
||||
|
||||
|
||||
class XP(commands.Cog):
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.db = DatabaseManager("")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, ctx: discord.Message) -> None:
|
||||
if ctx.author.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
data = self.db.fetch_one(
|
||||
"SELECT XP, LEVEL FROM users WHERE ID = %s", (ctx.author.id,)
|
||||
)
|
||||
if data is None:
|
||||
# Insert a new row for this user
|
||||
current_level = 0
|
||||
current_xp = 0
|
||||
self.db.execute_query(
|
||||
"INSERT INTO users (ID, XP, LEVEL) VALUES (%s, %s, %s)",
|
||||
(ctx.author.id, current_xp, current_level),
|
||||
)
|
||||
else:
|
||||
current_level = data.get("LEVEL", 0)
|
||||
current_xp = data.get("XP", 0)
|
||||
|
||||
logging.info(f"XP: {current_xp}, Level: {current_level}")
|
||||
|
||||
# Balanced XP calculation
|
||||
base = randint(5, 10)
|
||||
length_bonus = (
|
||||
min(len(ctx.content), 100) // 10
|
||||
) # +1 XP per 10 chars, max +10
|
||||
extra_xp = base + length_bonus
|
||||
extra_xp = min(extra_xp, 20) # cap at 20 XP per message
|
||||
current_xp += extra_xp
|
||||
|
||||
if current_xp > calculate_xp_needed_for_next_level(current_level):
|
||||
current_level += 1
|
||||
player_balance = self.db.fetch_one(
|
||||
"SELECT BANK FROM economy WHERE ID = %s", (ctx.author.id,)
|
||||
)
|
||||
player_balance = player_balance.get("BANK", 0) if player_balance else 0
|
||||
levelup_bonus = randint(500, 2000)
|
||||
new_balance = player_balance + levelup_bonus
|
||||
await update_money(ctx.author, bank=new_balance)
|
||||
|
||||
await ctx.channel.send(
|
||||
f"Congrats!! You achieved a new level.\nYou are now level {current_level}. You received {levelup_bonus}<:flooney:1194943899765051473>"
|
||||
)
|
||||
current_xp = 0
|
||||
|
||||
# Use INSERT ... ON DUPLICATE KEY UPDATE for XP/level
|
||||
self.db.execute_query(
|
||||
"INSERT INTO users (ID, XP, LEVEL) VALUES (%s, %s, %s) "
|
||||
"ON DUPLICATE KEY UPDATE XP = %s, LEVEL = %s",
|
||||
(ctx.author.id, current_xp, current_level, current_xp, current_level),
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in XP on_message: {e}")
|
||||
|
||||
@commands.command()
|
||||
async def top(self, ctx: commands.Context) -> None:
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.purple(),
|
||||
timestamp=ctx.message.created_at,
|
||||
title="Top Messengers",
|
||||
)
|
||||
users = self.db.fetch_all("SELECT ID, XP, LEVEL FROM users")
|
||||
if not users:
|
||||
return
|
||||
|
||||
sorted_users = sorted(users, key=lambda x: int(x["XP"]), reverse=True)[:10]
|
||||
data = []
|
||||
|
||||
for index, member in enumerate(sorted_users, start=1):
|
||||
try:
|
||||
member_obj = await self.client.fetch_user(int(member["ID"]))
|
||||
member_name = (
|
||||
member_obj.display_name
|
||||
if hasattr(member_obj, "display_name")
|
||||
else str(member_obj)
|
||||
)
|
||||
except Exception:
|
||||
member_name = f"User {member['ID']}"
|
||||
member_xp, member_lvl = int(member["XP"]), int(member["LEVEL"])
|
||||
medals = ["🥇", "🥈", "🥉"]
|
||||
msg = f"**{medals[index - 1] if index <= 3 else index} `{member_name}` -- {member_xp:,}xp, {member_lvl:,}level**"
|
||||
data.append(msg)
|
||||
|
||||
msg = "\n".join(data)
|
||||
embed.description = f"It's Based on XP of Global Users\n\n{msg}"
|
||||
guild_name = ctx.guild.name if ctx.guild is not None else "Direct Message"
|
||||
embed.set_footer(text=f"GLOBAL - {guild_name}")
|
||||
embed.color = discord.Color(0x00FF00)
|
||||
await ctx.reply(embed=embed, mention_author=False)
|
||||
|
||||
@commands.command()
|
||||
async def stats(self, ctx: commands.Context) -> None:
|
||||
"""Shows the user's current XP and level."""
|
||||
try:
|
||||
# Use a fallback background if the random one doesn't exist
|
||||
bg_path = f"images/image_{randint(1,100)}.jpg"
|
||||
if not os.path.exists(bg_path):
|
||||
bg_path = "images/default.jpg"
|
||||
profile_image = await self.create_profile_card(ctx.author, bg_path)
|
||||
|
||||
await ctx.send(file=discord.File(profile_image, "profile.png"))
|
||||
|
||||
if profile_image is not None:
|
||||
profile_image.close()
|
||||
|
||||
except ZeroDivisionError:
|
||||
await ctx.send(f"{ctx.author.mention}, you haven't earned any XP yet.")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Error creating profile card: {e}")
|
||||
logging.error(f"Error creating profile card: {e}")
|
||||
|
||||
async def create_profile_card(
|
||||
self, user: discord.Member | discord.User, background_image_path: str
|
||||
) -> io.BytesIO:
|
||||
"""Create a profile card image for a user with a custom background and text overlay."""
|
||||
card_width, card_height = 400, 200
|
||||
text_color = (255, 255, 255)
|
||||
progress_bar_color = (0, 255, 0)
|
||||
outline_color = (255, 255, 255)
|
||||
tint_color = (50, 50, 50)
|
||||
transparency = 25
|
||||
opacity = int(255 * transparency / 100)
|
||||
|
||||
# Load and resize the background image
|
||||
try:
|
||||
background_image = Image.open(background_image_path).resize(
|
||||
(card_width, card_height)
|
||||
)
|
||||
except Exception:
|
||||
# Fallback to a plain background if image fails
|
||||
# type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
background_image = Image.new(
|
||||
"RGBA", (card_width, card_height), (30, 30, 30, 255) # type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
)
|
||||
|
||||
card_image = Image.new("RGBA", (card_width, card_height))
|
||||
card_image.paste(background_image, (0, 0))
|
||||
|
||||
overlay = Image.new(
|
||||
"RGBA",
|
||||
card_image.size,
|
||||
tint_color + (opacity,), # type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
)
|
||||
card_image = Image.alpha_composite(card_image, overlay)
|
||||
|
||||
draw = ImageDraw.Draw(card_image)
|
||||
|
||||
# Load fonts with fallback
|
||||
font_path = "arial.ttf"
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, 20)
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
draw.text((10, 10), f"User: {user.display_name}", fill=text_color, font=font)
|
||||
user_info = self.db.fetch_one(
|
||||
"SELECT XP, LEVEL FROM users WHERE ID = %s", (user.id,)
|
||||
)
|
||||
draw.text(
|
||||
(10, 40), f"Level: {user_info.get('LEVEL', 0)}", fill=text_color, font=font
|
||||
)
|
||||
draw.text(
|
||||
(10, 70),
|
||||
f"XP: {user_info.get('XP', 0)}/{calculate_xp_needed_for_next_level(user_info.get('LEVEL', 0))}",
|
||||
fill=text_color,
|
||||
font=font,
|
||||
)
|
||||
draw.text(
|
||||
(10, 100),
|
||||
f"Next level in: {calculate_xp_needed_for_next_level(user_info.get('LEVEL', 0))-user_info.get('XP', 0)}XP",
|
||||
fill=text_color,
|
||||
font=font,
|
||||
)
|
||||
|
||||
max_xp = calculate_xp_needed_for_next_level(user_info.get("LEVEL", 0))
|
||||
progress_ratio = user_info.get("XP", 0) / max_xp if max_xp > 0 else 0
|
||||
progress_width = progress_ratio * 200
|
||||
|
||||
draw.rectangle([(10, 130), (210, 150)], outline=outline_color, width=2)
|
||||
draw.rectangle([(10, 130), (10 + progress_width, 150)], fill=progress_bar_color)
|
||||
|
||||
try:
|
||||
profile_size = 96
|
||||
margin = 30
|
||||
if user.avatar is not None:
|
||||
profile_picture_data = await user.avatar.read()
|
||||
profile_image = Image.open(io.BytesIO(profile_picture_data)).resize(
|
||||
(profile_size, profile_size)
|
||||
)
|
||||
else:
|
||||
# Use a default avatar image if user has no avatar
|
||||
default_avatar_path = "images/default_avatar.png"
|
||||
if os.path.exists(default_avatar_path):
|
||||
profile_image = Image.open(default_avatar_path).resize(
|
||||
(profile_size, profile_size)
|
||||
)
|
||||
else:
|
||||
# Create a blank image if default not found
|
||||
# type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
profile_image = Image.new(
|
||||
"RGBA", (profile_size, profile_size), (100, 100, 100, 255) # type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
)
|
||||
card_image.paste(
|
||||
profile_image,
|
||||
(card_width - profile_size - margin, margin),
|
||||
profile_image.convert("RGBA"),
|
||||
)
|
||||
except Exception as error:
|
||||
logging.error(f"Error fetching profile picture: {error}")
|
||||
|
||||
image_bytes = io.BytesIO()
|
||||
card_image.save(image_bytes, format="PNG")
|
||||
image_bytes.seek(0)
|
||||
|
||||
return image_bytes
|
||||
|
||||
|
||||
async def setup(client):
|
||||
await client.add_cog(XP(client))
|
||||
Reference in New Issue
Block a user