# cogs/inventory.py import discord from discord import app_commands from discord.ext import commands import math import logging from database.db import db from utils.code_converter import encode_id, decode_id from utils.cdn import get_or_create_card_url logger = logging.getLogger(__name__) ITEMS_PER_PAGE = 10 # ========================================================== # šŸ” CORE INSPECT LOGIC (Used by Dropdown & Command) # ========================================================== async def handle_card_inspect(interaction: discord.Interaction, instance_id: int, ephemeral: bool = False): card_code = encode_id(instance_id) async with db.pool.acquire() as conn: char_data = await conn.fetchrow(""" SELECT uc.user_id, uc.level, uc.locked, uc.obtained_at, uc.print_num, uc.constellation, u.username AS owner_name, c.name, c.rarity, c.element, c.series, c.atk, c.def, c.image_url, c.c6_gif_url FROM user_characters uc JOIN characters c ON uc.char_id = c.char_id JOIN users u ON uc.user_id = u.user_id WHERE uc.instance_id = $1 """, instance_id) if not char_data: return await interaction.followup.send(f"āŒ Could not find a character with code `{card_code}`.", ephemeral=ephemeral) c6_star = "" # C6 Awakened GIF Bypass if char_data['constellation'] >= 6 and char_data['c6_gif_url']: image_url = char_data['c6_gif_url'] c6_star = "🌟 [AWAKENED] " else: # Calls your new factory seamlessly! image_url = await get_or_create_card_url( bot=interaction.client, instance_id=instance_id, image_url=char_data['image_url'], char_name=char_data['name'], print_num=char_data['print_num'], rarity=char_data['rarity'], element=char_data['element'] ) embed = discord.Embed( title=f"{c6_star}{char_data['name']} (Lvl. {char_data['level']} | C{char_data['constellation']})", color=discord.Color.blurple() ) embed.add_field(name="šŸ‘¤ Owner", value=f"<@{char_data['user_id']}>", inline=False) embed.add_field(name="Rarity", value=char_data['rarity'], inline=True) embed.add_field(name="Element", value=char_data['element'] or "None", inline=True) embed.add_field(name="Series", value=char_data['series'] or "Unknown", inline=True) embed.add_field(name="Card Code", value=f"`{card_code}`", inline=True) # šŸ”„ FIXED: Actually scales the stats displayed based on their current level! level_mult = 1.0 + ((char_data['level'] - 1) * 0.15) f_atk = int(char_data['atk'] * level_mult) f_def = int(char_data['def'] * level_mult) embed.add_field(name="Stats", value=f"āš”ļø {f_atk} | šŸ›”ļø {f_def}", inline=True) embed.set_image(url=image_url) await interaction.followup.send(embed=embed, ephemeral=ephemeral) # ========================================================== # šŸ–±ļø DYNAMIC DROPDOWN MENU # ========================================================== class CardInspectSelect(discord.ui.Select): def __init__(self, options): super().__init__(placeholder="šŸ” Inspect a card from this page...", min_values=1, max_values=1, options=options) async def callback(self, interaction: discord.Interaction): # We defer ephemerally so the giant images don't clutter up the main channel! await interaction.response.defer(ephemeral=True) instance_id = int(self.values[0]) await handle_card_inspect(interaction, instance_id, ephemeral=True) # ========================================================== # šŸŽ’ INVENTORY PAGINATOR # ========================================================== class InventoryPaginator(discord.ui.View): def __init__(self, user_id: int, total_pages: int, series_filter: str = None): super().__init__(timeout=120) self.user_id = user_id self.current_page = 1 self.total_pages = max(1, total_pages) self.series_filter = series_filter self.select_menu = None async def get_page_data(self) -> tuple[discord.Embed, list[discord.SelectOption]]: offset = (self.current_page - 1) * ITEMS_PER_PAGE query = """ SELECT uc.instance_id, c.name, c.rarity, uc.level, uc.locked, c.series, uc.constellation FROM user_characters uc JOIN characters c ON uc.char_id = c.char_id WHERE uc.user_id = $1 """ args = [self.user_id] if self.series_filter: query += " AND c.series ILIKE $2" args.append(f"%{self.series_filter}%") query += f" ORDER BY uc.obtained_at DESC LIMIT {ITEMS_PER_PAGE} OFFSET {offset}" async with db.pool.acquire() as conn: rows = await conn.fetch(query, *args) title = f"šŸŽ’ Inventory ({self.series_filter})" if self.series_filter else "šŸŽ’ Your Character Inventory" embed = discord.Embed(title=title, color=discord.Color.blurple()) desc = "" options = [] for row in rows: card_code = encode_id(row['instance_id']) lock = "šŸ”’" if row['locked'] else "šŸ”“" series_tag = f"[{row['series']}]" if row['series'] else "[Unknown Series]" desc += f"`{card_code}` | **{row['name']}** ({row['rarity']}) - Lvl {row['level']} | C{row['constellation']} {lock}\nā”” *{series_tag}*\n" # Populate the Dropdown options securely opt_label = f"{row['name']} (Lvl {row['level']})"[:100] options.append(discord.SelectOption( label=opt_label, description=f"{row['rarity']} | Code: {card_code}", value=str(row['instance_id']) )) embed.description = desc if desc else "No characters found matching this criteria." embed.set_footer(text=f"Page {self.current_page} of {self.total_pages}") return embed, options async def refresh_ui(self, interaction: discord.Interaction = None): embed, options = await self.get_page_data() # Destroy the old dropdown and load the new one for the current page if self.select_menu: self.remove_item(self.select_menu) if options: self.select_menu = CardInspectSelect(options) self.add_item(self.select_menu) self.prev_button.disabled = self.current_page <= 1 self.next_button.disabled = self.current_page >= self.total_pages if interaction: await interaction.response.edit_message(embed=embed, view=self) return embed @discord.ui.button(label="ā—€ļø Prev", style=discord.ButtonStyle.secondary, custom_id="prev") async def prev_button(self, interaction: discord.Interaction, button: discord.ui.Button): if interaction.user.id != self.user_id: return await interaction.response.send_message("Not your inventory!", ephemeral=True) self.current_page -= 1 await self.refresh_ui(interaction) @discord.ui.button(label="Next ā–¶ļø", style=discord.ButtonStyle.secondary, custom_id="next") async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): if interaction.user.id != self.user_id: return await interaction.response.send_message("Not your inventory!", ephemeral=True) self.current_page += 1 await self.refresh_ui(interaction) class InventoryCog(commands.Cog): def __init__(self, bot): self.bot = bot @app_commands.command(name="inventory", description="View your collected characters") @app_commands.describe(series="Filter by Series (e.g. Genshin Impact)") async def inventory(self, interaction: discord.Interaction, series: str = None): await interaction.response.defer(thinking=True) user_id = interaction.user.id async with db.pool.acquire() as conn: if series: total_items = await conn.fetchval("SELECT COUNT(*) FROM user_characters uc JOIN characters c ON uc.char_id = c.char_id WHERE uc.user_id = $1 AND c.series ILIKE $2", user_id, f"%{series}%") else: total_items = await conn.fetchval("SELECT COUNT(*) FROM user_characters WHERE user_id = $1", user_id) if total_items == 0: if series: return await interaction.followup.send(f"You don't own any cards from **{series}**.") return await interaction.followup.send("Your inventory is completely empty.") total_pages = math.ceil(total_items / ITEMS_PER_PAGE) view = InventoryPaginator(user_id, total_pages, series) # Build the UI on the first run embed = await view.refresh_ui() await interaction.followup.send(embed=embed, view=view) su = app_commands.Group(name="su", description="Sueni exclusive commands") @su.command(name="card", description="View all your cards, or filter by a specific series") @app_commands.describe(series="Filter by Series (e.g. Genshin Impact)") async def su_card(self, interaction: discord.Interaction, series: str = None): await self.inventory.callback(self, interaction, series) @app_commands.command(name="suinfo", description="Inspect ANY character card by its code") @app_commands.describe(card_code="The alphanumeric code of the card") async def suinfo(self, interaction: discord.Interaction, card_code: str): await interaction.response.defer(thinking=True) try: instance_id = decode_id(card_code) except ValueError: return await interaction.followup.send("āŒ Invalid card code format.") # Delegate directly to our newly upgraded handler await handle_card_inspect(interaction, instance_id, ephemeral=False) async def setup(bot): await bot.add_cog(InventoryCog(bot))