Ruby-Cogs/shop/ui.py

322 lines
No EOL
13 KiB
Python

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)