rewrite: Begin shop module.

This commit is contained in:
2022-12-05 04:29:57 +02:00
parent 72fd3c17f0
commit 4ef1b58007
9 changed files with 450 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
from babel import LocalBabel
babel = LocalBabel('shop')
async def setup(bot):
from .cog import Shopping
await bot.add_cog(Shopping(bot))

109
bot/modules/shop/cog.py Normal file
View File

@@ -0,0 +1,109 @@
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 . import babel
from .shops.colours import ColourShop
logger = logging.getLogger(__name__)
_p = babel._p
class Shopping(LionCog):
Shops = [ColourShop]
def __init__(self, bot: LionBot):
self.bot = bot
self.data = None
self.shops = []
async def cog_load(self):
for Shop in self.Shops:
shop = Shop(self.bot, self.data)
await shop.load_into(self)
self.shops.append(shop)
async def cog_unload(self):
for shop in self.shops:
await shop.unload()
@cmds.hybrid_group(name='editshop')
async def editshop_group(self, ctx: LionContext):
return
@cmds.hybrid_group(name='shop')
async def shop_group(self, ctx: LionContext):
return
@shop_group.command(name='open')
async def shop_open_cmd(self, ctx: LionContext):
"""
Opens the shop UI for the current guild.
"""
...
class StoreManager(ui.LeoUI):
def __init__(self, bot, data, shops):
self.bot = bot
self.data = data
self.shops = shops
self.page_num = 0
self._original: Optional[discord.Interaction] = None
self._store_row = self.make_buttons()
self._widgets = self.prepare_widgets()
async def redraw(self):
"""
Ask the current shop widget to redraw.
"""
...
def make_buttons(self):
"""
Make a tuple of shop buttons.
If there is only one shop, return an empty tuple.
"""
if len(self.shops) <= 1:
return ()
buttons = []
for i, shop in enumerate(self.shops):
@ui.AButton(label=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)
return tuple(buttons)
def prepare_widgets(self):
widgets = []
for shop in self.shops:
widget = shop.make_widget()
# TODO: Update this when we have a UILayout class
# widget.layout.set_row('shops', self._store_row, affinity=1)
widget.shop_row = self._store_row
widgets.append(widget)
return widgets
async def change_page(self, i):
"""
Change to the given page number.
"""
...
async def run(self, interaction):
...

87
bot/modules/shop/data.py Normal file
View File

@@ -0,0 +1,87 @@
from enums import Enum
from cachetools import TTLCache
from data import Registry, RowModel, RegisterEnum, WeakCache
from data.columns import Integer, String, Bool, Timestamp, Column
class ShopItemType(Enum):
"""
Schema
------
CREATE TYPE ShopItemType AS ENUM (
'COLOUR_ROLE'
);
"""
COLOUR = 'COLOUR_ROLE',
class ShopData(Registry, name='shop'):
_ShopItemType = RegisterEnum(ShopItemType)
class ShopItem(RowModel):
"""
Schema
------
CREATE TABLE shop_items(
itemid SERIAL PRIMARY KEY,
guildid BIGINT NOT NULL,
item_type ShopItemType NOT NULL,
price INTEGER NOT NULL,
purchasable BOOLEAN DEFAULT TRUE,
deleted BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT (now() at time zone 'utc')
);
CREATE INDEX guild_shop_items ON shop_items (guildid);
"""
_tablename_ = 'shop_items'
itemid = Integer(primary=True)
guildid = Integer()
item_type: Column[ShopItemType] = Column()
price = Integer()
purchasable = Bool()
deleted = Bool()
created_at = Timestamp()
class ColourRole(RowModel):
"""
Schema
------
CREATE TABLE shop_items_colour_roles(
itemid INTEGER PRIMARY KEY REFERENCES shop_items(itemid) ON DELETE CASCADE,
roleid BIGINT NOT NULL
);
"""
_tablename_ = 'shop_items_colour_roles'
itemid = Integer(primary=True)
roleid = Integer()
class MemberInventory(RowModel):
"""
Schema
------
CREATE TABLE member_inventory(
inventoryid SERIAL PRIMARY KEY,
guildid BIGINT NOT NULL,
userid BIGINT NOT NULL,
transactionid INTEGER REFERENCES coin_transactions(transactionid) ON DELETE SET NULL,
itemid INTEGER NOT NULL REFERENCES shop_items(itemid) ON DELETE CASCADE
);
CREATE INDEX member_inventory_members ON member_inventory(guildid, userid);
"""
_tablename_ = 'member_inventory'
inventoryid = Integer(primary=True)
guildid = Integer()
userid = Integer()
transactionid = Integer()
itemid = Integer()
async def fetch_inventory(self, guildid, userid) -> list['ShopData.MemberInventory']:
"""
Fetch the given member's inventory.
"""
return await self.fetch_where(guildid=guildid, userid=userid)

View File

View File

