rewrite: Begin shop module.
This commit is contained in:
8
bot/modules/shop/__init__.py
Normal file
8
bot/modules/shop/__init__.py
Normal 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
109
bot/modules/shop/cog.py
Normal 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
87
bot/modules/shop/data.py
Normal 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)
|
||||
0
bot/modules/shop/shops/__init__.py
Normal file
0
bot/modules/shop/shops/__init__.py
Normal file
81
bot/modules/shop/shops/base.py
Normal file
81
bot/modules/shop/shops/base.py
Normal 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
|
||||
0
bot/modules/shop/shops/boosts.py
Normal file
0
bot/modules/shop/shops/boosts.py
Normal file
147
bot/modules/shop/shops/colours.py
Normal file
147
bot/modules/shop/shops/colours.py
Normal 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=""
|
||||
)
|
||||
...
|
||||
|
||||
|
||||
0
bot/modules/shop/shops/roles.py
Normal file
0
bot/modules/shop/shops/roles.py
Normal 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:
|
||||
|
||||
Reference in New Issue
Block a user