(Economy): Completed base shop UI.

This commit is contained in:
2021-09-15 09:24:37 +03:00
parent b25380d072
commit bac72194a3
5 changed files with 251 additions and 18 deletions

View File

@@ -5,7 +5,8 @@ from collections import defaultdict
from cmdClient.checks import in_guild from cmdClient.checks import in_guild
from .module import module from .module import module
from .shop_core import ShopItem from .shop_core import ShopItem, ShopItemType, ColourRole
from wards import is_guild_admin
class ShopSession: class ShopSession:
@@ -80,23 +81,36 @@ class ShopSession:
'shop', 'shop',
group="Economy", group="Economy",
desc="Open the guild shop.", desc="Open the guild shop.",
flags=('add', 'remove', 'clear') flags=('add==', 'remove==', 'clear')
) )
@in_guild() @in_guild()
async def cmd_shop(ctx, flags): async def cmd_shop(ctx, flags):
""" """
Usage``: Usage``:
{prefix}shop {prefix}shop
{prefix}shop <category> {prefix}shop --add <price>, <item>
{prefix}shop --remove
{prefix}shop --remove itemid, itemid, ...
{prefix}shop --remove itemname, itemname, ...
{prefix}shop --clear
Description: Description:
Opens the guild shop. Opens the guild shop. Visible items may be bought using `{prefix}buy`.
*Modifying the guild shop requires administrator permissions.*
""" """
# TODO: (FUTURE) Register session (and cancel previous sessions) so we can track for buy # TODO: (FUTURE) Register session (and cancel previous sessions) so we can track for buy
# Whether we are modifying the shop
modifying = any(value is not False for value in flags.values())
if modifying and not is_guild_admin(ctx.author):
return await ctx.error_reply(
"You need to be a guild admin to modify the shop!"
)
# Fetch all purchasable elements, this also populates the cache # Fetch all purchasable elements, this also populates the cache
shop_items = ShopItem.fetch_where(guildid=ctx.guild.id, deleted=False, purchasable=True) shop_items = ShopItem.fetch_where(guildid=ctx.guild.id, deleted=False, purchasable=True)
if not shop_items: if not shop_items and not modifying:
# TODO: (FUTURE) Add tip for guild admins about setting up # TODO: (FUTURE) Add tip for guild admins about setting up
return await ctx.error_reply( return await ctx.error_reply(
"Nothing to buy! Please come back later." "Nothing to buy! Please come back later."
@@ -108,21 +122,38 @@ async def cmd_shop(ctx, flags):
item_cats[item.item_type].append(item) item_cats[item.item_type].append(item)
# If there is more than one category, ask for which category they want # If there is more than one category, ask for which category they want
item_type = None # All FUTURE TODO stuff, to be refactored into a shop widget
if len(item_cats) > 1: # item_type = None
# TODO # if ctx.args:
item_type = ... # # Assume category has been entered
... # ...
else: # elif len(item_cats) > 1:
item_type = next(iter(item_cats)) # # TODO: Show selection menu
# item_type = ...
# ...
# else:
# # Pick the next one automatically
# item_type = next(iter(item_cats))
item_type = ShopItemType.COLOUR_ROLE
item_class = ColourRole
if item_type is not None: if item_type is not None:
items = [item for item in item_cats[item_type]] items = [item for item in item_cats[item_type]]
embeds = items[0].cat_shop_embeds( embeds = item_class.cat_shop_embeds(
ctx.guild.id, ctx.guild.id,
[item.itemid for item in items] [item.itemid for item in items]
) )
if modifying:
if flags['add']:
await item_class.parse_add(ctx, flags['add'])
elif flags['remove'] is not False:
await item_class.parse_remove(ctx, flags['remove'], items)
elif flags['clear']:
await item_class.parse_clear(ctx)
return
# Present shop pages # Present shop pages
out_msg = await ctx.pager(embeds, add_cancel=True) out_msg = await ctx.pager(embeds, add_cancel=True)
await ctx.cancellable(out_msg, add_reaction=False) await ctx.cancellable(out_msg, add_reaction=False)

View File

@@ -1,9 +1,12 @@
import re
import asyncio
from typing import List from typing import List
import datetime import datetime
import discord import discord
from cmdClient.lib import SafeCancellation from cmdClient.lib import SafeCancellation
from meta import client from meta import client
from utils.lib import multiselect_regex, parse_ranges
from .ShopItem import ShopItem, ShopItemType from .ShopItem import ShopItem, ShopItemType
from .data import shop_items, shop_item_info, colour_roles from .data import shop_items, shop_item_info, colour_roles
@@ -27,6 +30,10 @@ class ColourRole(ShopItem):
def roleid(self) -> int: def roleid(self) -> int:
return self.data.roleid return self.data.roleid
@property
def role(self) -> discord.Role:
return self.guild.get_role(self.roleid)
@classmethod @classmethod
async def create(cls, guildid, price, roleid, **kwargs): async def create(cls, guildid, price, roleid, **kwargs):
""" """
@@ -45,7 +52,7 @@ class ColourRole(ShopItem):
# Shop interface # Shop interface
@classmethod @classmethod
def _cat_shop_embed_items(cls, items: List['ColourRole'], **kwargs) -> List[discord.Embed]: def _cat_shop_embed_items(cls, items: List['ColourRole'], hint: str = buy_hint, **kwargs) -> List[discord.Embed]:
""" """
Embed a list of items specifically for displaying in the shop. Embed a list of items specifically for displaying in the shop.
Subclasses will usually extend or override this, if only to add metadata. Subclasses will usually extend or override this, if only to add metadata.
@@ -56,7 +63,7 @@ class ColourRole(ShopItem):
embeds = cls._cat_embed_items(items, **kwargs) embeds = cls._cat_embed_items(items, **kwargs)
for embed in embeds: for embed in embeds:
embed.title = "{} shop!".format(cls.item_type.desc) embed.title = "{} shop!".format(cls.item_type.desc)
embed.description += "\n\n" + cls.buy_hint embed.description += "\n\n" + hint
else: else:
embed = discord.Embed( embed = discord.Embed(
title="{} shop!".format(cls.item_type.desc), title="{} shop!".format(cls.item_type.desc),
@@ -171,3 +178,192 @@ class ColourRole(ShopItem):
return False return False
return True return True
# Shop admin interface
@classmethod
async def parse_add(cls, ctx, args):
"""
Parse a request to add colour roles.
Syntax: `<price>, <role>`, with different lines treated as different entries.
Assumes the author is an admin.
"""
if not args:
raise SafeCancellation("No arguments given, nothing to do!")
lines = args.splitlines()
to_add = [] # List of (price, role) tuples to add
for line in lines:
# Syntax: <price>, <role>
splits = line.split(',')
splits = [split.strip() for split in splits]
if len(splits) < 2 or not splits[0].isdigit():
raise SafeCancellation("**Syntax:** `--add <price>, <role>`")
price = int(splits[0])
role = await ctx.find_role(splits[1], create=True, interactive=True, allow_notfound=False)
to_add.append((price, role))
# Add the roles to data
for price, role in to_add:
# TODO: Batch update would be best
await cls.create(ctx.guild.id, price, role.id)
# Report back
if len(to_add) > 1:
await ctx.reply(
embed=discord.Embed(
title="Shop Updated",
description="Added `{}` new colours to the shop!".format(len(to_add))
)
)
else:
await ctx.reply(
embed=discord.Embed(
title="Shop Updated",
description="Added {} to the shop for `{}` coins.".format(to_add[0][1].mention, to_add[0][0])
)
)
@classmethod
async def parse_remove(cls, ctx, args, items):
"""
Parse a request to remove colour roles.
Syntax: `<ids>` or `<command separated roles>`
Assumes the author is an admin.
"""
if not items:
raise SafeCancellation("Colour shop is empty, nothing to delete!")
to_delete = []
if args:
if re.search(multiselect_regex, args):
# ids were selected
indexes = parse_ranges(args)
to_delete = [items[index] for index in indexes if index < len(items)]
if not to_delete:
# Assume comma separated list of roles
splits = args.split(',')
splits = [split.strip() for split in splits]
available_roles = (item.role for item in items)
available_roles = [role for role in available_roles if role]
roles = [
await ctx.find_role(rolestr, collection=available_roles, interactive=True, allow_notfound=False)
for rolestr in splits
]
roleids = set(role.id for role in roles)
to_delete = [item for item in items if item.roleid in roleids]
else:
# Interactive multi-selector
itemids = [item.itemid for item in items]
embeds = cls.cat_shop_embeds(
ctx.guild.id,
itemids,
hint=("Please select colour(s) ids to remove, or `c` to cancel.\n"
"(Respond with e.g. `1, 2, 3` or `1-3`.)")
)
out_msg = await ctx.pager(embeds)
def check(msg):
valid = msg.channel == ctx.ch and msg.author == ctx.author
valid = valid and (re.search(multiselect_regex, msg.content) or msg.content.lower() == 'c')
return valid
try:
message = await ctx.client.wait_for('message', check=check, timeout=60)
except asyncio.TimeoutError:
await out_msg.delete()
await ctx.error_reply("Session timed out. No colour roles were removed.")
return
try:
await out_msg.delete()
await message.delete()
except discord.HTTPException:
pass
if message.content.lower() == 'c':
return
to_delete = [
items[index]
for index in parse_ranges(message.content) if index < len(items)
]
if not to_delete:
raise SafeCancellation("Nothing to delete!")
# Build an ack string before we delete
rolestr = to_delete[0].role.mention if to_delete[0].role else "`{}`".format(to_delete[0].roleid)
# Delete the items
shop_items.delete_where(itemid=[item.itemid for item in to_delete])
# Update the info cache
[shop_item_info.row_cache.pop(item.itemid, None) for item in to_delete]
# Ack and log
if len(to_delete) > 1:
try:
await ctx.reply(
embed=discord.Embed(
title="Colour Roles removed",
description="You have removed `{}` colour roles.".format(len(to_delete)),
colour=discord.Colour.orange()
)
)
except discord.HTTPException:
pass
event_log = ctx.guild_settings.event_log.value
if event_log:
try:
await event_log.send(
embed=discord.Embed(
title="Colour Roles deleted",
description="{} removed `{}` colour roles from the shop.".format(
ctx.author.mention,
len(to_delete)
),
timestamp=datetime.datetime.utcnow()
)
)
except discord.HTTPException:
pass
else:
try:
await ctx.reply(
embed=discord.Embed(
title="Colour Role removed",
description="You have removed the colour role {}.".format(rolestr),
colour=discord.Colour.orange()
)
)
except discord.HTTPException:
pass
event_log = ctx.guild_settings.event_log.value
if event_log:
try:
await event_log.send(
embed=discord.Embed(
title="Colour Role deleted",
description="{} removed the colour role {} from the shop.".format(
ctx.author.mention,
rolestr
),
timestamp=datetime.datetime.utcnow()
)
)
except discord.HTTPException:
pass
@classmethod
async def parse_clear(cls, ctx):
"""
Parse a request to clear colour roles.
Assumes the author is an admin.
"""
if await ctx.ask("Are you sure you want to remove all colour roles from the shop?"):
shop_items.delete_where(guildid=ctx.guild.id, item_type=cls.item_type)
await ctx.reply("All colour roles deleted.")
await ctx.guild_settings.event_log.log("{} cleared the colour role shop.".format(ctx.author.mention))

View File

@@ -27,7 +27,7 @@ class ShopItem:
item_type = None # type: ShopItemType item_type = None # type: ShopItemType
# Format string to use for each item of this type in the shop embed # Format string to use for each item of this type in the shop embed
shop_fmt = "`[{num:<{num_len}}]` | `{item.price:<{price_len}}` {item.display_name}" shop_fmt = "`[{num:<{num_len}}]` | `{item.price:<{price_len}} LC` {item.display_name}"
# Shop input modifiers # Shop input modifiers
allow_multi_select = True allow_multi_select = True
@@ -248,7 +248,7 @@ class ShopItem:
# Ensure cache is populated # Ensure cache is populated
shop_item_info.fetch_rows_where(itemid=itemids) shop_item_info.fetch_rows_where(itemid=itemids)
return cls._cat_shop_embed_items([cls(itemid) for itemid in itemids]) return cls._cat_shop_embed_items([cls(itemid) for itemid in itemids], **kwargs)
async def buy(self, ctx): async def buy(self, ctx):
""" """

View File

@@ -1,4 +1,4 @@
from . import data from . import data
from .ShopItem import ShopItem from .ShopItem import ShopItem, ShopItemType
from .ColourRole import ColourRole from .ColourRole import ColourRole

View File

@@ -510,3 +510,9 @@ class FieldEnum(str, Enum):
def __conform__(self, proto): def __conform__(self, proto):
return QuotedString(self.value) return QuotedString(self.value)
multiselect_regex = re.compile(
r"^([0-9, -]+)$",
re.DOTALL | re.IGNORECASE | re.VERBOSE
)