diff --git a/blue_archive/inventory.py b/blue_archive/inventory.py index 443ab1d..c5f6e11 100644 --- a/blue_archive/inventory.py +++ b/blue_archive/inventory.py @@ -1,5 +1,5 @@ -#this is the inventory cog for viewing collections and eligma balance## -"""Collection and eligma commands for the Blue Archive gacha system.""" +#this is the inventory cog — interactive collection browser with buttons## +"""Interactive collection browser for the Blue Archive gacha system.""" ###################################################################### import logging @@ -11,26 +11,232 @@ from .data import db log = logging.getLogger(__name__) +PAGE_SIZE = 10 +RARITY_ICON = {3: "<:purple:>", 2: "<:gold:>", 1: "<:blue:>"} +RARITY_SYMBOL = {3: "◆", 2: "⬟", 1: "●"} +RARITY_LABEL = {3: "3★", 2: "2★", 1: "1★"} +RARITY_COLOR = {3: 0xE6B8FF, 2: 0xFFF58C, 1: 0xAAD4FF} ELIGMA_YIELD = {1: 1, 2: 10, 3: 50} +# Pool totals for progress bars (standard pool only) +POOL_TOTALS = {3: 106, 2: 22, 1: 11} + +PAGE_LEFT = "◀" +PAGE_RIGHT = "▶" +FILTER_ALL = "All" +FILTER_LABELS = {FILTER_ALL: None, "3★": 3, "2★": 2, "1★": 1} + + +def _bar(current: int, total: int, width: int = 8) -> str: + """Unicode progress bar: ██████░░ 75%""" + if total == 0: + return "░" * width + " 0%" + pct = min(current / total, 1.0) + filled = int(pct * width) + return "█" * filled + "░" * (width - filled) + f" {int(pct * 100)}%" + + +class InventoryView(discord.ui.View): + """Interactive view with buttons for collection browsing.""" + + def __init__(self, uid: int, owner_id: int): + super().__init__(timeout=120) + self.uid = uid + self.owner_id = owner_id + self.page = 0 + self.filter_rarity: int | None = None + self._update_buttons() + + # ── helpers ────────────────────────────────────────────────────────── + + def _get_rows(self) -> list[tuple[int, str, int, int]]: + """Return [(student_id, name, count, rarity), ...] filtered and sorted.""" + rows = gacha_db.get_inventory(self.uid) + result = [] + for sid, count in rows: + s = db.get(sid) + if s: + r = s["StarGrade"] + if self.filter_rarity and r != self.filter_rarity: + continue + result.append((sid, s["Name"], count, r)) + result.sort(key=lambda x: (-x[3], x[1])) + return result + + def _build_dashboard(self) -> discord.Embed: + """Landing page with stats and progress bars.""" + stats = gacha_db.get_inventory_stats(self.uid) + eligma = gacha_db.get_eligma(self.uid) + + # Count by rarity + rows = gacha_db.get_inventory(self.uid) + by_rarity = {1: 0, 2: 0, 3: 0} + for sid, _ in rows: + s = db.get(sid) + if s: + by_rarity[s["StarGrade"]] = by_rarity.get(s["StarGrade"], 0) + 1 + + embed = discord.Embed( + title="📋 Collection Dashboard", + description=f"**{stats['unique']}** unique students from **{stats['total_pulls']}** total pulls", + color=0x5BA0D0, + ) + + # Progress bars per rarity + for r in (3, 2, 1): + pct = _bar(by_rarity[r], POOL_TOTALS[r]) + embed.add_field( + name=f"{RARITY_LABEL[r]} {by_rarity[r]}/{POOL_TOTALS[r]}", + value=f"`{pct}`", + inline=True, + ) + + embed.add_field( + name="Eligma", + value=f"**{eligma}**", + inline=True, + ) + + embed.set_footer(text="Use buttons below to browse") + return embed + + def _build_collection_page(self) -> discord.Embed: + """Paginated student list.""" + all_rows = self._get_rows() + total_pages = max(1, (len(all_rows) + PAGE_SIZE - 1) // PAGE_SIZE) + self.page = min(self.page, total_pages - 1) + + start = self.page * PAGE_SIZE + chunk = all_rows[start:start + PAGE_SIZE] + + rarity_label = RARITY_LABEL.get(self.filter_rarity, "All") + embed = discord.Embed( + title=f"Collection — {rarity_label} ({len(all_rows)} students)", + color=0x5BA0D0, + ) + + if not chunk: + embed.description = "No students in this category yet. Use `!pull` to recruit!" + return embed + + lines = [] + for sid, name, count, rarity in chunk: + symbol = RARITY_SYMBOL[rarity] + dupes = f" ×{count}" if count > 1 else "" + lines.append(f"{symbol} {name}{dupes}") + + embed.description = "\n".join(lines) + embed.set_footer(text=f"Page {self.page + 1}/{total_pages}") + return embed + + def _build_eligma_page(self) -> discord.Embed: + amount = gacha_db.get_eligma(self.uid) + return discord.Embed( + title="Eligma", + description=( + f"**Balance: {amount}**\n\n" + "Earned from duplicates:\n" + f"{RARITY_SYMBOL[1]} 1★ dupe = {ELIGMA_YIELD[1]} Eligma\n" + f"{RARITY_SYMBOL[2]} 2★ dupe = {ELIGMA_YIELD[2]} Eligma\n" + f"{RARITY_SYMBOL[3]} 3★ dupe = {ELIGMA_YIELD[3]} Eligma" + ), + color=0x5BA0D0, + ) + + # ── buttons ─────────────────────────────────────────────────────────── + + def _update_buttons(self) -> None: + self.clear_items() + + # Navigation row: dashboard, collection, eligma + dash = discord.ui.Button(label="Dashboard", style=discord.ButtonStyle.gray, row=0, custom_id="dash") + dash.callback = self._on_dash + self.add_item(dash) + + coll = discord.ui.Button(label="Collection", style=discord.ButtonStyle.blurple, row=0, custom_id="coll") + coll.callback = self._on_coll + self.add_item(coll) + + elig = discord.ui.Button(label="Eligma", style=discord.ButtonStyle.green, row=0, custom_id="elig") + elig.callback = self._on_elig + self.add_item(elig) + + # Filter row: All, 3★, 2★, 1★ + for label in (FILTER_ALL, "3★", "2★", "1★"): + style = discord.ButtonStyle.gray + if FILTER_LABELS[label] == self.filter_rarity: + style = discord.ButtonStyle.primary + elif label == FILTER_ALL and self.filter_rarity is None: + style = discord.ButtonStyle.primary + btn = discord.ui.Button(label=label, style=style, row=1, custom_id=f"f_{label}") + btn.callback = self._on_filter + self.add_item(btn) + + # Page row + left = discord.ui.Button(label=PAGE_LEFT, style=discord.ButtonStyle.gray, row=2, custom_id="prev") + left.callback = self._on_prev + self.add_item(left) + + right = discord.ui.Button(label=PAGE_RIGHT, style=discord.ButtonStyle.gray, row=2, custom_id="next") + right.callback = self._on_next + self.add_item(right) + + async def _on_dash(self, interaction: discord.Interaction) -> None: + self.page = 0 + self._update_buttons() + await interaction.response.edit_message(embed=self._build_dashboard(), view=self) + + async def _on_coll(self, interaction: discord.Interaction) -> None: + self.page = 0 + self._update_buttons() + await interaction.response.edit_message(embed=self._build_collection_page(), view=self) + + async def _on_elig(self, interaction: discord.Interaction) -> None: + self._update_buttons() + await interaction.response.edit_message(embed=self._build_eligma_page(), view=self) + + async def _on_filter(self, interaction: discord.Interaction) -> None: + label = interaction.data["custom_id"][2:] # strip "f_" + self.filter_rarity = FILTER_LABELS.get(label) + self.page = 0 + self._update_buttons() + await interaction.response.edit_message(embed=self._build_collection_page(), view=self) + + async def _on_prev(self, interaction: discord.Interaction) -> None: + self.page = max(0, self.page - 1) + self._update_buttons() + await interaction.response.edit_message(embed=self._build_collection_page(), view=self) + + async def _on_next(self, interaction: discord.Interaction) -> None: + all_rows = self._get_rows() + max_page = max(0, (len(all_rows) - 1) // PAGE_SIZE) + self.page = min(max_page, self.page + 1) + self._update_buttons() + await interaction.response.edit_message(embed=self._build_collection_page(), view=self) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user.id != self.owner_id: + await interaction.response.send_message( + "Only the person who ran this command can use the buttons. Run `!inv` yourself!", + ephemeral=True, + ) + return False + return True + class Inventory(commands.Cog): - """View your student collection and eligma from gacha pulls.""" + """Interactive collection browser for Blue Archive gacha.""" def __init__(self, bot: commands.Bot) -> None: self.bot = bot @commands.group(name="inv", aliases=["collection", "i"], invoke_without_command=True) async def inventory(self, ctx: commands.Context, *, target: str = "") -> None: - """View your student collection grouped by rarity. + """Open your interactive collection dashboard. - Usage: - !inv — your collection - !inv @user — someone else's - !inv 3star — filter to 3★ only - !inv stats — summary stats + Usage: !inv — your collection with buttons + !inv @user — someone else's (read-only) """ - # Determine whose collection to show if target and target.startswith("<@"): try: uid = int(target.strip("<@!>")) @@ -39,14 +245,6 @@ class Inventory(commands.Cog): else: uid = ctx.author.id - filter_rarity = None - if target in ("3star", "3", "3*", "3star"): - filter_rarity = 3 - elif target in ("2star", "2", "2*", "2star"): - filter_rarity = 2 - elif target in ("1star", "1", "1*", "1star"): - filter_rarity = 1 - rows = gacha_db.get_inventory(uid) if not rows: if uid == ctx.author.id: @@ -55,101 +253,8 @@ class Inventory(commands.Cog): await ctx.reply("This user's collection is empty.") return - # Group by rarity - by_rarity: dict[int, list[tuple[int, str, int]]] = {3: [], 2: [], 1: []} - for sid, count in rows: - s = db.get(sid) - if s: - r = s["StarGrade"] - by_rarity.get(r, []).append((sid, s["Name"], count)) - - # Build embed - title = f"Collection of {self.bot.get_user(uid).display_name if self.bot.get_user(uid) else 'User'}" - embed = discord.Embed(title=title, color=0x5BA0D0) - - for rarity, label, em_color in [ - (3, "3-star", 0xF0C0FF), - (2, "2-star", 0xFFF588), - (1, "1-star", 0xAADCFF), - ]: - if filter_rarity and rarity != filter_rarity: - continue - chars = sorted(by_rarity[rarity], key=lambda x: -x[2]) - if not chars: - continue - total_unique = len(chars) - total_dupes = sum(c[2] - 1 for c in chars) - header = f"{label} — {total_unique} unique" - if total_dupes: - header += f" (+{total_dupes} dupes)" - - # Format: "Aru x3, Hina x1, ..." - lines = [] - for _, name, count in chars: - lines.append(f"{name}" + (f" x{count}" if count > 1 else "")) - - # Split into chunks of ~10 per embed field line - text = ", ".join(lines) - if len(text) > 1024: - text = text[:1021] + "..." - embed.add_field(name=header, value=text, inline=False) - - # Footer stats - stats = gacha_db.get_inventory_stats(uid) - eligma = gacha_db.get_eligma(uid) - embed.set_footer( - text=f"{stats['unique']} unique · {stats['total_pulls']} total pulls · {eligma} Eligma" - ) - - await ctx.reply(embed=embed) - - @inventory.command(name="stats") - async def inv_stats(self, ctx: commands.Context) -> None: - """Show your collection summary stats.""" - uid = ctx.author.id - stats = gacha_db.get_inventory_stats(uid) - eligma = gacha_db.get_eligma(uid) - - # Count by rarity - rows = gacha_db.get_inventory(uid) - by_rarity = {1: 0, 2: 0, 3: 0} - for sid, _ in rows: - s = db.get(sid) - if s: - by_rarity[s["StarGrade"]] = by_rarity.get(s["StarGrade"], 0) + 1 - - embed = discord.Embed(title="Collection Stats", color=0x5BA0D0) - embed.add_field(name="Total Pulls", value=str(stats["total_pulls"]), inline=True) - embed.add_field(name="Unique Students", value=str(stats["unique"]), inline=True) - embed.add_field(name="Eligma", value=str(eligma), inline=True) - embed.add_field( - name="By Rarity", - value=f"3★: {by_rarity[3]} · 2★: {by_rarity[2]} · 1★: {by_rarity[1]}", - inline=False, - ) - - await ctx.reply(embed=embed) - - @commands.command(name="eligma", aliases=["elig"]) - async def eligma(self, ctx: commands.Context) -> None: - """Show your eligma balance and explain how it works.""" - uid = ctx.author.id - amount = gacha_db.get_eligma(uid) - - embed = discord.Embed( - title="Eligma", - description=( - f"**Balance: {amount}**\n\n" - "Eligma is earned from duplicate pulls:\n" - f"1★ dupe = {ELIGMA_YIELD[1]} Eligma\n" - f"2★ dupe = {ELIGMA_YIELD[2]} Eligma\n" - f"3★ dupe = {ELIGMA_YIELD[3]} Eligma\n\n" - "In the real game, Eligma is used to buy student shards " - "in the shop. (Shop coming soon.)" - ), - color=0x5BA0D0, - ) - await ctx.reply(embed=embed) + view = InventoryView(uid, ctx.author.id) + await ctx.reply(embed=view._build_dashboard(), view=view) async def setup(bot: commands.Bot) -> None: