import discord from discord.ui import View, Select, Button, button, Item from typing import Optional, List, Dict, Any from redbot.core.utils.chat_formatting import box, humanize_list import asyncio class ShopView(View): def __init__(self, ctx, shops: Dict[str, Any], timeout: int = 180): # 3 minutes timeout super().__init__(timeout=timeout) self.ctx = ctx self.shops = shops self.current_shop = None self.current_item = None self.quantity = None self.message = None # Store message for timeout handling self.setup_shop_select() cancel_button = Button(label="Cancel", style=discord.ButtonStyle.red, custom_id="cancel", row=4) cancel_button.callback = self.cancel self.add_item(cancel_button) 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) async def cancel(self, interaction: discord.Interaction): await interaction.response.edit_message(content="Shop menu cancelled.", embed=None, view=None) self.stop() def setup_shop_select(self): options = [] for shop_name, shop_data in self.shops.items(): if shop_data["Items"]: # Only show shops with items options.append(discord.SelectOption( label=shop_name[:25], # Discord limit description=f"{len(shop_data['Items'])} items", value=shop_name )) if options: shop_select = Select( placeholder="Select a shop", options=options, row=0 # Explicitly set to row 0 ) shop_select.callback = self.shop_selected 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 # Clear previous item select if it exists for item in self.children[:]: if isinstance(item, Select) and item.row == 1: self.remove_item(item) # Add new item select 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}" options.append(discord.SelectOption( label=item_name[:25], description=desc, value=item_name )) if options: item_select = Select( placeholder="Select an item", options=options[:25], # Discord limit row=1 # Explicitly set to row 1 ) item_select.callback = self.item_selected self.add_item(item_select) 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] # Create purchase confirmation view purchase_view = PurchaseView(self.ctx, self.current_shop, item_name, item_data) embed = purchase_view.build_embed() await interaction.response.edit_message(embed=embed, view=purchase_view) # Wait for the purchase view to complete await purchase_view.wait() self.quantity = purchase_view.quantity # Store the quantity from purchase view self.stop() # Stop the shop view since we're done async def on_timeout(self) -> None: if self.message: await self.message.edit(content="Shop menu timed out after 3 minutes of inactivity.", view=None) class PurchaseView(View): def __init__(self, ctx, shop: str, item: str, item_data: Dict[str, Any], timeout: int = 180): # 3 minutes timeout super().__init__(timeout=timeout) self.ctx = ctx self.shop = shop self.item = item self.item_data = item_data self.quantity = None self.message = None # Store message for timeout handling # Add buttons with explicit row assignments buy_one = Button(label="Buy 1", style=discord.ButtonStyle.green, row=0) buy_one.callback = self.buy_one self.add_item(buy_one) buy_five = Button(label="Buy 5", style=discord.ButtonStyle.green, row=0) buy_five.callback = self.buy_five self.add_item(buy_five) custom = Button(label="Custom Amount", style=discord.ButtonStyle.blurple, row=0) custom.callback = self.custom_amount self.add_item(custom) cancel = Button(label="Cancel", style=discord.ButtonStyle.red, row=0) cancel.callback = self.cancel self.add_item(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 def build_embed(self) -> discord.Embed: e = discord.Embed( title=f"Purchase {self.item}", description=self.item_data["Info"], color=discord.Color.blue() ) # First row of fields e.add_field(name="Stock", value="Infinite" if self.item_data["Qty"] == "--" else self.item_data["Qty"], inline=True) e.add_field(name="Cost", value=self.item_data["Cost"], inline=True) e.add_field(name="Type", value=self.item_data["Type"].title(), inline=True) # Add role in a new row if it's a role type item if self.item_data["Type"] == "role": e.add_field(name="Role", value=self.item_data["Role"], inline=False) e.set_footer(text="Select a quantity to purchase") return e async def buy_one(self, interaction: discord.Interaction): self.quantity = 1 await self.handle_purchase(interaction) async def buy_five(self, interaction: discord.Interaction): self.quantity = 5 await self.handle_purchase(interaction) async def custom_amount(self, interaction: discord.Interaction): 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 ( m.author == self.ctx.author and m.channel == self.ctx.channel and m.content.isdigit() ) try: msg = await self.ctx.bot.wait_for("message", timeout=60.0, check=check) # 1 minute to type amount self.quantity = int(msg.content) await self.handle_purchase(interaction) except asyncio.TimeoutError: await interaction.message.reply("Custom amount entry timed out after 1 minute.", mention_author=True) self.stop() async def cancel(self, interaction: discord.Interaction): await interaction.response.edit_message(content="Purchase cancelled.", embed=None, view=None) self.stop() async def handle_purchase(self, interaction: discord.Interaction): # Validate quantity if self.quantity is None: await interaction.response.send_message( "Invalid quantity specified.", ephemeral=True ) return # Validate quantity for random items if self.item_data["Type"] == "random" and self.quantity != 1: await interaction.response.send_message( "You can only buy 1 random item at a time.", ephemeral=True ) return if self.item_data["Qty"] != "--" and self.quantity > self.item_data["Qty"]: await interaction.response.send_message( f"Not enough stock! Only {self.item_data['Qty']} available.", ephemeral=True ) return 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(embed=embed, view=None) async def on_timeout(self) -> None: if self.message: await self.message.edit(content="Purchase menu timed out after 3 minutes of inactivity.", view=None) class InventoryView(View): def __init__(self, ctx, inventory: Dict[str, Any], timeout: int = 180): # 3 minutes timeout super().__init__(timeout=timeout) self.ctx = ctx self.inventory = inventory self.selected_item = None self.message = None # Store message for timeout handling self.setup_inventory_select() 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="Close", style=discord.ButtonStyle.red, custom_id="inventory_close", row=4) 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 = [] for item_name, item_data in self.inventory.items(): desc = f"Quantity: {item_data['Qty']} | Type: {item_data['Type']}" options.append(discord.SelectOption( label=item_name[:25], description=desc, value=item_name )) if options: inv_select = Select( placeholder="Select an item to view/use", options=options[:25], # Discord limit row=0 ) inv_select.callback = self.item_selected 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) self.selected_item = interaction.data["values"][0] self.stop() async def on_timeout(self) -> None: if self.message: await self.message.edit(content="Inventory menu timed out after 3 minutes of inactivity.", view=None) class UseItemView(View): def __init__(self, ctx, item_name: str, timeout: int = 60): super().__init__(timeout=timeout) self.ctx = ctx self.item_name = item_name self.value = None self.message = None @button(label="Use", style=discord.ButtonStyle.green) async def use(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 self.stop() await interaction.response.defer() @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 self.stop() await interaction.response.defer() async def on_timeout(self) -> None: if self.message: await self.message.edit(content="Item use menu timed out.", view=None)