@@ -0,0 +1,81 @@
import discord
from discord.ui.button import Button
from meta import LionBot
from utils import ui
from ..data import ShopData
class MemberInventory:
"""
An interface to the member's inventory.
"""
def __init__(self, bot, shop_data, lion, inventory):
self.bot = bot
self.lion = lion
self.guildid = lion.guildid
self.userid = lion.userid
# A list of InventoryItems held by this user
self.inventory = inventory
@classmethod
async def fetch(cls, bot: LionBot, shop_data: ShopData, guildid: int, userid: int):
lion = await bot.core.lions.fetch(guildid, userid)
inventory = await shop_data.fetch_where(guildid=guildid, userid=userid)
return cls(bot, shop_data, lion, inventory)
async def refresh(self):
"""
Refresh the data for this member.
"""
self.lion = self.bot.core.lions.fetch(self.guild.id, self.user.id)
data = self.bot.get_cog('Shopping').data
self.inventory_items = await data.InventoryItem.fetch_where(userid=self.userid, guildid=self.guildid)
class ShopItem:
"""
ABC representing a purchasable guild shop item.
"""
def __init__(self, data):
self.data = data
async def purchase(self, userid):
"""
Called when a member purchases this item.
"""
...
class Shop:
"""
Base class representing a Shop for a particular member.
"""
def __init__(self, bot: LionBot, shop_data: ShopData, member: discord.Member):
self.bot = bot
self.data = shop_data
self.member = member
self.guild = member.guild
# A list of ShopItems that are currently visible to the member
self.items = []
# Current inventory for the member
self.inventory = None
async def refresh(self):
...
class Store(ui.LeoUI):
"""
Base UI for the different shops.
"""
def __init__(self, bot: LionBot, data, shops):
self.bot = bot
self.data = data
self.shops = shops

View File

View File

@@ -0,0 +1,147 @@
from typing import TYPE_CHECKING
import discord
from discord.ext import commands as cmds
from discord import app_commands as appcmds
from discord.ui.select import select, Select
from discord.ui.button import button, Button
from meta import LionCog, LionContext, LionBot
from utils import ui
from .data import ShopData
if TYPE_CHECKING:
from ..cog import Shopping
class ColourShopping(LionCog):
"""
Cog in charge of colour shopping.
Registers colour shop related commands and methods.
"""
def __init__(self, bot: LionBot, data):
self.bot = bot
self.data = data
async def load_into(self, cog: 'Shopping'):
self.crossload_group(self.editshop_group, cog.editshop_group)
await cog.bot.add_cog(self)
async def unload(self):
pass
@LionCog.placeholder_group
@cmds.hybrid_group('editshopp', with_app_command=False)
async def editshop_group(self, ctx: LionContext):
pass
@editshop_group.group('colours')
async def editshop_colour_group(self, ctx: LionContext):
...
@editshop_colour_group.command('edit')
async def editshop_colours_edit_cmd(self, ctx: LionContext):
await ctx.reply(f"I am a {self.__class__.__name__} version 2")
...
def make_widget(self):
"""
Instantiate and return a new UI for this shop.
"""
return ColourStore(self.bot, self.data)
def make_shop_for(self, member: discord.Member):
return ColourShop(member, self.data)
class ColourShop:
"""
A Shop representing a colour shop for a particular member.
Parameters
----------
bot: LionBot
The current LionBot.
member: discord.Member
The member this particular shop is for.
data: ShopData
An initialised ShopData registry.
"""
def __init__(self, bot, member, data):
self.bot = bot
self.user = member
self.guild = member.guild
self.data = data
# List of items in this shop. Initialised in refresh()
self.items = []
# Current inventory for this member
self.inventory = None
def make_store(self):
"""
Initialise and return a new Store UI for this shop.
"""
return ColourStore(self)
class ColourStore:
"""
Ephemeral UI providing access to the colour store.
"""
def __init__(self, shop: ColourShop):
self.bot = shop.bot
self.data = shop.data
self.shop = shop
self.shop_row = ()
async def refresh(self):
"""
Refresh the data.
"""
# Refresh current item list
# Refresh user's current item
...
async def redraw(self):
...
@select(placeholder="Select to Buy")
async def select_colour(self, interaction: discord.Interaction, selection: Select):
# User selected a colour from the list
# Run purchase pathway for that item
...
async def select_colour_refresh(self):
"""
Refresh the select colour menu.
For an item to be purchasable,
it needs to be affordable and not currently owned by the member.
"""
...
def make_embed(self):
"""
Embed for this shop.
"""
lines = []
for i, item in enumerate(self.shop.items):
line = f"[{i+1}] | `{item.price} LC` | <@&{item.data.roleid}>"
if item.itemid in self.shop.member_inventory.itemids:
line += " (You own this!)"
embed = discord.Embed(
title="Colour Role Shop",
description=""
)
...

View File

View File

@@ -1,3 +1,5 @@
BEGIN;
-- Add metdata to configuration tables {{{
ALTER TABLE user_config ADD COLUMN name TEXT;
ALTER TABLE user_config ADD COLUMN first_seen TIMESTAMPTZ DEFAULT now();
@@ -205,9 +207,25 @@ CREATE TABLE coin_transactions_admin_actions(
PRIMARY KEY (actionid, transactionid)
);
CREATE INDEX coin_transactions_admin_actions_transactionid ON coin_transactions_admin_actions (transactionid);
-- }}}
-- Shop data {{{
ALTER TABLE member_inventory DROP CONSTRAINT member_inventory_pkey;
ALTER TABLE member_inventory
ADD COLUMN inventoryid SERIAL PRIMARY KEY;
ALTER TABLE member_inventory
ADD COLUMN transactionid INTEGER REFERENCES coin_transactions (transactionid) ON DELETE SET NULL;
ALTER TABLE member_inventory
DROP COLUMN count;
CREATE INDEX member_inventory_members ON member_inventory(guildid, userid);
-- }}}
INSERT INTO VersionHistory (version, author) VALUES (13, 'v12-v13 migration');
COMMIT;
-- vim: set fdm=marker: