(Economy): Base shop system.
This commit is contained in:
215
bot/modules/economy/shop_cmds.py
Normal file
215
bot/modules/economy/shop_cmds.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import asyncio
|
||||
import discord
|
||||
from collections import defaultdict
|
||||
|
||||
from cmdClient.checks import in_guild
|
||||
|
||||
from .module import module
|
||||
from .shop_core import ShopItem
|
||||
|
||||
|
||||
class ShopSession:
|
||||
__slots__ = (
|
||||
'key', 'response',
|
||||
'_event', '_task'
|
||||
)
|
||||
_sessions = {}
|
||||
|
||||
def __init__(self, userid, channelid):
|
||||
# Build unique context key for shop session
|
||||
self.key = (userid, channelid)
|
||||
self.response = None
|
||||
|
||||
# Cancel any existing sessions
|
||||
if self.key in self._sessions:
|
||||
self._sessions[self.key].cancel()
|
||||
|
||||
self._event = asyncio.Event()
|
||||
self._task = None
|
||||
|
||||
# Add self to the session list
|
||||
self._sessions[self.key] = self
|
||||
|
||||
@classmethod
|
||||
def get(cls, userid, channelid) -> 'ShopSession':
|
||||
"""
|
||||
Get a ShopSession matching the given key, if it exists.
|
||||
Otherwise, returns None.
|
||||
"""
|
||||
return cls._sessions.get((userid, channelid), None)
|
||||
|
||||
async def wait(self, timeout=None):
|
||||
"""
|
||||
Wait for a buy response. Return the set response or raise an appropriate exception.
|
||||
"""
|
||||
self._task = asyncio.create_task(self._event.wait())
|
||||
try:
|
||||
await asyncio.wait_for(self._task, timeout=timeout)
|
||||
except asyncio.CancelledError:
|
||||
# Session was cancelled, likely due to creation of a new session
|
||||
raise
|
||||
except asyncio.TimeoutError:
|
||||
# Session timed out, likely due to reaching the timeout
|
||||
raise
|
||||
finally:
|
||||
if self._sessions.get(self.key, None) == self:
|
||||
self._sessions.pop(self.key, None)
|
||||
|
||||
return self.response
|
||||
|
||||
def set(self, response):
|
||||
"""
|
||||
Set response.
|
||||
"""
|
||||
self.response = response
|
||||
self._event.set()
|
||||
|
||||
def cancel(self):
|
||||
"""
|
||||
Cancel a session.
|
||||
"""
|
||||
if self._task:
|
||||
if self._sessions.get(self.key, None) == self:
|
||||
self._sessions.pop(self.key, None)
|
||||
self._task.cancel()
|
||||
else:
|
||||
raise ValueError("Cancelling a ShopSession that is already completed!")
|
||||
|
||||
|
||||
@module.cmd(
|
||||
'shop',
|
||||
group="Economy",
|
||||
desc="Open the guild shop.",
|
||||
flags=('add', 'remove', 'clear')
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_shop(ctx, flags):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}shop
|
||||
{prefix}shop <category>
|
||||
Description:
|
||||
Opens the guild shop.
|
||||
"""
|
||||
# TODO: (FUTURE) Register session (and cancel previous sessions) so we can track for buy
|
||||
|
||||
# Fetch all purchasable elements, this also populates the cache
|
||||
shop_items = ShopItem.fetch_where(guildid=ctx.guild.id, deleted=False, purchasable=True)
|
||||
|
||||
if not shop_items:
|
||||
# TODO: (FUTURE) Add tip for guild admins about setting up
|
||||
return await ctx.error_reply(
|
||||
"Nothing to buy! Please come back later."
|
||||
)
|
||||
|
||||
# Categorise items
|
||||
item_cats = defaultdict(list)
|
||||
for item in shop_items:
|
||||
item_cats[item.item_type].append(item)
|
||||
|
||||
# If there is more than one category, ask for which category they want
|
||||
item_type = None
|
||||
if len(item_cats) > 1:
|
||||
# TODO
|
||||
item_type = ...
|
||||
...
|
||||
else:
|
||||
item_type = next(iter(item_cats))
|
||||
|
||||
if item_type is not None:
|
||||
items = [item for item in item_cats[item_type]]
|
||||
embeds = items[0].cat_shop_embeds(
|
||||
ctx.guild.id,
|
||||
[item.itemid for item in items]
|
||||
)
|
||||
|
||||
# Present shop pages
|
||||
out_msg = await ctx.pager(embeds, add_cancel=True)
|
||||
await ctx.cancellable(out_msg, add_reaction=False)
|
||||
while True:
|
||||
try:
|
||||
response = await ShopSession(ctx.author.id, ctx.ch.id).wait(timeout=300)
|
||||
except asyncio.CancelledError:
|
||||
# User opened a new session
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
# Session timed out. Close the shop.
|
||||
# TODO: (FUTURE) time out shop session by removing hint.
|
||||
try:
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.red(),
|
||||
description="Shop closed! (Session timed out.)"
|
||||
)
|
||||
await out_msg.edit(
|
||||
embed=embed
|
||||
)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
break
|
||||
|
||||
# Selection was made
|
||||
|
||||
# Sanity checks
|
||||
# TODO: (FUTURE) Handle more flexible ways of selecting items
|
||||
if int(response.args) >= len(items):
|
||||
await response.error_reply(
|
||||
"No item with this number exists! Please try again."
|
||||
)
|
||||
continue
|
||||
|
||||
item = items[int(response.args)]
|
||||
if item.price > ctx.alion.coins:
|
||||
await response.error_reply(
|
||||
"Sorry, {} costs `{}` LionCoins, but you only have `{}`!".format(
|
||||
item.display_name,
|
||||
item.price,
|
||||
ctx.alion.coins
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Run the selection and keep the shop open in case they want to buy more.
|
||||
# TODO: (FUTURE) The item may no longer exist
|
||||
success = await item.buy(response)
|
||||
if success and not item.allow_multi_select:
|
||||
try:
|
||||
await out_msg.delete()
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
break
|
||||
|
||||
|
||||
@module.cmd(
|
||||
'buy',
|
||||
group="Hidden",
|
||||
desc="Buy an item from the guild shop."
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_buy(ctx):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}buy <number>
|
||||
Description:
|
||||
Only usable while you have a shop open (see `{prefix}shop`).
|
||||
|
||||
Buys the selected item from the shop.
|
||||
"""
|
||||
# Check relevant session exists
|
||||
session = ShopSession.get(ctx.author.id, ctx.ch.id)
|
||||
if session is None:
|
||||
return await ctx.error_reply(
|
||||
"No shop open, nothing to buy!\n"
|
||||
"Please open a shop with `{prefix}shop` first.".format(prefix=ctx.best_prefix)
|
||||
)
|
||||
# Check input is an integer
|
||||
if not ctx.args.isdigit():
|
||||
return await ctx.error_reply(
|
||||
"**Usage:** `{prefix}buy <number>`, for example `{prefix}buy 1`.".format(prefix=ctx.best_prefix)
|
||||
)
|
||||
|
||||
# Pass context back to session
|
||||
session.set(ctx)
|
||||
|
||||
|
||||
# TODO: (FUTURE) Buy command short-circuiting the shop command and acting on shop sessions
|
||||
# TODO: (FUTURE) Inventory command showing the user's purchases
|
||||
Reference in New Issue
Block a user