Add Shop Cog with inventory and trading features. Implement shop management commands for creating, editing, and deleting shops and items. Introduce inventory display and item redemption functionalities. Include example CSV for bulk item addition and enhance user experience with structured menus and checks.
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
3fcd50cbf7
commit
7fc2053951
10 changed files with 1872 additions and 1 deletions
|
@ -50,6 +50,7 @@ We extend our gratitude to the following developers and their amazing contributi
|
|||
- [Toxic Cogs](https://github.com/NeuroAssassin/Toxic-Cogs)
|
||||
- [Karlo Cogs](https://github.com/karlsbjorn/karlo-cogs)
|
||||
- [Mister-42 Cogs](https://github.com/Mister-42/mr42-cogs)
|
||||
- [Jumper Plugins](https://github.com/Redjumpman/Jumper-Plugins/tree/V3_dpy2) (V3_dpy2)
|
||||
|
||||
## 📜 Licenses
|
||||
This repository includes cogs that may be licensed under either:
|
||||
|
|
8
shop/__init__.py
Normal file
8
shop/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from .shop import Shop
|
||||
|
||||
__red_end_user_data_statement__ = "This cog stores discord IDs as needed for operation."
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
cog = Shop()
|
||||
await bot.add_cog(cog)
|
54
shop/checks.py
Normal file
54
shop/checks.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from collections.abc import Iterable
|
||||
|
||||
|
||||
class Checks:
|
||||
"""Class of predicates for waiting events.
|
||||
|
||||
This class requires you to pass ctx so that the person who
|
||||
invocated the command can be determined.
|
||||
|
||||
You may pass an optional iterable in custom to check if the
|
||||
content is a member of it.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx, custom: Iterable = None, length: int = None):
|
||||
self.ctx = ctx
|
||||
self.custom = custom
|
||||
self.length = length
|
||||
|
||||
def same(self, m):
|
||||
return self.ctx.author == m.author
|
||||
|
||||
def confirm(self, m):
|
||||
return self.same(m) and m.content.lower() in ("yes", "no")
|
||||
|
||||
def valid_int(self, m):
|
||||
return self.same(m) and m.content.isdigit()
|
||||
|
||||
def valid_float(self, m):
|
||||
try:
|
||||
return self.same(m) and float(m.content) >= 1
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def positive(self, m):
|
||||
return self.same(m) and m.content.isdigit() and int(m.content) > 0
|
||||
|
||||
def role(self, m):
|
||||
roles = [r.name for r in self.ctx.guild.roles if r.name != "Bot"]
|
||||
return self.same(m) and m.content in roles
|
||||
|
||||
def member(self, m):
|
||||
return self.same(m) and m.content in [x.name for x in self.ctx.guild.members]
|
||||
|
||||
def length_under(self, m):
|
||||
try:
|
||||
return self.same(m) and len(m.content) <= self.length
|
||||
except TypeError:
|
||||
raise ValueError("Length was not specified in Checks.")
|
||||
|
||||
def content(self, m):
|
||||
try:
|
||||
return self.same(m) and m.content in self.custom
|
||||
except TypeError:
|
||||
raise ValueError("A custom iterable was not set in Checks.")
|
11
shop/data/Example.csv
Normal file
11
shop/data/Example.csv
Normal file
|
@ -0,0 +1,11 @@
|
|||
Shop,Item,Type,Cost,Qty,Info,Role,Messages
|
||||
Holy Temple,Bread,basic,25,12,Hard as a rock.,,
|
||||
Holy Temple,Holy Water,basic,30,8,Strong against Demons.,,
|
||||
Holy Temple,Divine Training,role,500,5,Grants the Priest role.,Priest,
|
||||
Holy Temple,Healing Potion,basic,85,10,Recovers some health.,,
|
||||
Holy Temple,Enchanted Stave,basic,900,1,Divine blunt weapon.,,
|
||||
Holy Temple,Box of faith,random,100,0,Pray for prosperity.,,
|
||||
Holy Temple,Angelic Whisper,auto,5,6,Your prayers answered.,,"msg1, msg2, msg3, msg4, msg5, msg6"
|
||||
Holy Temple,Robes,basic,25,18,Simple cloth robes.,,
|
||||
Mage Tower,Wand of Inferno,basic,9000,1,Fiery magic implement.,,
|
||||
Mage Tower,Magic Beans,basic,5,3,These might be useful.,,
|
|
12
shop/info.json
Normal file
12
shop/info.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"author" : ["Redjumpman (Redjumpman#1337)"],
|
||||
"install_msg" : "Thank you for installing shop. Be sure to check out the wiki here: https://github.com/Redjumpman/Jumper-Plugins/wiki/Shop-Red-V3\nThis cog comes with a bundled CSV file as an example for adding bulk shops and items.",
|
||||
"name" : "Shop",
|
||||
"short" : "Create, buy, trade, and redeem items.",
|
||||
"requirements" : ["tabulate"],
|
||||
"description" : "Shop system that allows for multiple shops with their own list of items for sale. Players can purchase these items with economy currency and redeem them for roles, or other server defined value.",
|
||||
"permissions" : ["Manage Messages", "Embed Links", "Add Reactions", "Manage Roles"],
|
||||
"tags" : ["Economy", "Fun", "Shop"],
|
||||
"min_python_version": [3, 6, 0],
|
||||
"end_user_data_statement": "This cog stores discord IDs as needed for operation."
|
||||
}
|
97
shop/inventory.py
Normal file
97
shop/inventory.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
import asyncio
|
||||
import discord
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from .menu import MenuCheck
|
||||
|
||||
|
||||
class Inventory:
|
||||
def __init__(self, ctx, data):
|
||||
self.ctx = ctx
|
||||
self.data = data
|
||||
|
||||
async def display(self):
|
||||
msg, groups = await self.setup()
|
||||
try:
|
||||
return await self.inv_loop(groups, msg)
|
||||
except asyncio.TimeoutError:
|
||||
await msg.delete()
|
||||
await self.ctx.send("Menu timed out.")
|
||||
raise RuntimeError
|
||||
except ExitMenu:
|
||||
await self.ctx.send("Exited inventory.")
|
||||
raise RuntimeError
|
||||
|
||||
async def setup(self, groups=None, page=0, msg=None):
|
||||
if not groups:
|
||||
groups = self.splitter()
|
||||
options = self.update(groups, page)
|
||||
embed = self.build_embed(options, page, groups)
|
||||
if msg:
|
||||
await msg.edit(embed=embed)
|
||||
else:
|
||||
msg = await self.ctx.send(embed=embed)
|
||||
return msg, groups
|
||||
|
||||
async def inv_loop(self, groups, msg):
|
||||
page = 0
|
||||
maximum = len(groups) - 1
|
||||
while True:
|
||||
check = MenuCheck(self.ctx, groups, page, maximum)
|
||||
choice = await self.ctx.bot.wait_for("message", timeout=35.0, check=check.predicate)
|
||||
if choice.content.isdigit() and int(choice.content) in range(1, len(groups[page]) + 1):
|
||||
try:
|
||||
await choice.delete()
|
||||
await msg.delete()
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
pass
|
||||
return groups[page][int(choice.content) - 1][0]
|
||||
elif choice.content.lower() in (">", "n", "next"):
|
||||
page += 1
|
||||
elif choice.content.lower() in ("b", "<", "back"):
|
||||
page -= 1
|
||||
elif choice.content.lower() in ("e", "x", "exit"):
|
||||
try:
|
||||
await choice.delete()
|
||||
await msg.delete()
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
pass
|
||||
raise ExitMenu
|
||||
elif choice.content.lower() in ("p", "prev"):
|
||||
continue
|
||||
else:
|
||||
msg, _ = await self.setup(groups=groups, page=page, msg=msg)
|
||||
await msg.edit(embed=msg)
|
||||
msg, _ = await self.setup(groups=groups, page=page, msg=msg)
|
||||
|
||||
def splitter(self):
|
||||
return [self.data[i : i + 5] if len(self.data) > 5 else self.data for i in range(0, len(self.data), 5)]
|
||||
|
||||
def update(self, groups, page=0):
|
||||
header = f"{'#':<3} {'Items':<29} {'Qty':<7} {'Type':<8}\n{'--':<3} {'-'*29:<29} {'-'*4:<7} {'-'*8:<8}"
|
||||
fmt = [header]
|
||||
for idx, x in enumerate(groups[page], 1):
|
||||
line_one = f"{f'{idx}.': <{3}} {x[0]: <{28}s} {x[1]['Qty']: < {9}}{x[1]['Type']: <{7}s}"
|
||||
fmt.append(line_one)
|
||||
fmt.append(f'< {x[1]["Info"][:50]} >' if len(x[1]["Info"]) < 50 else f'< {x[1]["Info"][:47]}... >')
|
||||
fmt.append("",)
|
||||
return box("\n".join(fmt), lang="md")
|
||||
|
||||
def build_embed(self, options, page, groups):
|
||||
title = "{}'s Inventory".format(self.ctx.author.name)
|
||||
footer = "You are viewing page {} of {}.".format(page + 1 if page > 0 else 1, len(groups))
|
||||
instructions = (
|
||||
"Type the number for your selection or one of the words below "
|
||||
"for page navigation if there are multiple pages available.\n"
|
||||
"Next page: Type n, next, or >\n"
|
||||
"Previous page: Type b, back, or <\n"
|
||||
"Exit menu system: Type e, x, or exit"
|
||||
)
|
||||
embed = discord.Embed(color=0x5EC6FF)
|
||||
embed.add_field(name=title, value=options, inline=False)
|
||||
embed.set_footer(text="\n".join((instructions, footer)))
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
class ExitMenu(Exception):
|
||||
pass
|
246
shop/menu.py
Normal file
246
shop/menu.py
Normal file
|
@ -0,0 +1,246 @@
|
|||
import asyncio
|
||||
import discord
|
||||
from tabulate import tabulate
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
|
||||
class ShopMenu:
|
||||
def __init__(self, ctx, origin, mode=0, sorting="price"):
|
||||
self.ctx = ctx
|
||||
self.origin = origin
|
||||
self.shop = None
|
||||
self.user = None
|
||||
self.enabled = True
|
||||
self.mode = mode
|
||||
self.sorting = sorting
|
||||
|
||||
async def display(self):
|
||||
data = self.origin
|
||||
msg, groups, page, maximum = await self.setup(data)
|
||||
try:
|
||||
item = await self.menu_loop(data, groups, page, maximum, msg)
|
||||
except asyncio.TimeoutError:
|
||||
await msg.delete()
|
||||
await self.ctx.send("No response. Menu exited.")
|
||||
raise RuntimeError
|
||||
except MenuExit:
|
||||
await msg.delete()
|
||||
await self.ctx.send("Exited menu.")
|
||||
raise RuntimeError
|
||||
else:
|
||||
if self.mode == 0:
|
||||
return self.shop, item
|
||||
else:
|
||||
return self.user, item
|
||||
|
||||
async def setup(self, data=None, msg=None):
|
||||
if data is None:
|
||||
data = self.origin
|
||||
if (self.shop is None and self.mode == 0) or (self.user is None and self.mode == 1):
|
||||
data = await self.parse_data(data)
|
||||
|
||||
groups = self.group_data(data)
|
||||
page, maximum = 0, len(groups) - 1
|
||||
e = await self.build_menu(groups, page)
|
||||
|
||||
if msg is None:
|
||||
msg = await self.ctx.send(self.ctx.author.mention, embed=e)
|
||||
else:
|
||||
await msg.edit(embed=e)
|
||||
return msg, groups, page, maximum
|
||||
|
||||
async def menu_loop(self, data, groups, page, maximum, msg):
|
||||
while True:
|
||||
check = MenuCheck(self.ctx, groups, page, maximum)
|
||||
choice = await self.ctx.bot.wait_for("message", timeout=35.0, check=check.predicate)
|
||||
if choice.content.isdigit() and int(choice.content) in range(1, len(groups[page]) + 1):
|
||||
selection = groups[page][int(choice.content) - 1]
|
||||
try:
|
||||
await choice.delete()
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
pass
|
||||
if self.mode == 0:
|
||||
item = await self.next_menu(data, selection, msg)
|
||||
if not self.enabled:
|
||||
try:
|
||||
await msg.delete()
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
pass
|
||||
return item
|
||||
else:
|
||||
pending_id = await self.pending_menu(data, selection, msg)
|
||||
if not self.enabled:
|
||||
try:
|
||||
await msg.delete()
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
pass
|
||||
return pending_id
|
||||
if choice.content.lower() in (">", "n", "next"):
|
||||
page += 1
|
||||
elif choice.content.lower() in ("b", "<", "back"):
|
||||
page -= 1
|
||||
elif choice.content.lower() in ("p", "prev"):
|
||||
if (self.shop and self.mode == 0) or (self.user and self.mode == 1):
|
||||
try:
|
||||
await choice.delete()
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
pass
|
||||
if self.mode == 0:
|
||||
self.shop = None
|
||||
else:
|
||||
self.user = None
|
||||
break
|
||||
pass
|
||||
elif choice.content.lower() in ("e", "x", "exit"):
|
||||
try:
|
||||
await choice.delete()
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
pass
|
||||
raise MenuExit
|
||||
|
||||
try:
|
||||
await choice.delete()
|
||||
except discord.NotFound:
|
||||
msg, groups, page, maximum = await self.setup(msg=msg)
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
embed = await self.build_menu(groups, page=page)
|
||||
await msg.edit(embed=embed)
|
||||
|
||||
async def parse_data(self, data):
|
||||
if self.shop is None and self.mode == 0:
|
||||
perms = self.ctx.author.guild_permissions.administrator
|
||||
author_roles = [r.name for r in self.ctx.author.roles]
|
||||
return [x for x, y in data.items() if (y["Role"] in author_roles or perms) and y["Items"]]
|
||||
else:
|
||||
try:
|
||||
return list(data.items())
|
||||
except AttributeError:
|
||||
return data
|
||||
|
||||
async def build_menu(self, groups, page):
|
||||
footer = "You are viewing page {} of {}.".format(page + 1 if page > 0 else 1, len(groups))
|
||||
if self.shop is None and self.mode == 0:
|
||||
output = ["{} - {}".format(idx, ele) for idx, ele in enumerate(groups[page], 1)]
|
||||
elif self.mode == 0:
|
||||
header = f"{'#':<3} {'Name':<29} {'Qty':<7} {'Cost':<8}\n{'--':<3} {'-'*29:<29} {'-'*4:<7} {'-'*8:<8}"
|
||||
fmt = [header]
|
||||
for idx, x in enumerate(groups[page], 1):
|
||||
line_one = f"{f'{idx}.': <{3}} {x[0]: <{29}s} {x[1]['Qty']:<{8}}{x[1]['Cost']: < {7}}"
|
||||
fmt.append(line_one)
|
||||
fmt.append(f'< {x[1]["Info"][:50]} >' if len(x[1]["Info"]) < 50 else f'< {x[1]["Info"][:47]}... >')
|
||||
fmt.append("",)
|
||||
output = box("\n".join(fmt), "md")
|
||||
elif self.mode == 1 and self.user is None:
|
||||
headers = ("#", "User", "Pending Items")
|
||||
fmt = [
|
||||
(idx, discord.utils.get(self.ctx.bot.users, id=int(x[0])).name, len(x[1]))
|
||||
for idx, x in enumerate(groups[page], 1)
|
||||
]
|
||||
output = box(tabulate(fmt, headers=headers, numalign="left"), lang="md")
|
||||
elif self.mode == 1:
|
||||
headers = ("#", "Item", "Order ID", "Timestamp")
|
||||
fmt = [(idx, x[1]["Item"], x[0], x[1]["Timestamp"]) for idx, x in enumerate(groups[page], 1)]
|
||||
|
||||
output = box(tabulate(fmt, headers=headers, numalign="left"), lang="md")
|
||||
else:
|
||||
output = None
|
||||
return self.build_embed(output, footer)
|
||||
|
||||
def sorter(self, groups):
|
||||
if self.sorting == "name":
|
||||
return sorted(groups, key=lambda x: x[0])
|
||||
elif self.sorting == "price":
|
||||
return sorted(groups, key=lambda x: x[1]["Cost"], reverse=True)
|
||||
else:
|
||||
return sorted(groups, key=lambda x: x[1]["Quantity"], reverse=True)
|
||||
|
||||
def group_data(self, data):
|
||||
grouped = []
|
||||
for idx in range(0, len(data), 5):
|
||||
if len(data) > 5:
|
||||
grouped.append(self.sorter(data[idx : idx + 5]))
|
||||
else:
|
||||
if not isinstance(data, dict):
|
||||
grouped.append(data)
|
||||
else:
|
||||
grouped.append([self.sorter(data)])
|
||||
return grouped
|
||||
|
||||
def build_embed(self, options, footer):
|
||||
instructions = (
|
||||
"Type the number for your selection or one of the words below "
|
||||
"for page navigation if there are multiple pages available.\n"
|
||||
"Next page: Type n, next, or >\n"
|
||||
"Previous page: Type b, back, or <\n"
|
||||
"Return to previous menu: Type p or prev\n"
|
||||
"Exit menu system: Type e, x, or exit"
|
||||
)
|
||||
|
||||
if self.shop is None and self.mode == 0:
|
||||
options = "\n".join(options)
|
||||
|
||||
if self.mode == 0:
|
||||
title = "{}".format(self.shop if self.shop else "List of shops")
|
||||
else:
|
||||
title = "{} Pending".format(self.user.name if self.user else "Items")
|
||||
|
||||
embed = discord.Embed(color=0x5EC6FF)
|
||||
embed.add_field(name=title, value=options, inline=False)
|
||||
embed.set_footer(text="\n".join([instructions, footer]))
|
||||
|
||||
return embed
|
||||
|
||||
async def next_menu(self, data, selection, msg):
|
||||
try:
|
||||
items = data[selection]["Items"]
|
||||
except TypeError:
|
||||
self.enabled = False
|
||||
return selection[0]
|
||||
else:
|
||||
self.shop = selection
|
||||
new_data = await self.parse_data(items)
|
||||
msg, groups, page, maximum = await self.setup(data=new_data, msg=msg)
|
||||
return await self.menu_loop(new_data, groups, page, maximum, msg)
|
||||
|
||||
async def pending_menu(self, data, selection, msg):
|
||||
try:
|
||||
items = data[selection[0]]
|
||||
except TypeError:
|
||||
self.enabled = False
|
||||
return selection[0]
|
||||
else:
|
||||
self.user = discord.utils.get(self.ctx.bot.users, id=int(selection[0]))
|
||||
new_data = await self.parse_data(items)
|
||||
msg, groups, page, maximum = await self.setup(data=new_data, msg=msg)
|
||||
return await self.menu_loop(new_data, groups, page, maximum, msg)
|
||||
|
||||
|
||||
class MenuCheck:
|
||||
"""Special check class for menu.py"""
|
||||
|
||||
def __init__(self, ctx, data, page, maximum):
|
||||
self.ctx = ctx
|
||||
self.page = page
|
||||
self.maximum = maximum
|
||||
self.data = data
|
||||
|
||||
def predicate(self, m):
|
||||
choices = list(map(str, range(1, len(self.data[self.page]) + 1)))
|
||||
if self.ctx.author == m.author:
|
||||
if m.content in choices:
|
||||
return True
|
||||
elif m.content.lower() in ("exit", "prev", "p", "x", "e"):
|
||||
return True
|
||||
elif m.content.lower() in ("n", ">", "next") and (self.page + 1) <= self.maximum:
|
||||
return True
|
||||
elif m.content.lower() in ("b", "<", "back") and (self.page - 1) >= 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class MenuExit(Exception):
|
||||
pass
|
1423
shop/shop.py
Normal file
1423
shop/shop.py
Normal file
File diff suppressed because it is too large
Load diff
19
translator/menus.py
Normal file
19
translator/menus.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
import discord
|
||||
|
||||
class ButtonMenu(discord.ui.View):
|
||||
def __init__(self, embeds: list, timeout: int = 180):
|
||||
super().__init__(timeout=timeout)
|
||||
self.embeds = embeds
|
||||
self.current_page = 0
|
||||
|
||||
@discord.ui.button(label="◀️", style=discord.ButtonStyle.gray)
|
||||
async def previous(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page > 0:
|
||||
self.current_page -= 1
|
||||
await interaction.response.edit_message(embed=self.embeds[self.current_page])
|
||||
|
||||
@discord.ui.button(label="▶️", style=discord.ButtonStyle.gray)
|
||||
async def next(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if self.current_page < len(self.embeds) - 1:
|
||||
self.current_page += 1
|
||||
await interaction.response.edit_message(embed=self.embeds[self.current_page])
|
|
@ -5,7 +5,7 @@ from discord import app_commands
|
|||
import asyncio
|
||||
from datetime import datetime
|
||||
from redbot.core.bot import Red
|
||||
from . import ButtonMenu
|
||||
from .menus import ButtonMenu
|
||||
|
||||
class TranslatorCog(commands.Cog):
|
||||
"""Translate messages using Google Translate"""
|
||||
|
|
Loading…
Add table
Reference in a new issue