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.
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run

This commit is contained in:
Valerie 2025-05-25 23:40:22 -04:00
parent 9b7be4987f
commit 5cc572ca7a
2 changed files with 130 additions and 66 deletions

View file

@ -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()

View file

@ -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()