diff --git a/utils/image_generator.py b/utils/image_generator.py index b310524..c851583 100644 --- a/utils/image_generator.py +++ b/utils/image_generator.py @@ -917,15 +917,15 @@ def draw_box( if tier in tier_avatars: avatars_to_draw = tier_avatars[tier][:3] - # On dessine la liste à l'envers pour que le #1 (index 0) soit dessiné en dernier - # et apparaisse donc visuellement "par-dessus" les autres + # We draw the list in reverse order so that #1 (index 0) is drawn last + # and thus appears visually "on top" of the others for idx, avatar in reversed(list(enumerate(avatars_to_draw))): - # Un écart de 14 pixels pour des images de 24px crée un chevauchement de 10px + # A 14-pixel difference for 24-pixel images results in a 10-pixel overlap avatar_x = int(x_pos + 235 + (idx * 14)) avatar_y = int(y_pos - 1) - # On dessine un cercle gris foncé (BOX_COLOR) derrière l'avatar pour créer - # une petite bordure qui "coupe" l'avatar en dessous et les détache proprement + # Draw a dark gray circle (BOX_COLOR) behind the avatar to create + # a small border that "cuts off" the bottom of the avatar and separates them neatly draw.ellipse( [avatar_x - 2, avatar_y - 2, avatar_x + 24 + 2, avatar_y + 24 + 2], fill=self.BOX_COLOR, @@ -1132,414 +1132,3 @@ def create_avatar_mask(avatar_img, avatar_size, avatar_x, avatar_y, img): mask = mask.resize((avatar_size, avatar_size), Image.Resampling.LANCZOS) avatar_img.putalpha(mask) img.paste(avatar_img, (avatar_x, avatar_y), avatar_img) - - -class ServerStatGenerator: - WIDTH: int = 800 - HEIGHT: int = 710 - HEADER_HEIGHT: int = 150 - - BG_COLOR = (25, 25, 25) - TEXT_COLOR = (255, 255, 255) - HEADER_BG_COLOR = (50, 50, 50) - BOX_COLOR = (35, 35, 35) - TITLE_COLOR = (200, 200, 200) - SUBTEXT_COLOR = (170, 170, 170) - - def __init__(self): - self.base_path = pathlib.Path(__file__).parent.resolve() / ".." / "ressources" - self._load_fonts() - - def _load_fonts(self): - font_file = self.base_path / "font" / "outfit.ttf" - try: - self.font_title = ImageFont.truetype(str(font_file), 45) - self.font_large = ImageFont.truetype(str(font_file), 38) - self.font_regular = ImageFont.truetype(str(font_file), 28) - self.font_small = ImageFont.truetype(str(font_file), 22) - self.font_tiny = ImageFont.truetype(str(font_file), 18) - except IOError: - self.font_title = ImageFont.load_default() - self.font_large = ImageFont.load_default() - self.font_regular = ImageFont.load_default() - self.font_small = ImageFont.load_default() - self.font_tiny = ImageFont.load_default() - - async def generate_server_stat(self, guild: discord.Guild, stats: dict): - img = Image.new("RGB", (self.WIDTH, self.HEIGHT), self.BG_COLOR) - draw = ImageDraw.Draw(img) - - draw.rectangle([0, 0, self.WIDTH, self.HEADER_HEIGHT], fill=self.HEADER_BG_COLOR) - - icon_x, icon_y, icon_size = 40, 25, 100 - try: - if guild.icon: - icon_data = await guild.icon.read() - icon_img = ( - Image.open(BytesIO(icon_data)).resize((icon_size, icon_size)).convert("RGBA") - ) - else: - raise Exception() - self.create_avatar_mask(icon_img, icon_size, icon_x, icon_y, img) - except Exception: - default_icon = Image.new("RGBA", (icon_size, icon_size), (120, 120, 120, 255)) - self.create_avatar_mask(default_icon, icon_size, icon_x, icon_y, img) - - draw.text((170, 45), "RNGdle - Server Stats", fill=self.TEXT_COLOR, font=self.font_title) - - async def fetch_avatar(member, size): - if member: - try: - data = await member.display_avatar.read() - return Image.open(BytesIO(data)).resize((size, size)).convert("RGBA") - except: - pass - return Image.new("RGBA", (size, size), (120, 120, 120, 255)) - - best_avatar = await fetch_avatar(stats["best_roll"].get("member"), 32) - worst_avatar = await fetch_avatar(stats["worst_roll"].get("member"), 32) - - tier_avatars = {} - for t, member in stats.get("tier_members", {}).items(): - if member: - tier_avatars[t] = await fetch_avatar(member, 24) - - def draw_box( - x, - y, - w, - h, - title, - value, - subtext=None, - value_color=None, - outline_color=None, - score_suffix=None, - avatar=None, - ): - if value_color is None: - value_color = self.TEXT_COLOR - - if outline_color: - draw.rounded_rectangle( - [x, y, x + w, y + h], - radius=12, - fill=self.BOX_COLOR, - outline=outline_color, - width=2, - ) - else: - draw.rounded_rectangle([x, y, x + w, y + h], radius=12, fill=self.BOX_COLOR) - - draw.text((x + 20, y + 15), title, fill=self.TITLE_COLOR, font=self.font_small) - - if avatar: - av_x = int(x + w - avatar.width - 15) - av_y = int(y + 10) - self.create_avatar_mask(avatar, avatar.width, av_x, av_y, img) - - draw.text((x + 20, y + 45), value, fill=value_color, font=self.font_regular) - - if score_suffix: - val_width = draw.textlength(value + " ", font=self.font_regular) - draw.text( - (x + 20 + val_width, y + 45), - score_suffix, - fill=(215, 215, 215), - font=self.font_regular, - ) - - if subtext: - draw.text((x + 20, y + 80), subtext, fill=self.SUBTEXT_COLOR, font=self.font_tiny) - - best_color = get_tier_color(get_score_tier(stats["best_roll"]["score"])) - worst_color = get_tier_color(get_score_tier(stats["worst_roll"]["score"])) - - best_number_str = f"{stats['best_roll']['number']:,}".replace(",", " ") - best_score_str = f"({stats['best_roll']['score']:,} EP)".replace(",", " ") - - worst_number_str = f"{stats['worst_roll']['number']:,}".replace(",", " ") - worst_score_str = f"({stats['worst_roll']['score']:,} EP)".replace(",", " ") - - draw_box( - 40, - 180, - 345, - 110, - "Best Roll OAT", - best_number_str, - f"by {stats['best_roll']['user']}", - value_color=best_color, - outline_color=best_color, - score_suffix=best_score_str, - avatar=best_avatar, - ) - draw_box( - 415, - 180, - 345, - 110, - "Worst Roll OAT", - worst_number_str, - f"by {stats['worst_roll']['user']}", - value_color=worst_color, - outline_color=worst_color, - score_suffix=worst_score_str, - avatar=worst_avatar, - ) - - box_w = 226 - avg_color = get_tier_color(get_score_tier(stats["avg_score"])) - - draw_box(40, 310, box_w, 110, "Total Rolls", f"{stats['total_rolls']:,}".replace(",", " ")) - draw_box( - 286, - 310, - box_w, - 110, - "Average Score", - f"{stats['avg_score']:,} EP".replace(",", " "), - value_color=avg_color, - outline_color=avg_color, - ) - draw_box( - 532, - 310, - box_w, - 110, - "Overall Score", - f"{stats['overall_score']:,} EP".replace(",", " "), - ) - - draw.rounded_rectangle([40, 440, 760, 680], radius=12, fill=self.BOX_COLOR) - draw.text((60, 455), "Tier Breakdown", fill=self.TITLE_COLOR, font=self.font_small) - - tiers = ["MYTHIC", "ANOMALY", "EPIC", "RARE", "UNCOMMON", "COMMON", "TRASH"] - col_x = [80, 440] - start_y = 500 - - for i, tier in enumerate(tiers): - count = stats["rarities"].get(tier, 0) - color = get_tier_color(Tier[tier]) - - col_index = 0 if i < 4 else 1 - row_index = i if i < 4 else i - 4 - - x_pos = col_x[col_index] - y_pos = start_y + (45 * row_index) - - draw.text((x_pos, y_pos), f"{tier}:", fill=color, font=self.font_small) - - count_str = f"{count:,}".replace(",", " ") - count_width = draw.textlength(count_str, font=self.font_small) - draw.text( - (x_pos + 220 - count_width, y_pos), - count_str, - fill=self.TEXT_COLOR, - font=self.font_small, - ) - - if tier in tier_avatars: - self.create_avatar_mask( - tier_avatars[tier], 24, int(x_pos + 235), int(y_pos - 1), img - ) - - return img - - @staticmethod - def create_avatar_mask(avatar_img, avatar_size, avatar_x, avatar_y, img): - mask = Image.new("L", (avatar_size * 4, avatar_size * 4), 0) - draw_mask = ImageDraw.Draw(mask) - draw_mask.ellipse((0, 0, avatar_size * 4, avatar_size * 4), fill=255) - mask = mask.resize((avatar_size, avatar_size), Image.Resampling.LANCZOS) - avatar_img.putalpha(mask) - img.paste(avatar_img, (avatar_x, avatar_y), avatar_img) - - -class OverallLeaderboardGenerator: - WIDTH: int = 800 - ROW_HEIGHT: int = 90 - HEADER_HEIGHT: int = 100 - - BG_COLOR = (25, 25, 25) - TEXT_COLOR = (255, 255, 255) - HEADER_BG_COLOR = (50, 50, 50) - ROW_EVEN_COLOR = (35, 35, 35) - ROW_ODD_COLOR = (45, 45, 45) - - def __init__(self): - self.base_path: pathlib.Path = pathlib.Path(__file__).parent.resolve() / ".." / "ressources" - self._load_fonts() - self._load_images() - - def _load_fonts(self): - font_file = self.base_path / "font" / "outfit.ttf" - try: - self.font_header = ImageFont.truetype(str(font_file), 40) - self.font_regular = ImageFont.truetype(str(font_file), 30) - except IOError: - self.font_header = ImageFont.load_default() - self.font_regular = ImageFont.load_default() - - def _load_images(self): - try: - img_dir = self.base_path / "images" - self.PODIUM_BRONZE = ( - Image.open(str(img_dir / "medal_bronze.png")).convert("RGBA").resize((50, 50)) - ) - self.PODIUM_SILVER = ( - Image.open(str(img_dir / "medal_silver.png")).convert("RGBA").resize((50, 50)) - ) - self.PODIUM_GOLD = ( - Image.open(str(img_dir / "medal_gold.png")).convert("RGBA").resize((50, 50)) - ) - except Exception: - self.PODIUM_BRONZE = None - self.PODIUM_SILVER = None - self.PODIUM_GOLD = None - - @staticmethod - def format_short_number(num: int) -> str: - if num >= 1_000_000_000: - formatted = f"{num / 1_000_000_000:.1f}" - return f"{formatted.replace('.0', '')}Md" - elif num >= 1_000_000: - formatted = f"{num / 1_000_000:.1f}" - return f"{formatted.replace('.0', '')}M" - elif num >= 1_000: - formatted = f"{num / 1_000:.1f}" - return f"{formatted.replace('.0', '')}k" - else: - return str(num) - - def _draw_fitted( - self, - draw: ImageDraw.ImageDraw, - text: str, - x: float, - y: float, - max_width: float, - base_font, - fill, - anchor="lt", - ): - font = base_font - font_path = getattr(font, "path", None) - font_size = getattr(font, "size", 30) - - if font_path: - while draw.textlength(text, font=font) > max_width and font_size > 1: - font_size -= 1 - font = ImageFont.truetype(font_path, font_size) - - draw.text((x, y), text, fill=fill, font=font, anchor=anchor) - - async def _draw_user_row(self, draw, img, data, rank, y_pos, row_color): - draw.rectangle([0, y_pos, self.WIDTH, y_pos + self.ROW_HEIGHT], fill=row_color) - - rank_text_x = 30 - rank_text_y = y_pos + (self.ROW_HEIGHT / 2) - 15 - - if rank == 1 and self.PODIUM_GOLD: - img.paste(self.PODIUM_GOLD, (rank_text_x - 15, int(rank_text_y - 10)), self.PODIUM_GOLD) - elif rank == 2 and self.PODIUM_SILVER: - img.paste( - self.PODIUM_SILVER, (rank_text_x - 15, int(rank_text_y - 10)), self.PODIUM_SILVER - ) - elif rank == 3 and self.PODIUM_BRONZE: - img.paste( - self.PODIUM_BRONZE, (rank_text_x - 15, int(rank_text_y - 10)), self.PODIUM_BRONZE - ) - else: - draw.text( - (rank_text_x, rank_text_y), str(rank), fill=self.TEXT_COLOR, font=self.font_regular - ) - - avatar_x = 100 - avatar_y = y_pos + 15 - avatar_size = 60 - - try: - if data["discord_user"]: - avatar_data = await data["discord_user"].display_avatar.read() - avatar_img = ( - Image.open(BytesIO(avatar_data)) - .resize((avatar_size, avatar_size)) - .convert("RGBA") - ) - else: - raise Exception() - self.create_avatar_mask(avatar_img, avatar_size, avatar_x, avatar_y, img) - except Exception: - default_avatar = Image.new("RGBA", (avatar_size, avatar_size), (120, 120, 120, 255)) - self.create_avatar_mask(default_avatar, avatar_size, avatar_x, avatar_y, img) - - username_x = 190 - username_y = y_pos + (self.ROW_HEIGHT / 2) - 15 - display_name = ( - data["discord_user"].name if data["discord_user"] else data["rngdle_username"] - ) - self._draw_fitted( - draw, display_name, username_x, username_y, 320, self.font_regular, self.TEXT_COLOR - ) - - short_score = self.format_short_number(data["total_score"]) - score_str = f"{short_score} EP" - - score_x = 770 - score_y = y_pos + (self.ROW_HEIGHT / 2) - 15 - self._draw_fitted( - draw, score_str, score_x, score_y, 250, self.font_regular, (251, 251, 251), anchor="rt" - ) - - async def generate_leaderboard( - self, users_data: list[dict], start_rank: int = 1, caller_info: dict = None - ): - GAP = 20 - base_rows = len(users_data) - total_height = self.HEADER_HEIGHT + (base_rows * self.ROW_HEIGHT) - - if caller_info: - total_height += self.ROW_HEIGHT + GAP - - img = Image.new("RGB", (self.WIDTH, total_height), self.BG_COLOR) - draw = ImageDraw.Draw(img) - - draw.rectangle([0, 0, self.WIDTH, self.HEADER_HEIGHT], fill=self.HEADER_BG_COLOR) - - headers = ["Rang", "Pseudo", "Overall Score"] - x_offsets = [15, 190, 770] - anchors = ["lt", "lt", "rt"] - - for i, header in enumerate(headers): - x, y = x_offsets[i], self.HEADER_HEIGHT / 2 - 15 - draw.text( - (x, y), header, fill=self.TEXT_COLOR, font=self.font_regular, anchor=anchors[i] - ) - - for index, data in enumerate(users_data): - y_pos = self.HEADER_HEIGHT + (index * self.ROW_HEIGHT) - row_color = self.ROW_EVEN_COLOR if index % 2 == 0 else self.ROW_ODD_COLOR - await self._draw_user_row(draw, img, data, start_rank + index, y_pos, row_color) - - if caller_info: - base_y = self.HEADER_HEIGHT + (base_rows * self.ROW_HEIGHT) - line_y = base_y + (GAP // 2) - - draw.line([(0, line_y), (self.WIDTH, line_y)], fill=(80, 80, 80), width=2) - - caller_y = base_y + GAP - await self._draw_user_row( - draw, img, caller_info["data"], caller_info["rank"], caller_y, self.ROW_EVEN_COLOR - ) - - return img - - @staticmethod - def create_avatar_mask(avatar_img, avatar_size, avatar_x, avatar_y, img): - mask = Image.new("L", (avatar_size * 4, avatar_size * 4), 0) - draw_mask = ImageDraw.Draw(mask) - draw_mask.ellipse((0, 0, avatar_size * 4, avatar_size * 4), fill=255) - mask = mask.resize((avatar_size, avatar_size), Image.Resampling.LANCZOS) - avatar_img.putalpha(mask) - img.paste(avatar_img, (avatar_x, avatar_y), avatar_img)