# src/cogs/packs.py import random import discord from discord.ext import commands from typing import Optional, Dict, List, Tuple from .. import db # ---- Tunables (override via constants.py if you want) ---- try: from ..utils.constants import PACK_MIN_BET, PACK_SIZE, PACK_RARITY_WEIGHTS except Exception: PACK_MIN_BET = 50 PACK_SIZE = 5 # weights per rarity (sum doesn't need to be 1; we normalize) PACK_RARITY_WEIGHTS = { "Common": 600, "Uncommon": 250, "Rare": 100, "Epic": 40, "Legendary": 9, "Mythic": 1, } # ----------------------------------------------------------------------------- # Catalog: id, emoji, display name, rarity, multiplier (ร— bet) # Kept your original 25 items & IDs; expanded to 100 total. # ----------------------------------------------------------------------------- CATALOG: List[Dict] = [ # ---------------------- Common (40) ---------------------- {"id":"cherry","emoji":"๐Ÿ’","name":"Cherries","rarity":"Common","mult":0.10}, {"id":"coin","emoji":"๐Ÿช™","name":"Coin","rarity":"Common","mult":0.12}, {"id":"mug","emoji":"โ˜•","name":"Mug","rarity":"Common","mult":0.15}, {"id":"leaf","emoji":"๐ŸŒฟ","name":"Leaf","rarity":"Common","mult":0.18}, {"id":"sock","emoji":"๐Ÿงฆ","name":"Sock","rarity":"Common","mult":0.20}, {"id":"teddy","emoji":"๐Ÿงธ","name":"Teddy","rarity":"Common","mult":0.22}, {"id":"ice","emoji":"๐ŸงŠ","name":"Ice","rarity":"Common","mult":0.24}, {"id":"clover","emoji":"๐Ÿ€","name":"Clover","rarity":"Common","mult":0.25}, {"id":"apple","emoji":"๐ŸŽ","name":"Apple","rarity":"Common","mult":0.12}, {"id":"banana","emoji":"๐ŸŒ","name":"Banana","rarity":"Common","mult":0.14}, {"id":"pear","emoji":"๐Ÿ","name":"Pear","rarity":"Common","mult":0.13}, {"id":"peach","emoji":"๐Ÿ‘","name":"Peach","rarity":"Common","mult":0.16}, {"id":"strawberry","emoji":"๐Ÿ“","name":"Strawberry","rarity":"Common","mult":0.18}, {"id":"grapes","emoji":"๐Ÿ‡","name":"Grapes","rarity":"Common","mult":0.20}, {"id":"carrot","emoji":"๐Ÿฅ•","name":"Carrot","rarity":"Common","mult":0.12}, {"id":"corn","emoji":"๐ŸŒฝ","name":"Corn","rarity":"Common","mult":0.15}, {"id":"bread","emoji":"๐Ÿž","name":"Bread","rarity":"Common","mult":0.14}, {"id":"cheese","emoji":"๐Ÿง€","name":"Cheese","rarity":"Common","mult":0.18}, {"id":"egg","emoji":"๐Ÿฅš","name":"Egg","rarity":"Common","mult":0.16}, {"id":"milk","emoji":"๐Ÿฅ›","name":"Milk","rarity":"Common","mult":0.12}, {"id":"mushroom","emoji":"๐Ÿ„","name":"Mushroom","rarity":"Common","mult":0.20}, {"id":"seedling","emoji":"๐ŸŒฑ","name":"Seedling","rarity":"Common","mult":0.16}, {"id":"flower","emoji":"๐ŸŒผ","name":"Flower","rarity":"Common","mult":0.16}, {"id":"maple","emoji":"๐Ÿ","name":"Maple Leaf","rarity":"Common","mult":0.18}, {"id":"sun","emoji":"โ˜€๏ธ","name":"Sun","rarity":"Common","mult":0.24}, {"id":"moon","emoji":"๐ŸŒ™","name":"Moon","rarity":"Common","mult":0.22}, {"id":"cloud","emoji":"โ˜๏ธ","name":"Cloud","rarity":"Common","mult":0.18}, {"id":"snowflake","emoji":"โ„๏ธ","name":"Snowflake","rarity":"Common","mult":0.20}, {"id":"umbrella","emoji":"โ˜‚๏ธ","name":"Umbrella","rarity":"Common","mult":0.18}, {"id":"balloon","emoji":"๐ŸŽˆ","name":"Balloon","rarity":"Common","mult":0.22}, {"id":"pencil","emoji":"โœ๏ธ","name":"Pencil","rarity":"Common","mult":0.14}, {"id":"book","emoji":"๐Ÿ“˜","name":"Book","rarity":"Common","mult":0.16}, {"id":"paperclip","emoji":"๐Ÿ–‡๏ธ","name":"Paperclip","rarity":"Common","mult":0.14}, {"id":"scissors","emoji":"โœ‚๏ธ","name":"Scissors","rarity":"Common","mult":0.16}, {"id":"bulb","emoji":"๐Ÿ’ก","name":"Light Bulb","rarity":"Common","mult":0.24}, {"id":"battery","emoji":"๐Ÿ”‹","name":"Battery","rarity":"Common","mult":0.22}, {"id":"wrench","emoji":"๐Ÿ”ง","name":"Wrench","rarity":"Common","mult":0.20}, {"id":"hammer","emoji":"๐Ÿ”จ","name":"Hammer","rarity":"Common","mult":0.20}, {"id":"camera","emoji":"๐Ÿ“ท","name":"Camera","rarity":"Common","mult":0.24}, {"id":"gamepad","emoji":"๐ŸŽฎ","name":"Gamepad","rarity":"Common","mult":0.24}, # -------------------- Uncommon (25) --------------------- {"id":"donut","emoji":"๐Ÿฉ","name":"Donut","rarity":"Uncommon","mult":0.35}, {"id":"pizza","emoji":"๐Ÿ•","name":"Pizza","rarity":"Uncommon","mult":0.40}, {"id":"soccer","emoji":"โšฝ","name":"Soccer Ball","rarity":"Uncommon","mult":0.45}, {"id":"headset","emoji":"๐ŸŽง","name":"Headset","rarity":"Uncommon","mult":0.50}, {"id":"magnet","emoji":"๐Ÿงฒ","name":"Magnet","rarity":"Uncommon","mult":0.55}, {"id":"cat","emoji":"๐Ÿฑ","name":"Cat","rarity":"Uncommon","mult":0.60}, {"id":"basketball","emoji":"๐Ÿ€","name":"Basketball","rarity":"Uncommon","mult":0.45}, {"id":"baseball","emoji":"โšพ","name":"Baseball","rarity":"Uncommon","mult":0.42}, {"id":"guitar","emoji":"๐ŸŽธ","name":"Guitar","rarity":"Uncommon","mult":0.55}, {"id":"violin","emoji":"๐ŸŽป","name":"Violin","rarity":"Uncommon","mult":0.58}, {"id":"joystick","emoji":"๐Ÿ•น๏ธ","name":"Joystick","rarity":"Uncommon","mult":0.50}, {"id":"keyboard","emoji":"โŒจ๏ธ","name":"Keyboard","rarity":"Uncommon","mult":0.48}, {"id":"laptop","emoji":"๐Ÿ’ป","name":"Laptop","rarity":"Uncommon","mult":0.60}, {"id":"robot","emoji":"๐Ÿค–","name":"Robot","rarity":"Uncommon","mult":0.62}, {"id":"dog","emoji":"๐Ÿถ","name":"Dog","rarity":"Uncommon","mult":0.55}, {"id":"fox","emoji":"๐ŸฆŠ","name":"Fox","rarity":"Uncommon","mult":0.58}, {"id":"penguin","emoji":"๐Ÿง","name":"Penguin","rarity":"Uncommon","mult":0.60}, {"id":"koala","emoji":"๐Ÿจ","name":"Koala","rarity":"Uncommon","mult":0.60}, {"id":"panda","emoji":"๐Ÿผ","name":"Panda","rarity":"Uncommon","mult":0.60}, {"id":"owl","emoji":"๐Ÿฆ‰","name":"Owl","rarity":"Uncommon","mult":0.65}, {"id":"butterfly","emoji":"๐Ÿฆ‹","name":"Butterfly","rarity":"Uncommon","mult":0.65}, {"id":"car","emoji":"๐Ÿš—","name":"Car","rarity":"Uncommon","mult":0.55}, {"id":"train","emoji":"๐Ÿš†","name":"Train","rarity":"Uncommon","mult":0.55}, {"id":"sailboat","emoji":"โ›ต","name":"Sailboat","rarity":"Uncommon","mult":0.58}, {"id":"airplane","emoji":"โœˆ๏ธ","name":"Airplane","rarity":"Uncommon","mult":0.62}, # ---------------------- Rare (18) ----------------------- {"id":"melon","emoji":"๐Ÿ‰","name":"Watermelon","rarity":"Rare","mult":0.85}, {"id":"tiger","emoji":"๐Ÿฏ","name":"Tiger","rarity":"Rare","mult":1.00}, {"id":"ufo","emoji":"๐Ÿ›ธ","name":"UFO","rarity":"Rare","mult":1.10}, {"id":"unicorn","emoji":"๐Ÿฆ„","name":"Unicorn","rarity":"Rare","mult":1.20}, {"id":"elephant","emoji":"๐Ÿ˜","name":"Elephant","rarity":"Rare","mult":0.95}, {"id":"lion","emoji":"๐Ÿฆ","name":"Lion","rarity":"Rare","mult":1.05}, {"id":"wolf","emoji":"๐Ÿบ","name":"Wolf","rarity":"Rare","mult":1.05}, {"id":"dolphin","emoji":"๐Ÿฌ","name":"Dolphin","rarity":"Rare","mult":0.90}, {"id":"whale","emoji":"๐Ÿณ","name":"Whale","rarity":"Rare","mult":0.90}, {"id":"alien","emoji":"๐Ÿ‘ฝ","name":"Alien","rarity":"Rare","mult":1.10}, {"id":"ghost","emoji":"๐Ÿ‘ป","name":"Ghost","rarity":"Rare","mult":1.00}, {"id":"crystalball","emoji":"๐Ÿ”ฎ","name":"Crystal Ball","rarity":"Rare","mult":1.10}, {"id":"satellite","emoji":"๐Ÿ›ฐ๏ธ","name":"Satellite","rarity":"Rare","mult":1.15}, {"id":"comet","emoji":"โ˜„๏ธ","name":"Comet","rarity":"Rare","mult":1.20}, {"id":"ninja","emoji":"๐Ÿฅท","name":"Ninja","rarity":"Rare","mult":1.10}, {"id":"mountain","emoji":"โ›ฐ๏ธ","name":"Mountain","rarity":"Rare","mult":0.95}, {"id":"volcano","emoji":"๐ŸŒ‹","name":"Volcano","rarity":"Rare","mult":1.00}, {"id":"ring_rare","emoji":"๐Ÿ’","name":"Ring","rarity":"Rare","mult":1.25}, # ---------------------- Epic (10) ----------------------- {"id":"dragon","emoji":"๐Ÿ‰","name":"Dragon","rarity":"Epic","mult":2.0}, {"id":"lab","emoji":"๐Ÿงช","name":"Lab Flask","rarity":"Epic","mult":2.5}, {"id":"rocket","emoji":"๐Ÿš€","name":"Rocket","rarity":"Epic","mult":3.0}, {"id":"wizard","emoji":"๐Ÿง™","name":"Wizard","rarity":"Epic","mult":2.2}, {"id":"wand","emoji":"๐Ÿช„","name":"Magic Wand","rarity":"Epic","mult":2.6}, {"id":"dragonface","emoji":"๐Ÿฒ","name":"Dragon Face","rarity":"Epic","mult":2.8}, {"id":"castle","emoji":"๐Ÿฐ","name":"Castle","rarity":"Epic","mult":2.4}, {"id":"shield","emoji":"๐Ÿ›ก๏ธ","name":"Shield","rarity":"Epic","mult":2.3}, {"id":"swords","emoji":"โš”๏ธ","name":"Crossed Swords","rarity":"Epic","mult":2.7}, {"id":"trophy_epic","emoji":"๐Ÿ†","name":"Trophy","rarity":"Epic","mult":3.2}, # ------------------- Legendary (5) ---------------------- {"id":"crown","emoji":"๐Ÿ‘‘","name":"Crown","rarity":"Legendary","mult":6.0}, {"id":"eagle","emoji":"๐Ÿฆ…","name":"Eagle","rarity":"Legendary","mult":7.5}, {"id":"medal","emoji":"๐Ÿฅ‡","name":"Gold Medal","rarity":"Legendary","mult":9.0}, {"id":"moneybag","emoji":"๐Ÿ’ฐ","name":"Money Bag","rarity":"Legendary","mult":8.5}, {"id":"ring_legend","emoji":"๐Ÿ’","name":"Royal Ring","rarity":"Legendary","mult":10.0}, # --------------------- Mythic (2) ----------------------- {"id":"dino","emoji":"๐Ÿฆ–","name":"Dino","rarity":"Mythic","mult":25.0}, {"id":"diamond","emoji":"๐Ÿ’Ž","name":"Diamond","rarity":"Mythic","mult":50.0}, ] # Build rarity index RARITY_TO_ITEMS: Dict[str, List[Dict]] = {} for it in CATALOG: RARITY_TO_ITEMS.setdefault(it["rarity"], []).append(it) def _norm_weights(weights: Dict[str, float]) -> Dict[str, float]: s = float(sum(weights.values())) return {k: (v / s if s > 0 else 0.0) for k, v in weights.items()} NORM_RARITY = _norm_weights(PACK_RARITY_WEIGHTS) def _weighted_choice(items: List[Tuple[float, Dict]]) -> Dict: """items: list of (weight, item) where weight >= 0""" total = sum(w for w, _ in items) r = random.random() * total upto = 0.0 for w, it in items: upto += w if r <= upto: return it return items[-1][1] def pick_one() -> Dict: # pick rarity rar_roll = _weighted_choice([(p, {"rarity": r}) for r, p in NORM_RARITY.items()])["rarity"] pool = RARITY_TO_ITEMS.get(rar_roll, CATALOG) # equal within rarity return random.choice(pool) def money(n: int) -> str: return f"${n:,}" # ---------- Pagination helpers ---------- RARITY_RANK = {"Mythic": 0, "Legendary": 1, "Epic": 2, "Rare": 3, "Uncommon": 4, "Common": 5} SORTED_CATALOG = sorted( CATALOG, key=lambda it: (RARITY_RANK.get(it["rarity"], 99), -float(it["mult"]), it["name"]) ) # Attach rank (1 = rarest) for i, it in enumerate(SORTED_CATALOG, 1): it["_rank"] = i PAGE_SIZE = 20 # 100 items -> 5 pages def _collection_page_lines(user_id: int, page: int) -> Tuple[str, int, int, int]: """ Returns (text, start_idx, end_idx, total_pages) for the given page (0-based). """ coll = db.get_packs_collection(user_id) total = len(SORTED_CATALOG) pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE) page = max(0, min(page, pages - 1)) start = page * PAGE_SIZE end = min(total, start + PAGE_SIZE) lines: List[str] = [] for it in SORTED_CATALOG[start:end]: owned = coll.get(it["id"], 0) > 0 idx = it["_rank"] if owned: lines.append(f"**#{idx:02d}** {it['emoji']} **{it['name']}** ยท *{it['rarity']}*") else: lines.append(f"**#{idx:02d}** ?") text = "\n".join(lines) return text, start, end, pages def _collection_embed(user_id: int, page: int) -> discord.Embed: coll = db.get_packs_collection(user_id) have = sum(1 for it in SORTED_CATALOG if coll.get(it["id"], 0) > 0) total = len(SORTED_CATALOG) text, start, end, pages = _collection_page_lines(user_id, page) e = discord.Embed( title="๐Ÿ—‚๏ธ Packs Collection", description=f"**Progress:** {have}/{total} collected\n" f"Rarest is **#01**, Common end is **#{total:02d}**.\n\n{text}", color=discord.Color.dark_gold() ) e.set_footer(text=f"Page {page+1}/{pages} โ€ข Showing #{start+1:02d}โ€“#{end:02d}") return e # ---------- UI: Set Bet modal ---------- class SetBetModal(discord.ui.Modal, title="Set Packs Bet"): def __init__(self, view: "PacksView"): super().__init__() self.view = view self.amount = discord.ui.TextInput( label=f"Amount (min {PACK_MIN_BET})", placeholder=str(self.view.bet), required=True, min_length=1, max_length=10, ) self.add_item(self.amount) async def on_submit(self, itx: discord.Interaction): if itx.user.id != self.view.user_id: return await itx.response.send_message("This isnโ€™t your Packs panel.", ephemeral=True) if self.view.busy: return await itx.response.send_message("Please wait a moment.", ephemeral=True) raw = str(self.amount.value) try: amt = int("".join(ch for ch in raw if ch.isdigit())) except Exception: return await itx.response.send_message("Enter a valid number.", ephemeral=True) self.view.bet = max(PACK_MIN_BET, amt) await itx.response.defer(ephemeral=True) if self.view.message: await self.view.message.edit(embed=self.view.render(), view=self.view) # ---------- UI: Collection View ---------- class PacksCollectionView(discord.ui.View): def __init__(self, user_id: int, *, start_page: int = 0, timeout: int = 180): super().__init__(timeout=timeout) self.user_id = user_id self.page = start_page self.pages = max(1, (len(SORTED_CATALOG) + PAGE_SIZE - 1) // PAGE_SIZE) self.message: Optional[discord.Message] = None async def on_timeout(self): for c in self.children: c.disabled = True try: if self.message: await self.message.edit(view=self) except: pass async def interaction_check(self, interaction: discord.Interaction) -> bool: if interaction.user.id != self.user_id: await interaction.response.send_message("This collection belongs to someone else.", ephemeral=True) return False return True def _sync(self): # enable/disable nav buttons based on current page for c in self.children: if isinstance(c, discord.ui.Button): if c.custom_id == "first": c.disabled = (self.page <= 0) if c.custom_id == "prev": c.disabled = (self.page <= 0) if c.custom_id == "next": c.disabled = (self.page >= self.pages - 1) if c.custom_id == "last": c.disabled = (self.page >= self.pages - 1) def embed(self) -> discord.Embed: self._sync() return _collection_embed(self.user_id, self.page) @discord.ui.button(label="โฎ๏ธ", style=discord.ButtonStyle.secondary, custom_id="first") async def first(self, itx: discord.Interaction, _): self.page = 0 await itx.response.edit_message(embed=self.embed(), view=self) @discord.ui.button(label="โ—€๏ธ", style=discord.ButtonStyle.secondary, custom_id="prev") async def prev(self, itx: discord.Interaction, _): if self.page > 0: self.page -= 1 await itx.response.edit_message(embed=self.embed(), view=self) @discord.ui.button(label="โ–ถ๏ธ", style=discord.ButtonStyle.secondary, custom_id="next") async def next(self, itx: discord.Interaction, _): if self.page < self.pages - 1: self.page += 1 await itx.response.edit_message(embed=self.embed(), view=self) @discord.ui.button(label="โญ๏ธ", style=discord.ButtonStyle.secondary, custom_id="last") async def last(self, itx: discord.Interaction, _): self.page = self.pages - 1 await itx.response.edit_message(embed=self.embed(), view=self) @discord.ui.button(label="Close", style=discord.ButtonStyle.danger) async def close(self, itx: discord.Interaction, _): try: await itx.message.delete() except: try: for c in self.children: c.disabled = True await itx.response.edit_message(view=self) except: pass # ---------- Packs game UI ---------- class PacksView(discord.ui.View): """ Row 0: Set Bet ยท ร—2 ยท ยฝ ยท Open Pack Row 1: Open Again ยท Collection """ def __init__(self, user_id: int, initial_bet: int = 100, *, timeout: int = 180): super().__init__(timeout=timeout) self.user_id = user_id self.bet = max(PACK_MIN_BET, initial_bet) self.last_pack: Optional[List[Dict]] = None self.last_return: int = 0 self.last_net: int = 0 self.busy = False self.message: Optional[discord.Message] = None async def on_timeout(self): for c in self.children: c.disabled = True try: if self.message: await self.message.edit(view=self) except: pass # ---- render def render(self) -> discord.Embed: color = discord.Color.blurple() if self.last_pack is not None: color = discord.Color.green() if self.last_net > 0 else (discord.Color.red() if self.last_net < 0 else discord.Color.orange()) e = discord.Embed(title="๐Ÿ“ฆ Packs", color=color) cash, _ = db.get_wallet(self.user_id) desc = [f"**Bet:** {money(self.bet)} โ€ข **Balance:** {money(cash)}"] if self.last_pack is None: desc += ["Open a pack of 5 items. Each item pays its multiplier ร— your bet. Collect them all!"] e.description = "\n".join(desc) if self.last_pack is not None: lines = [] for it in self.last_pack: tag = "NEW" if it.get("_new") else "DUP" lines.append(f"{it['emoji']} **{it['name']}** ยท *{it['rarity']}* ยท ร—{it['mult']:.2f} `{tag}`") e.add_field(name="Last Pack", value="\n".join(lines), inline=False) e.add_field( name="Result", value=f"Return: **{money(self.last_return)}** โ€ข Net: **{'+' if self.last_net>0 else ''}{money(self.last_net)}**", inline=False ) return e # ---- actions async def _open_pack(self, itx: discord.Interaction): if self.busy: return self.busy = True cash, _ = db.get_wallet(self.user_id) if self.bet > cash: self.busy = False return await itx.response.send_message("Not enough cash for that bet.", ephemeral=True) # debit db.add_cash(self.user_id, -self.bet) # current collection to mark NEW/DUP coll = db.get_packs_collection(self.user_id) # dict card_id -> qty pulled: List[Dict] = [] total_mult = 0.0 for _ in range(PACK_SIZE): it = pick_one().copy() it["_new"] = (coll.get(it["id"], 0) == 0) pulled.append(it) total_mult += it["mult"] # persist collection increment db.add_pack_card(self.user_id, it["id"], 1) coll[it["id"]] = coll.get(it["id"], 0) + 1 returned = int(self.bet * total_mult) if returned > 0: db.add_cash(self.user_id, returned) db.record_packs_open(self.user_id, bet=self.bet, return_amount=returned, won=(returned > self.bet)) self.last_pack = pulled self.last_return = returned self.last_net = returned - self.bet await itx.response.edit_message(embed=self.render(), view=self) self.busy = False # ----- Buttons @discord.ui.button(label="Set Bet", style=discord.ButtonStyle.secondary, row=0) async def set_bet(self, itx: discord.Interaction, _): if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True) await itx.response.send_modal(SetBetModal(self)) @discord.ui.button(label="ร—2", style=discord.ButtonStyle.secondary, row=0) async def x2(self, itx: discord.Interaction, _): if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True) self.bet = max(PACK_MIN_BET, self.bet * 2) await itx.response.edit_message(embed=self.render(), view=self) @discord.ui.button(label="ยฝ", style=discord.ButtonStyle.secondary, row=0) async def half(self, itx: discord.Interaction, _): if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True) self.bet = max(PACK_MIN_BET, self.bet // 2) await itx.response.edit_message(embed=self.render(), view=self) @discord.ui.button(label="Open Pack", style=discord.ButtonStyle.success, emoji="๐ŸŽ", row=0) async def open_pack(self, itx: discord.Interaction, _): if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True) await self._open_pack(itx) @discord.ui.button(label="Open Again", style=discord.ButtonStyle.primary, emoji="๐Ÿ”", row=1) async def open_again(self, itx: discord.Interaction, _): if itx.user.id != self.user_id: return await itx.response.send_message("Not your panel.", ephemeral=True) await self._open_pack(itx) @discord.ui.button(label="Collection", style=discord.ButtonStyle.secondary, emoji="๐Ÿ—‚๏ธ", row=1) async def collection(self, itx: discord.Interaction, _): if itx.user.id != self.user_id: return await itx.response.send_message("This isnโ€™t your Packs panel.", ephemeral=True) view = PacksCollectionView(self.user_id, start_page=0, timeout=180) await itx.response.send_message(embed=view.embed(), view=view, ephemeral=True) # ---------- Cog ---------- class PacksCog(commands.Cog): def __init__(self, bot): self.bot = bot @commands.command(name="packs") async def packs(self, ctx: commands.Context, bet: int = None): uid = ctx.author.id view = PacksView(uid, initial_bet=bet or PACK_MIN_BET, timeout=180) msg = await ctx.send(embed=view.render(), view=view) view.message = msg @commands.command(name="collection", aliases=["packdex","packsdex"]) async def collection(self, ctx: commands.Context): uid = ctx.author.id view = PacksCollectionView(uid, start_page=0, timeout=180) msg = await ctx.send(embed=view.embed(), view=view) view.message = msg async def setup(bot): # make sure DB tables exist for packs db.ensure_packs_tables() await bot.add_cog(PacksCog(bot))