Files
croccybot/src/modules/shop/cog.py

255 lines
8.4 KiB
Python

"""
The initial Shopping Cog acts as the entry point to the shopping system.
It provides the commands:
/shop open [type:<acmpl str>]
/shop buy <item:<acmpl str>>
It also provides the "/editshop" group,
which each shop type should hook into (via placeholder groups)
to provide a shop creation and edit interface.
A "Shop" represents a colour shop, role shop, boost shop, etc.
Each Shop has a different editing interface (the ShopCog),
and a different user interface page (the Store).
When a user runs /shop open, the "StoreManager" opens.
This PagedUI serves as a top-level manager for each shop store,
even though it has no UI elements itself.
The user sees the first Store page, and can freely use the store UI
to interact with that Shop.
They may also use the "shop row" buttons to switch between shops.
Now, onto the individual shop systems.
When the user first opens a shop, a Customer is created.
The Customer represents the member themselves (i.e. the lion),
along with their inventory (a list of raw items that they own).
For fast access (for e.g. autocomplete), the Customer may be
cached, as long as it has a unique registry map.
Each initialised Shop has a collection of ShopItems.
The ShopItems represent individual objects of that shop,
and usually contain state particular to that shop.
The ShopItems shouldn't need to remember their origin shop,
or the Customer.
The Shop itself handles purchases, checking whether a customer can purchase
a given item, and running the purchase logic for an item.
Note: Timing out Acmpl state to handle caching shared states between acmpl?
Note: shop_item_info table which joins the others? Include guild_item_id,
and cache the items.
/shop open:
- Create the Customer
- Initialise the Shops, with the current Guild and the Customer
- Pass the shop stores to the StoreManager, along with the command interaction
- Run display on the StoreManager, which displays the first Store UI
- The StoreManager gets a store button from each store, then passes those back to the individuals
- This would be best done with a UILayout modification, but we don't have that yet
- The Store displays its UI, which is Customer-dependent, relying on the Shop for most computations.
- (The Store might not even need to keep the Customer, just using Shop methods to access them.)
- The Customer may make a purchase/refund/etc, which gets mapped back to the Shop.
- After the Customer has made an action, the Store refreshes its UI, from the Shop data.
/shop buy <item>:
- Instantiates the Customer and Shops (via acmpl state?), which should _not_ require any data lookups.
- Acmpl shows an intelligent list of matching items, nicely formatted
- (Shopitem itself can be responsible for formatting?)
- (Shop can be responsible for showing which items the user can purchase?)
- Command gets the shopitemid (possibly partitioned by guild), gets that item, gets the item Shop.
- Command then gets the Shop to purchase that item.
"""
import asyncio
import logging
from typing import Optional
import discord
from discord.ext import commands as cmds
from discord import app_commands as appcmds
from meta import LionBot, LionCog, LionContext
from utils import ui
from utils.lib import error_embed
from wards import low_management_ward
from . import babel
from .shops.base import Customer, ShopCog
from .data import ShopData
logger = logging.getLogger(__name__)
_p = babel._p
class Shopping(LionCog):
# List of active Shop cogs
ShopCogs = ShopCog.active
def __init__(self, bot: LionBot):
self.bot = bot
self.data = bot.db.load_registry(ShopData())
self.active_cogs = []
async def cog_load(self):
await self.data.init()
for SCog in self.ShopCogs:
shop_cog = SCog(self.bot, self.data)
await shop_cog.load_into(self)
self.active_cogs.append(shop_cog)
async def cog_unload(self):
# for shop in self.shops:
# await shop.unload()
pass
@cmds.hybrid_group(
name=_p('group:editshop', 'editshop')
)
@appcmds.guild_only
@appcmds.default_permissions(manage_guild=True)
@low_management_ward
async def editshop_group(self, ctx: LionContext):
return
@cmds.hybrid_group(
name=_p('cmd:shop', 'shop'),
description=_p('cmd:shop|desc', "Purchase coloures, roles, and other goodies with LionCoins.")
)
@appcmds.guild_only
async def shop_group(self, ctx: LionContext):
return
@shop_group.command(
name=_p('cmd:shop_open', 'open'),
description=_p('cmd:shop_open|desc', "Open the server shop.")
)
@appcmds.guild_only
async def shop_open_cmd(self, ctx: LionContext):
"""
Opens the shop UI for the current guild.
"""
t = self.bot.translator.t
# Typechecker guards
if not ctx.guild:
return
if not ctx.interaction:
return
await ctx.interaction.response.defer(ephemeral=True, thinking=True)
# Create the Customer
customer = await Customer.fetch(self.bot, self.data, ctx.guild.id, ctx.author.id)
# Create the Shops
shops = [await cog.make_shop_for(customer) for cog in self.active_cogs]
# TODO: Filter by shops which actually have items
if not shops:
await ctx.reply(
embed=error_embed(
t(_p('cmd:shop_open|error:no_shops', "There is nothing to buy!"))
),
ephemeral=True
)
return
# Extract the Stores
stores = [shop.make_store(ctx.interaction) for shop in shops]
# Build the StoreManager from the Stores
manager = StoreManager(self.bot, self.data, stores)
# Display the StoreManager
await manager.run(ctx.interaction)
await manager.wait()
# TODO: shortcut shop buy command
class StoreManager(ui.LeoUI):
def __init__(self, bot, data, stores, **kwargs):
super().__init__(**kwargs)
self.bot = bot
self.data = data
self.stores = stores
self.page_num = 0
# Original interaction that opened this shop
self._original: Optional[discord.Interaction] = None
# tuple of Buttons to each active store
self._store_row = self.make_buttons()
async def redraw(self):
"""
Ask the current shop widget to redraw.
"""
self.page_num %= len(self.stores)
store = self.stores[self.page_num]
await store.refresh()
def make_buttons(self):
"""
Make a tuple of shop buttons.
If there is only one shop, returns an empty tuple.
"""
t = self.bot.translator.t
buttons = []
if len(self.stores) > 1:
for i, store in enumerate(self.stores):
@ui.AButton(label=store.shop.name)
async def pressed_switch_shop(press: discord.Interaction, pressed):
await press.response.defer()
await self.change_page(i)
buttons.append(pressed_switch_shop)
@ui.AButton(
label=t(_p('ui:stores|button:close|label', "Close")),
emoji=self.bot.config.emojis.getemoji('cancel')
)
async def pressed_close(press: discord.Interaction, pressed):
await press.response.defer()
if not self._original.is_expired():
embed = discord.Embed(
title=t(_p('ui:stores|button:close|response|title', "Shop Closed")),
colour=discord.Colour.orange()
)
await self._original.edit_original_response(embed=embed, view=None)
await self.close()
buttons.append(pressed_close)
for button in buttons:
self.add_item(button)
return tuple(buttons)
async def change_page(self, i):
"""
Change to the given page number.
"""
self.page_num = i
self.page_num %= len(self.stores)
await self.redraw()
async def run(self, interaction):
self._original = interaction
for store in self.stores:
self.children.append(store)
store.set_store_row(self._store_row)
await self.redraw()
async def monitor(self):
"""
When one of the stores closes, we want all the stores to close,
along with this parent UI.
"""
# TODO
...