interactive collection browser with dashboard, pagination, rarity filter buttons
This commit is contained in:
1 parent
11d7c139e6
commit
9500a2902f
1 file changed
+218
-113
+218
-113
@@ -1,5 +1,5 @@
|
|||||||
#this is the inventory cog for viewing collections and eligma balance##
|
#this is the inventory cog — interactive collection browser with buttons##
|
||||||
"""Collection and eligma commands for the Blue Archive gacha system."""
|
"""Interactive collection browser for the Blue Archive gacha system."""
|
||||||
######################################################################
|
######################################################################
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -11,26 +11,232 @@ from .data import db
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PAGE_SIZE = 10
|
||||||
|
RARITY_ICON = {3: "<:purple:>", 2: "<:gold:>", 1: "<:blue:>"}
|
||||||
|
RARITY_SYMBOL = {3: "◆", 2: "⬟", 1: "●"}
|
||||||
|
RARITY_LABEL = {3: "3★", 2: "2★", 1: "1★"}
|
||||||
|
RARITY_COLOR = {3: 0xE6B8FF, 2: 0xFFF58C, 1: 0xAAD4FF}
|
||||||
ELIGMA_YIELD = {1: 1, 2: 10, 3: 50}
|
ELIGMA_YIELD = {1: 1, 2: 10, 3: 50}
|
||||||
|
|
||||||
|
# Pool totals for progress bars (standard pool only)
|
||||||
|
POOL_TOTALS = {3: 106, 2: 22, 1: 11}
|
||||||
|
|
||||||
|
PAGE_LEFT = "◀"
|
||||||
|
PAGE_RIGHT = "▶"
|
||||||
|
FILTER_ALL = "All"
|
||||||
|
FILTER_LABELS = {FILTER_ALL: None, "3★": 3, "2★": 2, "1★": 1}
|
||||||
|
|
||||||
|
|
||||||
|
def _bar(current: int, total: int, width: int = 8) -> str:
|
||||||
|
"""Unicode progress bar: ██████░░ 75%"""
|
||||||
|
if total == 0:
|
||||||
|
return "░" * width + " 0%"
|
||||||
|
pct = min(current / total, 1.0)
|
||||||
|
filled = int(pct * width)
|
||||||
|
return "█" * filled + "░" * (width - filled) + f" {int(pct * 100)}%"
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryView(discord.ui.View):
|
||||||
|
"""Interactive view with buttons for collection browsing."""
|
||||||
|
|
||||||
|
def __init__(self, uid: int, owner_id: int):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.uid = uid
|
||||||
|
self.owner_id = owner_id
|
||||||
|
self.page = 0
|
||||||
|
self.filter_rarity: int | None = None
|
||||||
|
self._update_buttons()
|
||||||
|
|
||||||
|
# ── helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _get_rows(self) -> list[tuple[int, str, int, int]]:
|
||||||
|
"""Return [(student_id, name, count, rarity), ...] filtered and sorted."""
|
||||||
|
rows = gacha_db.get_inventory(self.uid)
|
||||||
|
result = []
|
||||||
|
for sid, count in rows:
|
||||||
|
s = db.get(sid)
|
||||||
|
if s:
|
||||||
|
r = s["StarGrade"]
|
||||||
|
if self.filter_rarity and r != self.filter_rarity:
|
||||||
|
continue
|
||||||
|
result.append((sid, s["Name"], count, r))
|
||||||
|
result.sort(key=lambda x: (-x[3], x[1]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _build_dashboard(self) -> discord.Embed:
|
||||||
|
"""Landing page with stats and progress bars."""
|
||||||
|
stats = gacha_db.get_inventory_stats(self.uid)
|
||||||
|
eligma = gacha_db.get_eligma(self.uid)
|
||||||
|
|
||||||
|
# Count by rarity
|
||||||
|
rows = gacha_db.get_inventory(self.uid)
|
||||||
|
by_rarity = {1: 0, 2: 0, 3: 0}
|
||||||
|
for sid, _ in rows:
|
||||||
|
s = db.get(sid)
|
||||||
|
if s:
|
||||||
|
by_rarity[s["StarGrade"]] = by_rarity.get(s["StarGrade"], 0) + 1
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="📋 Collection Dashboard",
|
||||||
|
description=f"**{stats['unique']}** unique students from **{stats['total_pulls']}** total pulls",
|
||||||
|
color=0x5BA0D0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Progress bars per rarity
|
||||||
|
for r in (3, 2, 1):
|
||||||
|
pct = _bar(by_rarity[r], POOL_TOTALS[r])
|
||||||
|
embed.add_field(
|
||||||
|
name=f"{RARITY_LABEL[r]} {by_rarity[r]}/{POOL_TOTALS[r]}",
|
||||||
|
value=f"`{pct}`",
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name="Eligma",
|
||||||
|
value=f"**{eligma}**",
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.set_footer(text="Use buttons below to browse")
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _build_collection_page(self) -> discord.Embed:
|
||||||
|
"""Paginated student list."""
|
||||||
|
all_rows = self._get_rows()
|
||||||
|
total_pages = max(1, (len(all_rows) + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||||
|
self.page = min(self.page, total_pages - 1)
|
||||||
|
|
||||||
|
start = self.page * PAGE_SIZE
|
||||||
|
chunk = all_rows[start:start + PAGE_SIZE]
|
||||||
|
|
||||||
|
rarity_label = RARITY_LABEL.get(self.filter_rarity, "All")
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"Collection — {rarity_label} ({len(all_rows)} students)",
|
||||||
|
color=0x5BA0D0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not chunk:
|
||||||
|
embed.description = "No students in this category yet. Use `!pull` to recruit!"
|
||||||
|
return embed
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for sid, name, count, rarity in chunk:
|
||||||
|
symbol = RARITY_SYMBOL[rarity]
|
||||||
|
dupes = f" ×{count}" if count > 1 else ""
|
||||||
|
lines.append(f"{symbol} {name}{dupes}")
|
||||||
|
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_footer(text=f"Page {self.page + 1}/{total_pages}")
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _build_eligma_page(self) -> discord.Embed:
|
||||||
|
amount = gacha_db.get_eligma(self.uid)
|
||||||
|
return discord.Embed(
|
||||||
|
title="Eligma",
|
||||||
|
description=(
|
||||||
|
f"**Balance: {amount}**\n\n"
|
||||||
|
"Earned from duplicates:\n"
|
||||||
|
f"{RARITY_SYMBOL[1]} 1★ dupe = {ELIGMA_YIELD[1]} Eligma\n"
|
||||||
|
f"{RARITY_SYMBOL[2]} 2★ dupe = {ELIGMA_YIELD[2]} Eligma\n"
|
||||||
|
f"{RARITY_SYMBOL[3]} 3★ dupe = {ELIGMA_YIELD[3]} Eligma"
|
||||||
|
),
|
||||||
|
color=0x5BA0D0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── buttons ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _update_buttons(self) -> None:
|
||||||
|
self.clear_items()
|
||||||
|
|
||||||
|
# Navigation row: dashboard, collection, eligma
|
||||||
|
dash = discord.ui.Button(label="Dashboard", style=discord.ButtonStyle.gray, row=0, custom_id="dash")
|
||||||
|
dash.callback = self._on_dash
|
||||||
|
self.add_item(dash)
|
||||||
|
|
||||||
|
coll = discord.ui.Button(label="Collection", style=discord.ButtonStyle.blurple, row=0, custom_id="coll")
|
||||||
|
coll.callback = self._on_coll
|
||||||
|
self.add_item(coll)
|
||||||
|
|
||||||
|
elig = discord.ui.Button(label="Eligma", style=discord.ButtonStyle.green, row=0, custom_id="elig")
|
||||||
|
elig.callback = self._on_elig
|
||||||
|
self.add_item(elig)
|
||||||
|
|
||||||
|
# Filter row: All, 3★, 2★, 1★
|
||||||
|
for label in (FILTER_ALL, "3★", "2★", "1★"):
|
||||||
|
style = discord.ButtonStyle.gray
|
||||||
|
if FILTER_LABELS[label] == self.filter_rarity:
|
||||||
|
style = discord.ButtonStyle.primary
|
||||||
|
elif label == FILTER_ALL and self.filter_rarity is None:
|
||||||
|
style = discord.ButtonStyle.primary
|
||||||
|
btn = discord.ui.Button(label=label, style=style, row=1, custom_id=f"f_{label}")
|
||||||
|
btn.callback = self._on_filter
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
# Page row
|
||||||
|
left = discord.ui.Button(label=PAGE_LEFT, style=discord.ButtonStyle.gray, row=2, custom_id="prev")
|
||||||
|
left.callback = self._on_prev
|
||||||
|
self.add_item(left)
|
||||||
|
|
||||||
|
right = discord.ui.Button(label=PAGE_RIGHT, style=discord.ButtonStyle.gray, row=2, custom_id="next")
|
||||||
|
right.callback = self._on_next
|
||||||
|
self.add_item(right)
|
||||||
|
|
||||||
|
async def _on_dash(self, interaction: discord.Interaction) -> None:
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._build_dashboard(), view=self)
|
||||||
|
|
||||||
|
async def _on_coll(self, interaction: discord.Interaction) -> None:
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._build_collection_page(), view=self)
|
||||||
|
|
||||||
|
async def _on_elig(self, interaction: discord.Interaction) -> None:
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._build_eligma_page(), view=self)
|
||||||
|
|
||||||
|
async def _on_filter(self, interaction: discord.Interaction) -> None:
|
||||||
|
label = interaction.data["custom_id"][2:] # strip "f_"
|
||||||
|
self.filter_rarity = FILTER_LABELS.get(label)
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._build_collection_page(), view=self)
|
||||||
|
|
||||||
|
async def _on_prev(self, interaction: discord.Interaction) -> None:
|
||||||
|
self.page = max(0, self.page - 1)
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._build_collection_page(), view=self)
|
||||||
|
|
||||||
|
async def _on_next(self, interaction: discord.Interaction) -> None:
|
||||||
|
all_rows = self._get_rows()
|
||||||
|
max_page = max(0, (len(all_rows) - 1) // PAGE_SIZE)
|
||||||
|
self.page = min(max_page, self.page + 1)
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._build_collection_page(), view=self)
|
||||||
|
|
||||||
|
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||||
|
if interaction.user.id != self.owner_id:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Only the person who ran this command can use the buttons. Run `!inv` yourself!",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Inventory(commands.Cog):
|
class Inventory(commands.Cog):
|
||||||
"""View your student collection and eligma from gacha pulls."""
|
"""Interactive collection browser for Blue Archive gacha."""
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot) -> None:
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@commands.group(name="inv", aliases=["collection", "i"], invoke_without_command=True)
|
@commands.group(name="inv", aliases=["collection", "i"], invoke_without_command=True)
|
||||||
async def inventory(self, ctx: commands.Context, *, target: str = "") -> None:
|
async def inventory(self, ctx: commands.Context, *, target: str = "") -> None:
|
||||||
"""View your student collection grouped by rarity.
|
"""Open your interactive collection dashboard.
|
||||||
|
|
||||||
Usage:
|
Usage: !inv — your collection with buttons
|
||||||
!inv — your collection
|
!inv @user — someone else's (read-only)
|
||||||
!inv @user — someone else's
|
|
||||||
!inv 3star — filter to 3★ only
|
|
||||||
!inv stats — summary stats
|
|
||||||
"""
|
"""
|
||||||
# Determine whose collection to show
|
|
||||||
if target and target.startswith("<@"):
|
if target and target.startswith("<@"):
|
||||||
try:
|
try:
|
||||||
uid = int(target.strip("<@!>"))
|
uid = int(target.strip("<@!>"))
|
||||||
@@ -39,14 +245,6 @@ class Inventory(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
uid = ctx.author.id
|
uid = ctx.author.id
|
||||||
|
|
||||||
filter_rarity = None
|
|
||||||
if target in ("3star", "3", "3*", "3star"):
|
|
||||||
filter_rarity = 3
|
|
||||||
elif target in ("2star", "2", "2*", "2star"):
|
|
||||||
filter_rarity = 2
|
|
||||||
elif target in ("1star", "1", "1*", "1star"):
|
|
||||||
filter_rarity = 1
|
|
||||||
|
|
||||||
rows = gacha_db.get_inventory(uid)
|
rows = gacha_db.get_inventory(uid)
|
||||||
if not rows:
|
if not rows:
|
||||||
if uid == ctx.author.id:
|
if uid == ctx.author.id:
|
||||||
@@ -55,101 +253,8 @@ class Inventory(commands.Cog):
|
|||||||
await ctx.reply("This user's collection is empty.")
|
await ctx.reply("This user's collection is empty.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Group by rarity
|
view = InventoryView(uid, ctx.author.id)
|
||||||
by_rarity: dict[int, list[tuple[int, str, int]]] = {3: [], 2: [], 1: []}
|
await ctx.reply(embed=view._build_dashboard(), view=view)
|
||||||
for sid, count in rows:
|
|
||||||
s = db.get(sid)
|
|
||||||
if s:
|
|
||||||
r = s["StarGrade"]
|
|
||||||
by_rarity.get(r, []).append((sid, s["Name"], count))
|
|
||||||
|
|
||||||
# Build embed
|
|
||||||
title = f"Collection of {self.bot.get_user(uid).display_name if self.bot.get_user(uid) else 'User'}"
|
|
||||||
embed = discord.Embed(title=title, color=0x5BA0D0)
|
|
||||||
|
|
||||||
for rarity, label, em_color in [
|
|
||||||
(3, "3-star", 0xF0C0FF),
|
|
||||||
(2, "2-star", 0xFFF588),
|
|
||||||
(1, "1-star", 0xAADCFF),
|
|
||||||
]:
|
|
||||||
if filter_rarity and rarity != filter_rarity:
|
|
||||||
continue
|
|
||||||
chars = sorted(by_rarity[rarity], key=lambda x: -x[2])
|
|
||||||
if not chars:
|
|
||||||
continue
|
|
||||||
total_unique = len(chars)
|
|
||||||
total_dupes = sum(c[2] - 1 for c in chars)
|
|
||||||
header = f"{label} — {total_unique} unique"
|
|
||||||
if total_dupes:
|
|
||||||
header += f" (+{total_dupes} dupes)"
|
|
||||||
|
|
||||||
# Format: "Aru x3, Hina x1, ..."
|
|
||||||
lines = []
|
|
||||||
for _, name, count in chars:
|
|
||||||
lines.append(f"{name}" + (f" x{count}" if count > 1 else ""))
|
|
||||||
|
|
||||||
# Split into chunks of ~10 per embed field line
|
|
||||||
text = ", ".join(lines)
|
|
||||||
if len(text) > 1024:
|
|
||||||
text = text[:1021] + "..."
|
|
||||||
embed.add_field(name=header, value=text, inline=False)
|
|
||||||
|
|
||||||
# Footer stats
|
|
||||||
stats = gacha_db.get_inventory_stats(uid)
|
|
||||||
eligma = gacha_db.get_eligma(uid)
|
|
||||||
embed.set_footer(
|
|
||||||
text=f"{stats['unique']} unique · {stats['total_pulls']} total pulls · {eligma} Eligma"
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
|
|
||||||
@inventory.command(name="stats")
|
|
||||||
async def inv_stats(self, ctx: commands.Context) -> None:
|
|
||||||
"""Show your collection summary stats."""
|
|
||||||
uid = ctx.author.id
|
|
||||||
stats = gacha_db.get_inventory_stats(uid)
|
|
||||||
eligma = gacha_db.get_eligma(uid)
|
|
||||||
|
|
||||||
# Count by rarity
|
|
||||||
rows = gacha_db.get_inventory(uid)
|
|
||||||
by_rarity = {1: 0, 2: 0, 3: 0}
|
|
||||||
for sid, _ in rows:
|
|
||||||
s = db.get(sid)
|
|
||||||
if s:
|
|
||||||
by_rarity[s["StarGrade"]] = by_rarity.get(s["StarGrade"], 0) + 1
|
|
||||||
|
|
||||||
embed = discord.Embed(title="Collection Stats", color=0x5BA0D0)
|
|
||||||
embed.add_field(name="Total Pulls", value=str(stats["total_pulls"]), inline=True)
|
|
||||||
embed.add_field(name="Unique Students", value=str(stats["unique"]), inline=True)
|
|
||||||
embed.add_field(name="Eligma", value=str(eligma), inline=True)
|
|
||||||
embed.add_field(
|
|
||||||
name="By Rarity",
|
|
||||||
value=f"3★: {by_rarity[3]} · 2★: {by_rarity[2]} · 1★: {by_rarity[1]}",
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
|
|
||||||
@commands.command(name="eligma", aliases=["elig"])
|
|
||||||
async def eligma(self, ctx: commands.Context) -> None:
|
|
||||||
"""Show your eligma balance and explain how it works."""
|
|
||||||
uid = ctx.author.id
|
|
||||||
amount = gacha_db.get_eligma(uid)
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="Eligma",
|
|
||||||
description=(
|
|
||||||
f"**Balance: {amount}**\n\n"
|
|
||||||
"Eligma is earned from duplicate pulls:\n"
|
|
||||||
f"1★ dupe = {ELIGMA_YIELD[1]} Eligma\n"
|
|
||||||
f"2★ dupe = {ELIGMA_YIELD[2]} Eligma\n"
|
|
||||||
f"3★ dupe = {ELIGMA_YIELD[3]} Eligma\n\n"
|
|
||||||
"In the real game, Eligma is used to buy student shards "
|
|
||||||
"in the shop. (Shop coming soon.)"
|
|
||||||
),
|
|
||||||
color=0x5BA0D0,
|
|
||||||
)
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
|||||||
Reference in new issue
Block a user