From 5cc572ca7aacabb19f9afe84b38718b3dd35494d Mon Sep 17 00:00:00 2001 From: Valerie Date: Sun, 25 May 2025 23:40:22 -0400 Subject: [PATCH] Refactor Shop cog to enhance user experience by implementing interactive embeds for inventory and shop commands. Improve error handling with descriptive embeds for various scenarios, including empty inventories and invalid parameters. Add cancel buttons to shop and purchase views for better navigation and user control. --- shop/shop.py | 79 ++++++++++++++++++++++++---------- shop/ui.py | 117 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 130 insertions(+), 66 deletions(-) diff --git a/shop/shop.py b/shop/shop.py index 589556d..5028bad 100644 --- a/shop/shop.py +++ b/shop/shop.py @@ -108,7 +108,6 @@ class Shop(commands.Cog): # -----------------------COMMANDS------------------------------------- @commands.command() - @commands.max_concurrency(1, commands.BucketType.user) async def inventory(self, ctx): """Displays your purchased items.""" try: @@ -118,20 +117,26 @@ class Shop(commands.Cog): inventory = await instance.Inventory.all() if not inventory: - return await ctx.send("Your inventory is empty.") + embed = discord.Embed( + title="Empty Inventory", + description="Your inventory is empty.", + color=discord.Color.red() + ) + return await ctx.send(embed=embed) - view = InventoryView(ctx, inventory) - await ctx.send( - f"{ctx.author.mention}'s Inventory", - view=view + embed = discord.Embed( + title=f"{ctx.author.display_name}'s Inventory", + color=discord.Color.blue() ) + view = InventoryView(ctx, inventory) + await ctx.send(embed=embed, view=view) try: await view.wait() if view.selected_item: # An item was selected to use await self.pending_prompt(ctx, instance, inventory, view.selected_item) except asyncio.TimeoutError: - await ctx.send("Inventory view timed out.") + await ctx.send("Inventory view timed out.", ephemeral=True) async def inv_hook(self, user): """Inventory Hook for outside cogs @@ -165,7 +170,6 @@ class Shop(commands.Cog): pass @shop.command() - @commands.max_concurrency(1, commands.BucketType.user) async def buy(self, ctx, *purchase): """Opens the shop menu or directly purchases an item. @@ -183,33 +187,60 @@ class Shop(commands.Cog): return await ctx.send("You can't use this command in DMs when not in global mode.") if not await instance.Shops(): - return await ctx.send("No shops have been created yet.") + embed = discord.Embed( + title="No Shops Available", + description="No shops have been created yet.", + color=discord.Color.red() + ) + return await ctx.send(embed=embed) if await instance.Settings.Closed(): - return await ctx.send("The shop system is currently closed.") + embed = discord.Embed( + title="Shops Closed", + description="The shop system is currently closed.", + color=discord.Color.red() + ) + return await ctx.send(embed=embed) shops = await instance.Shops.all() available_shops = await self.check_availability(ctx, shops) if not available_shops: - return await ctx.send( - "Either no items have been created, you need a higher role, " - "or this command should be used in a server and not DMs." + embed = discord.Embed( + title="No Access", + description="Either no items have been created, you need a higher role, or this command should be used in a server and not DMs.", + color=discord.Color.red() ) + return await ctx.send(embed=embed) if purchase: try: shop, item = purchase except ValueError: - return await ctx.send("Too many parameters passed. Use help on this command for more information.") + embed = discord.Embed( + title="Invalid Parameters", + description="Too many parameters passed. Use help on this command for more information.", + color=discord.Color.red() + ) + return await ctx.send(embed=embed) if shop not in shops: - return await ctx.send("Either that shop does not exist, or you don't have access to it.") + embed = discord.Embed( + title="Shop Not Found", + description="Either that shop does not exist, or you don't have access to it.", + color=discord.Color.red() + ) + return await ctx.send(embed=embed) # Create purchase view directly for the specified item item_data = shops[shop]["Items"].get(item) if not item_data: - return await ctx.send(f"Item '{item}' not found in shop '{shop}'.") + embed = discord.Embed( + title="Item Not Found", + description=f"Item '{item}' not found in shop '{shop}'.", + color=discord.Color.red() + ) + return await ctx.send(embed=embed) view = PurchaseView(ctx, shop, item, item_data) embed = view.build_embed() @@ -222,24 +253,26 @@ class Shop(commands.Cog): sm = ShopManager(ctx, instance, user_data) await sm.order(shop, item, view.quantity) except asyncio.TimeoutError: - await ctx.send("Purchase menu timed out.") + await ctx.send("Purchase menu timed out.", ephemeral=True) else: # Open interactive shop menu - view = ShopView(ctx, shops) - await ctx.send( - f"Welcome to the shop, {ctx.author.mention}!", - view=view + embed = discord.Embed( + title="Welcome to the Shop", + description=f"Welcome {ctx.author.mention}! Please select a shop to browse.", + color=discord.Color.blue() ) + view = ShopView(ctx, shops) + await ctx.send(embed=embed, view=view) try: await view.wait() - if view.current_shop and view.current_item: # An item was selected + if view.current_shop and view.current_item and view.quantity: user_data = await self.get_instance(ctx, user=ctx.author) sm = ShopManager(ctx, instance, user_data) await sm.order(view.current_shop, view.current_item, view.quantity) except asyncio.TimeoutError: - await ctx.send("Shop menu timed out.") + await ctx.send("Shop menu timed out.", ephemeral=True) @commands.max_concurrency(1, commands.BucketType.user) @shop.command() diff --git a/shop/ui.py b/shop/ui.py index 602a390..57cf3a5 100644 --- a/shop/ui.py +++ b/shop/ui.py @@ -10,8 +10,23 @@ class ShopView(View): self.shops = shops self.current_shop = None self.current_item = None - self.quantity = None # Add quantity attribute + self.quantity = None self.setup_shop_select() + self.add_item(Button(label="Cancel", style=discord.ButtonStyle.red, custom_id="cancel")) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.ctx.author: + await interaction.response.send_message("This menu is not for you!", ephemeral=True) + return False + return True + + async def on_error(self, interaction: discord.Interaction, error: Exception, item: Item) -> None: + await interaction.response.send_message(f"An error occurred: {str(error)}", ephemeral=True) + + @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red, custom_id="cancel") + async def cancel(self, interaction: discord.Interaction, button: Button): + await interaction.response.edit_message(content="Shop menu cancelled.", embed=None, view=None) + self.stop() def setup_shop_select(self): options = [] @@ -33,9 +48,6 @@ class ShopView(View): self.add_item(shop_select) async def shop_selected(self, interaction: discord.Interaction): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) - shop_name = interaction.data["values"][0] self.current_shop = shop_name @@ -48,6 +60,12 @@ class ShopView(View): items = self.shops[shop_name]["Items"] options = [] + embed = discord.Embed( + title=f"Shop: {shop_name}", + description="Select an item to purchase", + color=discord.Color.blue() + ) + for item_name, item_data in items.items(): qty = "∞" if item_data["Qty"] == "--" else item_data["Qty"] desc = f"Cost: {item_data['Cost']} | Stock: {qty}" @@ -66,12 +84,9 @@ class ShopView(View): item_select.callback = self.item_selected self.add_item(item_select) - await interaction.response.edit_message(view=self) + await interaction.response.edit_message(embed=embed, view=self) async def item_selected(self, interaction: discord.Interaction): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) - item_name = interaction.data["values"][0] self.current_item = item_name item_data = self.shops[self.current_shop]["Items"][item_name] @@ -94,8 +109,14 @@ class PurchaseView(View): self.shop = shop self.item = item self.item_data = item_data - self.quantity = 1 + self.quantity = None + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.ctx.author: + await interaction.response.send_message("This menu is not for you!", ephemeral=True) + return False + return True + def build_embed(self) -> discord.Embed: e = discord.Embed( title=f"Purchase {self.item}", @@ -108,31 +129,27 @@ class PurchaseView(View): e.add_field(name="Type", value=self.item_data["Type"].title(), inline=True) if self.item_data["Type"] == "role": e.add_field(name="Role", value=self.item_data["Role"], inline=True) + e.set_footer(text="Select a quantity to purchase") return e @button(label="Buy 1", style=discord.ButtonStyle.green, row=1) async def buy_one(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) self.quantity = 1 await self.handle_purchase(interaction) @button(label="Buy 5", style=discord.ButtonStyle.green, row=1) async def buy_five(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) self.quantity = 5 await self.handle_purchase(interaction) @button(label="Custom Amount", style=discord.ButtonStyle.blurple, row=1) async def custom_amount(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) - - await interaction.response.send_message( - "How many would you like to buy? Type a number:", - ephemeral=True + embed = discord.Embed( + title="Custom Purchase Amount", + description="How many would you like to buy? Type a number in chat.", + color=discord.Color.blue() ) + await interaction.response.edit_message(embed=embed) def check(m): return ( @@ -147,12 +164,12 @@ class PurchaseView(View): await self.handle_purchase(interaction) except asyncio.TimeoutError: await interaction.followup.send("Purchase cancelled - took too long to respond.", ephemeral=True) + self.stop() @button(label="Cancel", style=discord.ButtonStyle.red, row=1) async def cancel(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) await interaction.response.edit_message(content="Purchase cancelled.", embed=None, view=None) + self.stop() async def handle_purchase(self, interaction: discord.Interaction): # Validate quantity @@ -172,21 +189,36 @@ class PurchaseView(View): total_cost = self.item_data["Cost"] * self.quantity + embed = discord.Embed( + title="Processing Purchase", + description=f"Processing purchase of {self.quantity}x {self.item}...", + color=discord.Color.green() + ) + embed.add_field(name="Total Cost", value=total_cost) + # This will be handled by the Shop cog's order method self.stop() - await interaction.response.edit_message( - content=f"Processing purchase of {self.quantity}x {self.item}...", - embed=None, - view=None - ) + await interaction.response.edit_message(embed=embed, view=None) class InventoryView(View): def __init__(self, ctx, inventory: Dict[str, Any], timeout: int = 60): super().__init__(timeout=timeout) self.ctx = ctx self.inventory = inventory - self.selected_item = None # Track selected item + self.selected_item = None self.setup_inventory_select() + self.add_item(Button(label="Close", style=discord.ButtonStyle.red, custom_id="close")) + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.ctx.author: + await interaction.response.send_message("This menu is not for you!", ephemeral=True) + return False + return True + + @discord.ui.button(label="Close", style=discord.ButtonStyle.red, custom_id="close") + async def close(self, interaction: discord.Interaction, button: Button): + await interaction.response.edit_message(content="Inventory closed.", embed=None, view=None) + self.stop() def setup_inventory_select(self): options = [] @@ -208,11 +240,8 @@ class InventoryView(View): self.add_item(inv_select) async def item_selected(self, interaction: discord.Interaction): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) - item_name = interaction.data["values"][0] - self.selected_item = item_name # Store selected item + self.selected_item = item_name item_data = self.inventory[item_name] embed = discord.Embed( @@ -240,25 +269,27 @@ class UseItemView(View): self.ctx = ctx self.item = item self.item_data = item_data - self.value = False # Track if item was used + self.value = False + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user != self.ctx.author: + await interaction.response.send_message("This menu is not for you!", ephemeral=True) + return False + return True + @button(label="Use Item", style=discord.ButtonStyle.green) async def use_item(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) - - self.value = True # Item was used - await interaction.response.edit_message( - content=f"Processing use of {self.item}...", - embed=None, - view=None + self.value = True + embed = discord.Embed( + title="Using Item", + description=f"Processing use of {self.item}...", + color=discord.Color.green() ) + await interaction.response.edit_message(embed=embed, view=None) self.stop() @button(label="Cancel", style=discord.ButtonStyle.red) async def cancel(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - return await interaction.response.send_message("This menu is not for you!", ephemeral=True) - self.value = False # Item was not used + self.value = False await interaction.response.edit_message(content="Cancelled.", embed=None, view=None) self.stop() \ No newline at end of file