add sqlite inventory, per-banner spark, eligma, collection commands
This commit is contained in:
1 parent
e61b0f1d88
commit
11d7c139e6
4 files changed
+386
-63
No files matched your search
@@ -0,0 +1,161 @@
|
|||||||
|
#this is the sqlite database layer for gacha inventory, spark, and eligma##
|
||||||
|
"""SQLite database for Blue Archive gacha persistence."""
|
||||||
|
######################################################################
|
||||||
|
import sqlite3
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DB_PATH = Path("data/ba_gacha.db")
|
||||||
|
|
||||||
|
# Eligma earned per duplicate by rarity
|
||||||
|
ELIGMA_YIELD = {1: 1, 2: 10, 3: 50}
|
||||||
|
|
||||||
|
|
||||||
|
def _connect() -> sqlite3.Connection:
|
||||||
|
"""Open a connection with WAL mode for concurrent reads."""
|
||||||
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(str(DB_PATH))
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def init_db() -> None:
|
||||||
|
"""Create tables if they don't exist."""
|
||||||
|
with _connect() as conn:
|
||||||
|
conn.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS inventory (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
student_id INTEGER NOT NULL,
|
||||||
|
count INTEGER NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (user_id, student_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS spark (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
banner_id TEXT NOT NULL,
|
||||||
|
points INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (user_id, banner_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS eligma (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
amount INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
log.info("Database initialised at %s", DB_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Inventory ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def add_pull(user_id: int, student_id: int, rarity: int) -> int:
|
||||||
|
"""Record a pull. If student already owned, increment count and return eligma earned."""
|
||||||
|
with _connect() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"SELECT count FROM inventory WHERE user_id = ? AND student_id = ?",
|
||||||
|
(user_id, student_id),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
# Duplicate — increment and earn eligma
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE inventory SET count = count + 1 WHERE user_id = ? AND student_id = ?",
|
||||||
|
(user_id, student_id),
|
||||||
|
)
|
||||||
|
eligma = ELIGMA_YIELD.get(rarity, 0)
|
||||||
|
_add_eligma(conn, user_id, eligma)
|
||||||
|
return eligma
|
||||||
|
else:
|
||||||
|
# New student
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO inventory (user_id, student_id, count) VALUES (?, ?, 1)",
|
||||||
|
(user_id, student_id),
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_inventory(user_id: int) -> list[tuple[int, int]]:
|
||||||
|
"""Return [(student_id, count), ...] for a user."""
|
||||||
|
with _connect() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT student_id, count FROM inventory WHERE user_id = ? ORDER BY student_id",
|
||||||
|
(user_id,),
|
||||||
|
).fetchall()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def get_inventory_stats(user_id: int) -> dict:
|
||||||
|
"""Return {unique_total, total_pulls, by_rarity: {1: n, 2: n, 3: n}}."""
|
||||||
|
with _connect() as conn:
|
||||||
|
total_pulls = conn.execute(
|
||||||
|
"SELECT COALESCE(SUM(count), 0) FROM inventory WHERE user_id = ?",
|
||||||
|
(user_id,),
|
||||||
|
).fetchone()[0]
|
||||||
|
unique = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM inventory WHERE user_id = ?",
|
||||||
|
(user_id,),
|
||||||
|
).fetchone()[0]
|
||||||
|
return {"total_pulls": total_pulls, "unique": unique}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Spark ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_spark(user_id: int, banner_id: str) -> int:
|
||||||
|
with _connect() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT points FROM spark WHERE user_id = ? AND banner_id = ?",
|
||||||
|
(user_id, banner_id),
|
||||||
|
).fetchone()
|
||||||
|
return row[0] if row else 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_spark(user_id: int, banner_id: str, points: int) -> int:
|
||||||
|
"""Add points to a banner's spark counter. Returns new total."""
|
||||||
|
with _connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO spark (user_id, banner_id, points) VALUES (?, ?, ?) "
|
||||||
|
"ON CONFLICT(user_id, banner_id) DO UPDATE SET points = points + ?",
|
||||||
|
(user_id, banner_id, points, points),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT points FROM spark WHERE user_id = ? AND banner_id = ?",
|
||||||
|
(user_id, banner_id),
|
||||||
|
).fetchone()
|
||||||
|
return row[0] if row else 0
|
||||||
|
|
||||||
|
|
||||||
|
def spend_spark(user_id: int, banner_id: str, cost: int = 200) -> bool:
|
||||||
|
"""Deduct spark points. Returns True if successful (had enough points)."""
|
||||||
|
with _connect() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT points FROM spark WHERE user_id = ? AND banner_id = ?",
|
||||||
|
(user_id, banner_id),
|
||||||
|
).fetchone()
|
||||||
|
if not row or row[0] < cost:
|
||||||
|
return False
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE spark SET points = points - ? WHERE user_id = ? AND banner_id = ?",
|
||||||
|
(cost, user_id, banner_id),
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ── Eligma ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _add_eligma(conn: sqlite3.Connection, user_id: int, amount: int) -> None:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO eligma (user_id, amount) VALUES (?, ?) "
|
||||||
|
"ON CONFLICT(user_id) DO UPDATE SET amount = amount + ?",
|
||||||
|
(user_id, amount, amount),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_eligma(user_id: int) -> int:
|
||||||
|
with _connect() as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT amount FROM eligma WHERE user_id = ?", (user_id,)
|
||||||
|
).fetchone()
|
||||||
|
return row[0] if row else 0
|
||||||
+68
-63
@@ -2,15 +2,14 @@
|
|||||||
"""Blue Archive Gacha Simulator — in-game-accurate recruitment with live banners."""
|
"""Blue Archive Gacha Simulator — in-game-accurate recruitment with live banners."""
|
||||||
######################################################################
|
######################################################################
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from . import db as gacha_db
|
||||||
from .data import (
|
from .data import (
|
||||||
db,
|
db,
|
||||||
fetch_banners,
|
fetch_banners,
|
||||||
@@ -24,40 +23,35 @@ from .gacha_renderer import render_pull
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
STATE_FILE = Path("data/ba_gacha_state.json")
|
|
||||||
SPARK_TARGET = 200
|
SPARK_TARGET = 200
|
||||||
_state_lock = asyncio.Lock()
|
BANNER_FILE = Path("data/ba_banner_state.json")
|
||||||
|
|
||||||
|
|
||||||
def _load_state() -> dict:
|
def _load_banner_state() -> dict:
|
||||||
"""Load per-user gacha state from disk."""
|
"""Which banner each user has picked. Lightweight JSON, separate from SQLite."""
|
||||||
if STATE_FILE.exists():
|
if BANNER_FILE.exists():
|
||||||
with open(STATE_FILE, "r") as f:
|
import json
|
||||||
|
with open(BANNER_FILE, "r") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _save_state(state: dict) -> None:
|
def _save_banner_state(state: dict) -> None:
|
||||||
"""Persist per-user gacha state to disk."""
|
import json
|
||||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
BANNER_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(STATE_FILE, "w") as f:
|
with open(BANNER_FILE, "w") as f:
|
||||||
json.dump(state, f, indent=2)
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
async def _get_user_state(user_id: int) -> dict:
|
def _get_banner_id(user_id: int) -> Optional[str]:
|
||||||
async with _state_lock:
|
state = _load_banner_state()
|
||||||
state = _load_state()
|
return state.get(str(user_id))
|
||||||
key = str(user_id)
|
|
||||||
if key not in state:
|
|
||||||
state[key] = {"banner_id": None, "spark": 0, "history": []}
|
|
||||||
return state[key]
|
|
||||||
|
|
||||||
|
|
||||||
async def _set_user_state(user_id: int, user_state: dict) -> None:
|
def _set_banner_id(user_id: int, banner_id: Optional[str]) -> None:
|
||||||
async with _state_lock:
|
state = _load_banner_state()
|
||||||
state = _load_state()
|
state[str(user_id)] = banner_id
|
||||||
state[str(user_id)] = user_state
|
_save_banner_state(state)
|
||||||
_save_state(state)
|
|
||||||
|
|
||||||
|
|
||||||
class BlueArchiveGacha(commands.Cog):
|
class BlueArchiveGacha(commands.Cog):
|
||||||
@@ -71,10 +65,10 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
# ── Lifecycle ───────────────────────────────────────────────────────
|
# ── Lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def cog_load(self) -> None:
|
async def cog_load(self) -> None:
|
||||||
"""Load student DB and fetch banners on startup."""
|
"""Load student DB, init SQLite, and fetch banners on startup."""
|
||||||
db.load()
|
db.load()
|
||||||
|
gacha_db.init_db()
|
||||||
await self._refresh_banners()
|
await self._refresh_banners()
|
||||||
# Background refresh every 6 hours
|
|
||||||
self._refresh_task = self.bot.loop.create_task(self._periodic_refresh())
|
self._refresh_task = self.bot.loop.create_task(self._periodic_refresh())
|
||||||
|
|
||||||
async def cog_unload(self) -> None:
|
async def cog_unload(self) -> None:
|
||||||
@@ -150,9 +144,7 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if choice == "regular":
|
if choice == "regular":
|
||||||
user_state = await _get_user_state(ctx.author.id)
|
_set_banner_id(ctx.author.id, "regular")
|
||||||
user_state["banner_id"] = "regular"
|
|
||||||
await _set_user_state(ctx.author.id, user_state)
|
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
"Active banner set to **Regular Recruitment** (permanent pool, no rate-up).\n"
|
"Active banner set to **Regular Recruitment** (permanent pool, no rate-up).\n"
|
||||||
"Use `!pull` to recruit!"
|
"Use `!pull` to recruit!"
|
||||||
@@ -180,15 +172,15 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
rateups = ", ".join(banner.get("rateups", [])) or "Standard Pool"
|
rateups = ", ".join(banner.get("rateups", [])) or "Standard Pool"
|
||||||
gtype = banner.get("gachaType", "PickupGacha")
|
gtype = banner.get("gachaType", "PickupGacha")
|
||||||
|
|
||||||
user_state = await _get_user_state(ctx.author.id)
|
_set_banner_id(ctx.author.id, str(banner["id"]))
|
||||||
user_state["banner_id"] = banner["id"]
|
|
||||||
user_state["spark"] = 0 # reset spark on banner switch
|
# Show current spark for this banner
|
||||||
await _set_user_state(ctx.author.id, user_state)
|
spark = gacha_db.get_spark(ctx.author.id, str(banner["id"]))
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
f"Active banner: **{gtype}**\n"
|
f"Active banner: **{gtype}**\n"
|
||||||
f"Rate-up: {rateups}\n"
|
f"Rate-up: {rateups}\n"
|
||||||
f"Spark count reset to 0. Use `!pull` to recruit!"
|
f"Recruitment Points: {spark}/{SPARK_TARGET}. Use `!pull` to recruit!"
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command(name="pull")
|
@commands.command(name="pull")
|
||||||
@@ -204,8 +196,8 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
count = 10
|
count = 10
|
||||||
|
|
||||||
user_state = await _get_user_state(ctx.author.id)
|
user_id = ctx.author.id
|
||||||
banner_id = user_state.get("banner_id")
|
banner_id = _get_banner_id(user_id)
|
||||||
|
|
||||||
# Determine banner info
|
# Determine banner info
|
||||||
try:
|
try:
|
||||||
@@ -255,16 +247,19 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
|
|
||||||
pulls.append(student)
|
pulls.append(student)
|
||||||
|
|
||||||
# Update spark
|
# Save pulls to inventory (eligma for dupes) and update spark
|
||||||
user_state["spark"] = user_state.get("spark", 0) + count
|
user_id = ctx.author.id
|
||||||
# Trim history to last 50
|
spark_banner = str(banner_id) if banner_id else "regular"
|
||||||
history = user_state.get("history", [])
|
spark = gacha_db.add_spark(user_id, spark_banner, count)
|
||||||
history.extend(p["Id"] for p in pulls)
|
|
||||||
user_state["history"] = history[-50:]
|
eligma_earned = 0
|
||||||
await _set_user_state(ctx.author.id, user_state)
|
for p in pulls:
|
||||||
|
e = gacha_db.add_pull(user_id, p["Id"], p["StarGrade"])
|
||||||
|
eligma_earned += e
|
||||||
|
|
||||||
|
dupe_msg = f" | +{eligma_earned} Eligma from dupes" if eligma_earned else ""
|
||||||
|
|
||||||
# Render result image
|
# Render result image
|
||||||
spark = user_state["spark"]
|
|
||||||
try:
|
try:
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
img_bytes = await render_pull(pulls, banner_name, spark)
|
img_bytes = await render_pull(pulls, banner_name, spark)
|
||||||
@@ -277,21 +272,24 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
await ctx.reply(f"Render failed: {e}")
|
await ctx.reply(f"Render failed: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Send image only
|
# Send image with spark+eligma caption
|
||||||
try:
|
try:
|
||||||
file = discord.File(img_bytes, filename="gacha_result.png")
|
file = discord.File(img_bytes, filename="gacha_result.png")
|
||||||
try:
|
try:
|
||||||
await teaser.delete()
|
await teaser.delete()
|
||||||
except (discord.NotFound, discord.Forbidden):
|
except (discord.NotFound, discord.Forbidden):
|
||||||
pass
|
pass
|
||||||
await ctx.reply(file=file)
|
await ctx.reply(
|
||||||
|
f"Recruitment Points: {spark}/{SPARK_TARGET}{dupe_msg}",
|
||||||
|
file=file,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("Failed to send result image")
|
log.exception("Failed to send result image")
|
||||||
await ctx.reply(f"Failed to send result: {e}")
|
await ctx.reply(f"Failed to send result: {e}")
|
||||||
|
|
||||||
@commands.command(name="spark")
|
@commands.command(name="spark")
|
||||||
async def spark(self, ctx: commands.Context, *, character_name: str = "") -> None:
|
async def spark(self, ctx: commands.Context, *, character_name: str = "") -> None:
|
||||||
"""Claim a rate-up student from your active banner (costs 200 recruitment points).
|
"""Claim a rate-up student using 200 Recruitment Points from your active banner.
|
||||||
|
|
||||||
Usage: !spark <character name>
|
Usage: !spark <character name>
|
||||||
"""
|
"""
|
||||||
@@ -299,12 +297,14 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
await ctx.reply("Usage: `!spark <character name>` — e.g., `!spark Aru`")
|
await ctx.reply("Usage: `!spark <character name>` — e.g., `!spark Aru`")
|
||||||
return
|
return
|
||||||
|
|
||||||
user_state = await _get_user_state(ctx.author.id)
|
user_id = ctx.author.id
|
||||||
spark = user_state.get("spark", 0)
|
banner_id = str(_get_banner_id(user_id) or "regular")
|
||||||
|
spark = gacha_db.get_spark(user_id, banner_id)
|
||||||
|
|
||||||
if spark < SPARK_TARGET:
|
if spark < SPARK_TARGET:
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
f"You need {SPARK_TARGET} recruitment points to spark. You have {spark}."
|
f"You need {SPARK_TARGET} Recruitment Points to spark. "
|
||||||
|
f"You have {spark} on this banner."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -314,22 +314,25 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
await ctx.reply(f"Character **{character_name}** not found in the database.")
|
await ctx.reply(f"Character **{character_name}** not found in the database.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Deduct spark
|
# Deduct spark and add student to inventory
|
||||||
user_state["spark"] = spark - SPARK_TARGET
|
if not gacha_db.spend_spark(user_id, banner_id):
|
||||||
await _set_user_state(ctx.author.id, user_state)
|
await ctx.reply("Not enough Recruitment Points.")
|
||||||
|
return
|
||||||
|
gacha_db.add_pull(user_id, student["Id"], student["StarGrade"])
|
||||||
|
remaining = gacha_db.get_spark(user_id, banner_id)
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
f"**Spark!** You claimed **{student['Name']}** ({student['StarGrade']}*, {student.get('School', 'Unknown')})!\n"
|
f"**Spark!** You claimed **{student['Name']}** "
|
||||||
f"Remaining recruitment points: {user_state['spark']}"
|
f"({student['StarGrade']}*, {student.get('School', 'Unknown')})!\n"
|
||||||
|
f"Remaining Recruitment Points: {remaining}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@gacha.command(name="info")
|
@gacha.command(name="info")
|
||||||
async def gacha_info(self, ctx: commands.Context) -> None:
|
async def gacha_info(self, ctx: commands.Context) -> None:
|
||||||
"""Show your current gacha state — active banner, spark count, and recent pulls."""
|
"""Show your active banner, spark count, and collection summary."""
|
||||||
user_state = await _get_user_state(ctx.author.id)
|
user_id = ctx.author.id
|
||||||
banner_id = user_state.get("banner_id", "regular")
|
banner_id = str(_get_banner_id(user_id) or "regular")
|
||||||
spark = user_state.get("spark", 0)
|
spark = gacha_db.get_spark(user_id, banner_id)
|
||||||
history = user_state.get("history", [])
|
|
||||||
|
|
||||||
# Banner name
|
# Banner name
|
||||||
banner_name = "Regular Recruitment"
|
banner_name = "Regular Recruitment"
|
||||||
@@ -356,11 +359,13 @@ class BlueArchiveGacha(commands.Cog):
|
|||||||
name="Recruitment Points", value=f"{spark}/{SPARK_TARGET}", inline=True
|
name="Recruitment Points", value=f"{spark}/{SPARK_TARGET}", inline=True
|
||||||
)
|
)
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Total Pulls", value=str(len(history)), inline=True
|
name="Eligma", value=str(gacha_db.get_eligma(user_id)), inline=True
|
||||||
)
|
)
|
||||||
if recent:
|
stats = gacha_db.get_inventory_stats(user_id)
|
||||||
embed.add_field(
|
embed.add_field(
|
||||||
name="Recent Pulls", value="\n".join(recent) or "None", inline=False
|
name="Collection",
|
||||||
|
value=f"{stats['unique']} unique students from {stats['total_pulls']} total pulls",
|
||||||
|
inline=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
await ctx.reply(embed=embed)
|
await ctx.reply(embed=embed)
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#this is the inventory cog for viewing collections and eligma balance##
|
||||||
|
"""Collection and eligma commands for the Blue Archive gacha system."""
|
||||||
|
######################################################################
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from . import db as gacha_db
|
||||||
|
from .data import db
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ELIGMA_YIELD = {1: 1, 2: 10, 3: 50}
|
||||||
|
|
||||||
|
|
||||||
|
class Inventory(commands.Cog):
|
||||||
|
"""View your student collection and eligma from gacha pulls."""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.group(name="inv", aliases=["collection", "i"], invoke_without_command=True)
|
||||||
|
async def inventory(self, ctx: commands.Context, *, target: str = "") -> None:
|
||||||
|
"""View your student collection grouped by rarity.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
!inv — your collection
|
||||||
|
!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("<@"):
|
||||||
|
try:
|
||||||
|
uid = int(target.strip("<@!>"))
|
||||||
|
except ValueError:
|
||||||
|
uid = ctx.author.id
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
if not rows:
|
||||||
|
if uid == ctx.author.id:
|
||||||
|
await ctx.reply("Your collection is empty. Use `!pull` to recruit students!")
|
||||||
|
else:
|
||||||
|
await ctx.reply("This user's collection is empty.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Group by rarity
|
||||||
|
by_rarity: dict[int, list[tuple[int, str, int]]] = {3: [], 2: [], 1: []}
|
||||||
|
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:
|
||||||
|
await bot.add_cog(Inventory(bot))
|
||||||
@@ -69,6 +69,7 @@ async def main():
|
|||||||
await bot.load_extension('twitterbridge.leaks_cmd')
|
await bot.load_extension('twitterbridge.leaks_cmd')
|
||||||
await bot.load_extension("rio.ssh_connection_rio")
|
await bot.load_extension("rio.ssh_connection_rio")
|
||||||
await bot.load_extension('blue_archive.gacha')
|
await bot.load_extension('blue_archive.gacha')
|
||||||
|
await bot.load_extension('blue_archive.inventory')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await bot.start(os.getenv('DISCORD_TOKEN'))
|
await bot.start(os.getenv('DISCORD_TOKEN'))
|
||||||
|
|||||||
Reference in new issue
Block a user