Refactor Shop cog to utilize modern UI components for inventory and purchase interactions. Replace menu-based displays with interactive views for better user experience. Update item redemption process to handle pending items with improved confirmation dialogs. Enhance purchase logic to support quantity selection and stock validation.
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
This commit is contained in:
parent
a63232fb2a
commit
c9f172c451
2 changed files with 549 additions and 134 deletions
435
shop/shop.py
435
shop/shop.py
|
@ -13,7 +13,7 @@ from itertools import zip_longest
|
|||
from typing import Literal
|
||||
|
||||
# Shop
|
||||
from .menu import ShopMenu
|
||||
from .ui import ShopView, InventoryView, PurchaseView, UseItemView
|
||||
from .inventory import Inventory
|
||||
from .checks import Checks
|
||||
|
||||
|
@ -29,7 +29,7 @@ from redbot.core.errors import BalanceTooHigh
|
|||
|
||||
log = logging.getLogger("red.shop")
|
||||
|
||||
__version__ = "3.1.13"
|
||||
__version__ = "3.2.0"
|
||||
__author__ = "Redjumpman"
|
||||
|
||||
|
||||
|
@ -114,16 +114,23 @@ class Shop(commands.Cog):
|
|||
instance = await self.get_instance(ctx, user=ctx.author)
|
||||
except AttributeError:
|
||||
return await ctx.send("You can't use this command in DMs when not in global mode.")
|
||||
if not await instance.Inventory():
|
||||
return await ctx.send("You don't have any items to display.")
|
||||
data = await instance.Inventory.all()
|
||||
menu = Inventory(ctx, list(data.items()))
|
||||
|
||||
|
||||
inventory = await instance.Inventory.all()
|
||||
if not inventory:
|
||||
return await ctx.send("Your inventory is empty.")
|
||||
|
||||
view = InventoryView(ctx, inventory)
|
||||
await ctx.send(
|
||||
f"{ctx.author.mention}'s Inventory",
|
||||
view=view
|
||||
)
|
||||
|
||||
try:
|
||||
item = await menu.display()
|
||||
except RuntimeError:
|
||||
return
|
||||
await self.pending_prompt(ctx, instance, data, item)
|
||||
await view.wait()
|
||||
if view.value: # An item was selected to use
|
||||
await self.pending_prompt(ctx, instance, inventory, view.value)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("Inventory view timed out.")
|
||||
|
||||
async def inv_hook(self, user):
|
||||
"""Inventory Hook for outside cogs
|
||||
|
@ -159,61 +166,79 @@ class Shop(commands.Cog):
|
|||
@shop.command()
|
||||
@commands.max_concurrency(1, commands.BucketType.user)
|
||||
async def buy(self, ctx, *purchase):
|
||||
"""Shop menu appears with no purchase order.
|
||||
|
||||
When no argument is specified for purchase, it will bring up the
|
||||
shop menu.
|
||||
|
||||
Using the purchase argument allows direct purchases from a shop.
|
||||
The order is "Shop Name" "Item Name" and names with spaces
|
||||
must include quotes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
[p]shop buy \"Secret Shop\" oil
|
||||
"""Opens the shop menu or directly purchases an item.
|
||||
|
||||
When no argument is specified, opens an interactive shop menu.
|
||||
For direct purchase, use: "Shop Name" "Item Name"
|
||||
|
||||
Examples:
|
||||
[p]shop buy "Secret Shop" oil
|
||||
[p]shop buy Junkyard tire
|
||||
[p]shop buy \"Holy Temple\" \"Healing Potion\"
|
||||
[p]shop buy "Holy Temple" "Healing Potion"
|
||||
"""
|
||||
try:
|
||||
instance = await self.get_instance(ctx, settings=True)
|
||||
except AttributeError:
|
||||
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.")
|
||||
|
||||
if await instance.Settings.Closed():
|
||||
return await ctx.send("The shop system is currently closed.")
|
||||
|
||||
shops = await instance.Shops.all()
|
||||
col = await self.check_availability(ctx, shops)
|
||||
if not col:
|
||||
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, "
|
||||
"Either no items have been created, you need a higher role, "
|
||||
"or this command should be used in a server and not DMs."
|
||||
)
|
||||
|
||||
if purchase:
|
||||
try:
|
||||
shop, item = purchase
|
||||
except ValueError:
|
||||
return await ctx.send("Too many parameters passed. Use help on this command for more information.")
|
||||
|
||||
if shop not in shops:
|
||||
return await ctx.send("Either that shop does not exist, or you don't have access to it.")
|
||||
|
||||
else:
|
||||
style = await instance.Settings.Sorting()
|
||||
menu = ShopMenu(ctx, shops, sorting=style)
|
||||
|
||||
# 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}'.")
|
||||
|
||||
view = PurchaseView(ctx, shop, item, item_data)
|
||||
embed = view.build_embed()
|
||||
await ctx.send(embed=embed, view=view)
|
||||
|
||||
try:
|
||||
shop, item = await menu.display()
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
user_data = await self.get_instance(ctx, user=ctx.author)
|
||||
sm = ShopManager(ctx, instance, user_data)
|
||||
try:
|
||||
await sm.order(shop, item)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("Request timed out.")
|
||||
except ExitProcess:
|
||||
await ctx.send("Transaction canceled.")
|
||||
await view.wait()
|
||||
if view.quantity: # A purchase was initiated
|
||||
user_data = await self.get_instance(ctx, user=ctx.author)
|
||||
sm = ShopManager(ctx, instance, user_data)
|
||||
await sm.order(shop, item, view.quantity)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("Purchase menu timed out.")
|
||||
|
||||
else:
|
||||
# Open interactive shop menu
|
||||
view = ShopView(ctx, shops)
|
||||
await ctx.send(
|
||||
f"Welcome to the shop, {ctx.author.mention}!",
|
||||
view=view
|
||||
)
|
||||
|
||||
try:
|
||||
await view.wait()
|
||||
if view.current_shop and view.current_item: # An item was selected
|
||||
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.")
|
||||
|
||||
@commands.max_concurrency(1, commands.BucketType.user)
|
||||
@shop.command()
|
||||
|
@ -372,18 +397,153 @@ class Shop(commands.Cog):
|
|||
instance = await self.get_instance(ctx, settings=True)
|
||||
if not await instance.Pending():
|
||||
return await ctx.send("There are not any pending items.")
|
||||
|
||||
data = await instance.Pending.all()
|
||||
menu = ShopMenu(ctx, data, mode=1, sorting="name")
|
||||
|
||||
try:
|
||||
user, item, = await menu.display()
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.clear_single_pending(ctx, instance, data, item, user)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("Request timed out.")
|
||||
|
||||
class PendingView(discord.ui.View):
|
||||
def __init__(self, cog, data, timeout=60):
|
||||
super().__init__(timeout=timeout)
|
||||
self.cog = cog
|
||||
self.data = data
|
||||
self.setup_user_select()
|
||||
|
||||
def setup_user_select(self):
|
||||
options = []
|
||||
for user_id, items in self.data.items():
|
||||
user = self.cog.bot.get_user(int(user_id))
|
||||
if user:
|
||||
desc = f"{len(items)} pending items"
|
||||
options.append(discord.SelectOption(
|
||||
label=user.name,
|
||||
description=desc,
|
||||
value=user_id
|
||||
))
|
||||
|
||||
if options:
|
||||
user_select = discord.ui.Select(
|
||||
placeholder="Select a user",
|
||||
options=options[:25], # Discord limit
|
||||
row=0
|
||||
)
|
||||
user_select.callback = self.user_selected
|
||||
self.add_item(user_select)
|
||||
|
||||
async def user_selected(self, interaction: discord.Interaction):
|
||||
if interaction.user != ctx.author:
|
||||
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
|
||||
|
||||
user_id = interaction.data["values"][0]
|
||||
user_items = self.data[user_id]
|
||||
|
||||
# Clear previous item select if it exists
|
||||
for item in self.children[:]:
|
||||
if isinstance(item, discord.ui.Select) and item.row == 1:
|
||||
self.remove_item(item)
|
||||
|
||||
# Add item select for this user
|
||||
options = []
|
||||
for item_id, item_data in user_items.items():
|
||||
options.append(discord.SelectOption(
|
||||
label=item_data["Item"][:25],
|
||||
description=f"ID: {item_id[:8]}...",
|
||||
value=f"{user_id}:{item_id}"
|
||||
))
|
||||
|
||||
if options:
|
||||
item_select = discord.ui.Select(
|
||||
placeholder="Select an item to process",
|
||||
options=options[:25],
|
||||
row=1
|
||||
)
|
||||
item_select.callback = self.item_selected
|
||||
self.add_item(item_select)
|
||||
|
||||
await interaction.response.edit_message(view=self)
|
||||
|
||||
async def item_selected(self, interaction: discord.Interaction):
|
||||
if interaction.user != ctx.author:
|
||||
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
|
||||
|
||||
user_id, item_id = interaction.data["values"][0].split(":")
|
||||
user = self.cog.bot.get_user(int(user_id))
|
||||
item_data = self.data[user_id][item_id]
|
||||
|
||||
# Create confirmation view
|
||||
confirm_view = PendingConfirmView(self.cog, user, item_id, item_data["Item"])
|
||||
embed = discord.Embed(
|
||||
title="Pending Item",
|
||||
description=f"Process pending item for {user.mention}",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Item", value=item_data["Item"])
|
||||
embed.add_field(name="ID", value=item_id)
|
||||
embed.add_field(name="Timestamp", value=item_data["Timestamp"])
|
||||
|
||||
await interaction.response.edit_message(embed=embed, view=confirm_view)
|
||||
|
||||
class PendingConfirmView(discord.ui.View):
|
||||
def __init__(self, cog, user, item_id, item_name, timeout=60):
|
||||
super().__init__(timeout=timeout)
|
||||
self.cog = cog
|
||||
self.user = user
|
||||
self.item_id = item_id
|
||||
self.item_name = item_name
|
||||
|
||||
@discord.ui.button(label="Approve", style=discord.ButtonStyle.green)
|
||||
async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user != ctx.author:
|
||||
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
|
||||
|
||||
async with instance.Pending() as p:
|
||||
del p[str(self.user.id)][self.item_id]
|
||||
if not p[str(self.user.id)]:
|
||||
del p[str(self.user.id)]
|
||||
|
||||
await interaction.response.edit_message(
|
||||
content=f"{self.item_name} was approved for {self.user.name}.",
|
||||
embed=None,
|
||||
view=None
|
||||
)
|
||||
try:
|
||||
await self.user.send(f"{ctx.author.name} approved your pending {self.item_name}!")
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label="Deny", style=discord.ButtonStyle.red)
|
||||
async def deny(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user != ctx.author:
|
||||
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
|
||||
|
||||
async with instance.Pending() as p:
|
||||
del p[str(self.user.id)][self.item_id]
|
||||
if not p[str(self.user.id)]:
|
||||
del p[str(self.user.id)]
|
||||
|
||||
await interaction.response.edit_message(
|
||||
content=f"{self.item_name} was denied for {self.user.name}.",
|
||||
embed=None,
|
||||
view=None
|
||||
)
|
||||
try:
|
||||
await self.user.send(f"{ctx.author.name} denied your pending {self.item_name}.")
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.grey)
|
||||
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user != ctx.author:
|
||||
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
|
||||
await interaction.response.edit_message(content="Action cancelled.", embed=None, view=None)
|
||||
self.stop()
|
||||
|
||||
# Start the pending menu
|
||||
view = PendingView(self, data)
|
||||
await ctx.send(
|
||||
"Select a user to view their pending items:",
|
||||
view=view
|
||||
)
|
||||
|
||||
@shop.command()
|
||||
@commands.guild_only()
|
||||
|
@ -774,22 +934,90 @@ class Shop(commands.Cog):
|
|||
await sm.remove(item)
|
||||
await ctx.send("{} was granted the {} role.".format(ctx.author.mention, role.name))
|
||||
|
||||
async def pending_prompt(self, ctx, instance, data, item):
|
||||
"""Handle item redemption with modern UI."""
|
||||
e = discord.Embed(color=await ctx.embed_colour())
|
||||
e.add_field(name=item, value=data[item]["Info"], inline=False)
|
||||
|
||||
class RedeemView(discord.ui.View):
|
||||
def __init__(self, cog, timeout=60):
|
||||
super().__init__(timeout=timeout)
|
||||
self.cog = cog
|
||||
self.value = None
|
||||
|
||||
@discord.ui.button(label="Redeem", style=discord.ButtonStyle.green)
|
||||
async def redeem(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user != 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()
|
||||
|
||||
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.red)
|
||||
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user != 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()
|
||||
|
||||
if data[item]["Type"].lower() == "role":
|
||||
prompt = f"{ctx.author.mention} Do you wish to redeem {item}? This will grant you the role assigned to this item and it will be removed from your inventory permanently."
|
||||
else:
|
||||
prompt = f"{ctx.author.mention} Do you wish to redeem {item}? This will add the item to the pending list for an admin to review and grant. The item will be removed from your inventory while this is processing."
|
||||
|
||||
view = RedeemView(self)
|
||||
msg = await ctx.send(prompt, embed=e, view=view)
|
||||
|
||||
try:
|
||||
await view.wait()
|
||||
if view.value is None:
|
||||
await msg.edit(content="Redemption timed out.", view=None)
|
||||
return
|
||||
elif not view.value:
|
||||
await msg.edit(content="Redemption cancelled.", view=None)
|
||||
return
|
||||
|
||||
if data[item]["Type"].lower() == "role":
|
||||
await self.assign_role(ctx, instance, item, data[item]["Role"])
|
||||
else:
|
||||
await self.pending_add(ctx, item)
|
||||
|
||||
sm = ShopManager(ctx, instance=None, user_data=instance)
|
||||
await sm.remove(item)
|
||||
|
||||
except Exception as exc:
|
||||
await msg.edit(content=f"An error occurred: {str(exc)}", view=None)
|
||||
|
||||
async def pending_add(self, ctx, item):
|
||||
"""Add an item to the pending list with modern UI."""
|
||||
instance = await self.get_instance(ctx, settings=True)
|
||||
unique_id = str(uuid.uuid4())[:17]
|
||||
timestamp = ctx.message.created_at.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
async with instance.Pending() as p:
|
||||
if str(ctx.author.id) in p:
|
||||
p[str(ctx.author.id)][unique_id] = {"Item": item, "Timestamp": timestamp}
|
||||
else:
|
||||
p[str(ctx.author.id)] = {unique_id: {"Item": item, "Timestamp": timestamp}}
|
||||
msg = "{} added {} to your pending list.".format(ctx.author.mention, item)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Item Pending",
|
||||
description=f"{ctx.author.mention} added {item} to the pending list.",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="ID", value=unique_id)
|
||||
embed.add_field(name="Timestamp", value=timestamp)
|
||||
|
||||
if await instance.Settings.Alerts():
|
||||
alert_role = await instance.Settings.Alert_Role()
|
||||
role = discord.utils.get(ctx.guild.roles, name=alert_role)
|
||||
if role:
|
||||
msg = "{}\n{}".format(role.mention, msg)
|
||||
await ctx.send(msg)
|
||||
await ctx.send(role.mention, embed=embed)
|
||||
else:
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
async def change_mode(self, mode):
|
||||
await self.config.clear_all()
|
||||
|
@ -879,40 +1107,6 @@ class Shop(commands.Cog):
|
|||
"list.".format(name.content)
|
||||
)
|
||||
|
||||
async def pending_prompt(self, ctx, instance, data, item):
|
||||
e = discord.Embed(color=await ctx.embed_colour())
|
||||
e.add_field(name=item, value=data[item]["Info"], inline=False)
|
||||
if data[item]["Type"].lower() == "Role":
|
||||
await ctx.send(
|
||||
"{} Do you wish to redeem {}? This will grant you the role assigned to "
|
||||
"this item and it will be removed from your inventory "
|
||||
"permanently.".format(ctx.author.mention, item),
|
||||
embed=e,
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
"{} Do you wish to redeem {}? This will add the item to the pending "
|
||||
"list for an admin to review and grant. The item will be removed from "
|
||||
"your inventory while this is "
|
||||
"processing.".format(ctx.author.mention, item),
|
||||
embed=e,
|
||||
)
|
||||
try:
|
||||
choice = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx).confirm)
|
||||
except asyncio.TimeoutError:
|
||||
return await ctx.send("No Response. Item redemption canceled.")
|
||||
|
||||
if choice.content.lower() != "yes":
|
||||
return await ctx.send("Canceled item redemption.")
|
||||
|
||||
if data[item]["Type"].lower() == "role":
|
||||
return await self.assign_role(ctx, instance, item, data[item]["Role"])
|
||||
else:
|
||||
await self.pending_add(ctx, item)
|
||||
|
||||
sm = ShopManager(ctx, instance=None, user_data=instance)
|
||||
await sm.remove(item)
|
||||
|
||||
|
||||
class ShopManager:
|
||||
def __init__(self, ctx, instance, user_data):
|
||||
|
@ -954,7 +1148,8 @@ class ShopManager:
|
|||
await asyncio.sleep(2) # At least a little buffer to prevent rate limiting
|
||||
await self.ctx.author.send(chunk)
|
||||
|
||||
async def order(self, shop, item):
|
||||
async def order(self, shop, item, quantity):
|
||||
"""Process a purchase order with the specified quantity."""
|
||||
try:
|
||||
async with self.instance.Shops() as shops:
|
||||
if shop not in shops:
|
||||
|
@ -972,55 +1167,27 @@ class ShopManager:
|
|||
if stock != "--" and stock <= 0:
|
||||
return await self.ctx.send(f"Sorry, {item} is out of stock.")
|
||||
|
||||
e = discord.Embed(color=await self.ctx.embed_colour())
|
||||
e.add_field(name=item, value=item_data["Info"], inline=False)
|
||||
e.add_field(name="Cost", value=f"{cost} {cur}", inline=True)
|
||||
e.add_field(name="Stock", value="Infinite" if stock == "--" else stock, inline=True)
|
||||
|
||||
text = (
|
||||
f"How many {item} would you like to purchase?\n*If this "
|
||||
f"is a random item, you can only buy 1 at a time.*"
|
||||
)
|
||||
await self.ctx.send(content=text, embed=e)
|
||||
# Validate quantity for random items
|
||||
if _type == "random" and quantity != 1:
|
||||
return await self.ctx.send("You can only buy 1 random item at a time.")
|
||||
|
||||
def predicate(m):
|
||||
if m.author == self.ctx.author and self.ctx.channel == m.channel:
|
||||
if m.content.isdigit():
|
||||
if _type == "random":
|
||||
return int(m.content) == 1
|
||||
try:
|
||||
amount = int(m.content)
|
||||
if stock == "--":
|
||||
return amount > 0
|
||||
return 0 < amount <= stock
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
elif m.content.lower() in ("exit", "cancel", "e", "x"):
|
||||
return True
|
||||
return False
|
||||
# Check if enough stock
|
||||
if stock != "--" and quantity > stock:
|
||||
return await self.ctx.send(f"Not enough stock! Only {stock} available.")
|
||||
|
||||
try:
|
||||
num = await self.ctx.bot.wait_for("message", timeout=25.0, check=predicate)
|
||||
except asyncio.TimeoutError:
|
||||
return await self.ctx.send("Purchase timed out.")
|
||||
|
||||
if num.content.lower() in ("exit", "cancel", "e", "x"):
|
||||
raise ExitProcess()
|
||||
|
||||
amount = int(num.content)
|
||||
total_cost = cost * amount
|
||||
total_cost = cost * quantity
|
||||
|
||||
try:
|
||||
await bank.withdraw_credits(self.ctx.author, total_cost)
|
||||
except ValueError:
|
||||
return await self.ctx.send(
|
||||
f"You cannot afford {amount}x {item} for {total_cost} {cur}. Transaction ended."
|
||||
f"You cannot afford {quantity}x {item} for {total_cost} {cur}. Transaction ended."
|
||||
)
|
||||
|
||||
# Handle different item types
|
||||
if _type == "auto":
|
||||
await self.auto_handler(shop, item, amount)
|
||||
await self.remove_stock(shop, item, stock, amount)
|
||||
await self.auto_handler(shop, item, quantity)
|
||||
await self.remove_stock(shop, item, stock, quantity)
|
||||
return await self.ctx.send("Message sent.")
|
||||
|
||||
if _type == "random":
|
||||
|
@ -1035,18 +1202,18 @@ class ShopManager:
|
|||
f"so {item} cannot be purchased."
|
||||
)
|
||||
else:
|
||||
await self.remove_stock(shop, item, stock, amount)
|
||||
await self.remove_stock(shop, item, stock, quantity)
|
||||
item = new_item
|
||||
async with self.instance.Shops() as shops:
|
||||
item_data = deepcopy(shops[shop]["Items"][new_item])
|
||||
stock = item_data["Qty"]
|
||||
|
||||
# Update stock and add to inventory
|
||||
await self.remove_stock(shop, item, stock, amount)
|
||||
await self.add_to_inventory(item, item_data, amount)
|
||||
await self.remove_stock(shop, item, stock, quantity)
|
||||
await self.add_to_inventory(item, item_data, quantity)
|
||||
|
||||
await self.ctx.send(
|
||||
f"{self.ctx.author.mention} purchased {amount}x {item} for {total_cost} {cur}."
|
||||
f"{self.ctx.author.mention} purchased {quantity}x {item} for {total_cost} {cur}."
|
||||
)
|
||||
|
||||
async def remove_stock(self, shop, item, current_stock, amount):
|
||||
|
|
248
shop/ui.py
Normal file
248
shop/ui.py
Normal file
|
@ -0,0 +1,248 @@
|
|||
import discord
|
||||
from discord.ui import View, Select, Button, button
|
||||
from typing import Optional, List, Dict, Any
|
||||
from redbot.core.utils.chat_formatting import box, humanize_list
|
||||
|
||||
class ShopView(View):
|
||||
def __init__(self, ctx, shops: Dict[str, Any], timeout: int = 60):
|
||||
super().__init__(timeout=timeout)
|
||||
self.ctx = ctx
|
||||
self.shops = shops
|
||||
self.current_shop = None
|
||||
self.current_item = None
|
||||
self.setup_shop_select()
|
||||
|
||||
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
|
||||
)
|
||||
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 = []
|
||||
|
||||
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
|
||||
)
|
||||
item_select.callback = self.item_selected
|
||||
self.add_item(item_select)
|
||||
|
||||
await interaction.response.edit_message(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)
|
||||
|
||||
class PurchaseView(View):
|
||||
def __init__(self, ctx, shop: str, item: str, item_data: Dict[str, Any], timeout: int = 60):
|
||||
super().__init__(timeout=timeout)
|
||||
self.ctx = ctx
|
||||
self.shop = shop
|
||||
self.item = item
|
||||
self.item_data = item_data
|
||||
self.quantity = 1
|
||||
|
||||
def build_embed(self) -> discord.Embed:
|
||||
e = discord.Embed(
|
||||
title=f"Purchase {self.item}",
|
||||
description=self.item_data["Info"],
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
qty = "Infinite" if self.item_data["Qty"] == "--" else self.item_data["Qty"]
|
||||
e.add_field(name="Stock", value=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)
|
||||
if self.item_data["Type"] == "role":
|
||||
e.add_field(name="Role", value=self.item_data["Role"], inline=True)
|
||||
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
|
||||
)
|
||||
|
||||
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=30.0, check=check)
|
||||
self.quantity = int(msg.content)
|
||||
await self.handle_purchase(interaction)
|
||||
except asyncio.TimeoutError:
|
||||
await interaction.followup.send("Purchase cancelled - took too long to respond.", ephemeral=True)
|
||||
|
||||
@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)
|
||||
|
||||
async def handle_purchase(self, interaction: discord.Interaction):
|
||||
# Validate quantity
|
||||
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
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
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.setup_inventory_select()
|
||||
|
||||
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)
|
||||
|
||||
item_name = interaction.data["values"][0]
|
||||
item_data = self.inventory[item_name]
|
||||
|
||||
embed = discord.Embed(
|
||||
title=item_name,
|
||||
description=item_data["Info"],
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Quantity", value=item_data["Qty"], inline=True)
|
||||
embed.add_field(name="Type", value=item_data["Type"].title(), inline=True)
|
||||
|
||||
if item_data["Type"] == "role":
|
||||
use_view = UseItemView(self.ctx, item_name, item_data)
|
||||
await interaction.response.edit_message(embed=embed, view=use_view)
|
||||
else:
|
||||
await interaction.response.edit_message(embed=embed)
|
||||
|
||||
class UseItemView(View):
|
||||
def __init__(self, ctx, item: str, item_data: Dict[str, Any], timeout: int = 60):
|
||||
super().__init__(timeout=timeout)
|
||||
self.ctx = ctx
|
||||
self.item = item
|
||||
self.item_data = item_data
|
||||
|
||||
@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)
|
||||
|
||||
await interaction.response.edit_message(
|
||||
content=f"Processing use of {self.item}...",
|
||||
embed=None,
|
||||
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)
|
||||
await interaction.response.edit_message(content="Cancelled.", embed=None, view=None)
|
||||
self.stop()
|
Loading…
Add table
Reference in a new issue