All checks were successful
Build Docker Image / build (pull_request) Successful in 3m17s
Check-In Player packs (auto-opened by daily check-in) could end up orphaned
in inventory if roll_for_cards failed. The open-packs command crashed because:
1. The hyphenated pack type name bypassed the pretty_name logic, producing an
empty select menu that Discord rejected (400 Bad Request)
2. Even if displayed, selecting it would raise KeyError in the callback since
"Check-In Player".split("-") doesn't match any known pack type token
Fixes:
- Filter auto-open pack types out of the manual open-packs menu
- Add fallback for hyphenated pack type names in pretty_name logic
- Replace KeyError with graceful user-facing message for unknown pack types
- Change "contact an admin" to "contact Cal" in all user-facing messages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
429 lines
16 KiB
Python
429 lines
16 KiB
Python
# Economy Packs Module
|
|
# Contains pack opening, daily rewards, and donation commands from the original economy.py
|
|
|
|
import logging
|
|
from discord.ext import commands
|
|
from discord import app_commands, Member
|
|
import discord
|
|
import datetime
|
|
|
|
# Import specific utilities needed by this module
|
|
import random
|
|
from api_calls import db_get, db_post
|
|
from helpers.constants import PD_PLAYERS_ROLE_NAME, PD_PLAYERS, IMAGES
|
|
from helpers import (
|
|
get_team_by_owner,
|
|
display_cards,
|
|
give_packs,
|
|
legal_channel,
|
|
get_channel,
|
|
get_cal_user,
|
|
refresh_sheet,
|
|
roll_for_cards,
|
|
int_timestamp,
|
|
get_context_user,
|
|
)
|
|
from helpers.discord_utils import get_team_embed, get_emoji
|
|
from discord_ui import SelectView, SelectOpenPack
|
|
|
|
|
|
logger = logging.getLogger("discord_app")
|
|
|
|
|
|
class Packs(commands.Cog):
|
|
"""Pack management, daily rewards, and donation system for Paper Dynasty."""
|
|
|
|
def __init__(self, bot):
|
|
self.bot = bot
|
|
|
|
@commands.hybrid_group(name="donation", help="Mod: Give packs for PD donations")
|
|
@commands.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def donation(self, ctx: commands.Context):
|
|
if ctx.invoked_subcommand is None:
|
|
await ctx.send(
|
|
"To buy packs, visit https://ko-fi.com/manticorum/shop and include your discord username!"
|
|
)
|
|
|
|
@donation.command(
|
|
name="premium", help="Mod: Give premium packs", aliases=["p", "prem"]
|
|
)
|
|
async def donation_premium(self, ctx: commands.Context, num_packs: int, gm: Member):
|
|
if ctx.author.id != self.bot.owner_id:
|
|
await ctx.send("Wait a second. You're not in charge here!")
|
|
return
|
|
|
|
team = await get_team_by_owner(gm.id)
|
|
p_query = await db_get("packtypes", params=[("name", "Premium")])
|
|
if p_query["count"] == 0:
|
|
await ctx.send("Oof. I couldn't find a Premium Pack")
|
|
return
|
|
|
|
total_packs = await give_packs(
|
|
team, num_packs, pack_type=p_query["packtypes"][0]
|
|
)
|
|
await ctx.send(
|
|
f"The {team['lname']} now have {total_packs['count']} total packs!"
|
|
)
|
|
|
|
@donation.command(
|
|
name="standard", help="Mod: Give standard packs", aliases=["s", "sta"]
|
|
)
|
|
async def donation_standard(
|
|
self, ctx: commands.Context, num_packs: int, gm: Member
|
|
):
|
|
if ctx.author.id != self.bot.owner_id:
|
|
await ctx.send("Wait a second. You're not in charge here!")
|
|
return
|
|
|
|
team = await get_team_by_owner(gm.id)
|
|
p_query = await db_get("packtypes", params=[("name", "Standard")])
|
|
if p_query["count"] == 0:
|
|
await ctx.send("Oof. I couldn't find a Standard Pack")
|
|
return
|
|
|
|
total_packs = await give_packs(
|
|
team, num_packs, pack_type=p_query["packtypes"][0]
|
|
)
|
|
await ctx.send(
|
|
f"The {team['lname']} now have {total_packs['count']} total packs!"
|
|
)
|
|
|
|
@commands.hybrid_command(name="lastpack", help="Replay your last pack")
|
|
@commands.check(legal_channel)
|
|
@commands.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def last_pack_command(self, ctx: commands.Context):
|
|
team = await get_team_by_owner(get_context_user(ctx).id)
|
|
if not team:
|
|
await ctx.send(
|
|
"I don't see a team for you, yet. You can sign up with the `/newteam` command!"
|
|
)
|
|
return
|
|
|
|
p_query = await db_get(
|
|
"packs",
|
|
params=[
|
|
("opened", True),
|
|
("team_id", team["id"]),
|
|
("new_to_old", True),
|
|
("limit", 1),
|
|
],
|
|
)
|
|
if not p_query["count"]:
|
|
await ctx.send("I do not see any packs for you, bub.")
|
|
return
|
|
|
|
pack_name = p_query["packs"][0]["pack_type"]["name"]
|
|
if pack_name == "Standard":
|
|
pack_cover = IMAGES["pack-sta"]
|
|
elif pack_name == "Premium":
|
|
pack_cover = IMAGES["pack-pre"]
|
|
else:
|
|
pack_cover = None
|
|
|
|
c_query = await db_get("cards", params=[("pack_id", p_query["packs"][0]["id"])])
|
|
if not c_query["count"]:
|
|
await ctx.send("Hmm...I didn't see any cards in that pack.")
|
|
return
|
|
|
|
await display_cards(
|
|
c_query["cards"],
|
|
team,
|
|
ctx.channel,
|
|
ctx.author,
|
|
self.bot,
|
|
pack_cover=pack_cover,
|
|
)
|
|
|
|
@app_commands.command(
|
|
name="comeonmanineedthis",
|
|
description="Daily check-in for cards, currency, and packs",
|
|
)
|
|
@commands.has_any_role(PD_PLAYERS)
|
|
@commands.check(legal_channel)
|
|
async def daily_checkin(self, interaction: discord.Interaction):
|
|
await interaction.response.defer()
|
|
team = await get_team_by_owner(interaction.user.id)
|
|
if not team:
|
|
await interaction.edit_original_response(
|
|
content="I don't see a team for you, yet. You can sign up with the `/newteam` command!"
|
|
)
|
|
return
|
|
|
|
current = await db_get("current")
|
|
now = datetime.datetime.now()
|
|
midnight = int_timestamp(
|
|
datetime.datetime(now.year, now.month, now.day, 0, 0, 0)
|
|
)
|
|
daily = await db_get(
|
|
"rewards",
|
|
params=[
|
|
("name", "Daily Check-in"),
|
|
("team_id", team["id"]),
|
|
("created_after", midnight),
|
|
],
|
|
)
|
|
logger.debug(f"midnight: {midnight} / now: {int_timestamp(now)}")
|
|
logger.debug(f"daily_return: {daily}")
|
|
|
|
if daily:
|
|
await interaction.edit_original_response(
|
|
content="Looks like you already checked in today - come back at midnight Central!"
|
|
)
|
|
return
|
|
|
|
await db_post(
|
|
"rewards",
|
|
payload={
|
|
"name": "Daily Check-in",
|
|
"team_id": team["id"],
|
|
"season": current["season"],
|
|
"week": current["week"],
|
|
"created": int_timestamp(now),
|
|
},
|
|
)
|
|
current = await db_get("current")
|
|
check_ins = await db_get(
|
|
"rewards",
|
|
params=[
|
|
("name", "Daily Check-in"),
|
|
("team_id", team["id"]),
|
|
("season", current["season"]),
|
|
],
|
|
)
|
|
|
|
check_count = check_ins["count"] % 5
|
|
|
|
# 2nd, 4th, and 5th check-ins
|
|
if check_count == 0 or check_count % 2 == 0:
|
|
# Every fifth check-in
|
|
if check_count == 0:
|
|
greeting = await interaction.edit_original_response(
|
|
content="Hey, you just earned a Standard pack of cards!"
|
|
)
|
|
pack_channel = get_channel(interaction, "pack-openings")
|
|
|
|
p_query = await db_get("packtypes", params=[("name", "Standard")])
|
|
if not p_query:
|
|
await interaction.edit_original_response(
|
|
content=f"I was not able to pull this pack for you. "
|
|
f"Maybe ping {get_cal_user(interaction).mention}?"
|
|
)
|
|
return
|
|
|
|
# Every second and fourth check-in
|
|
else:
|
|
greeting = await interaction.edit_original_response(
|
|
content="Hey, you just earned a player card!"
|
|
)
|
|
pack_channel = interaction.channel
|
|
|
|
p_query = await db_get(
|
|
"packtypes", params=[("name", "Check-In Player")]
|
|
)
|
|
if not p_query:
|
|
await interaction.edit_original_response(
|
|
content=f"I was not able to pull this card for you. "
|
|
f"Maybe ping {get_cal_user(interaction).mention}?"
|
|
)
|
|
return
|
|
|
|
await give_packs(team, 1, p_query["packtypes"][0])
|
|
p_query = await db_get(
|
|
"packs",
|
|
params=[
|
|
("opened", False),
|
|
("team_id", team["id"]),
|
|
("new_to_old", True),
|
|
("limit", 1),
|
|
],
|
|
)
|
|
if not p_query["count"]:
|
|
await interaction.edit_original_response(
|
|
content=f"I do not see any packs in here. {await get_emoji(interaction, 'ConfusedPsyduck')}"
|
|
)
|
|
return
|
|
|
|
pack_ids = await roll_for_cards(
|
|
p_query["packs"], extra_val=check_ins["count"]
|
|
)
|
|
if not pack_ids:
|
|
await greeting.edit(
|
|
content=f"I was not able to create these cards {await get_emoji(interaction, 'slight_frown')}"
|
|
)
|
|
return
|
|
|
|
all_cards = []
|
|
for p_id in pack_ids:
|
|
new_cards = await db_get("cards", params=[("pack_id", p_id)])
|
|
all_cards.extend(new_cards["cards"])
|
|
|
|
if not all_cards:
|
|
await interaction.edit_original_response(
|
|
content=f"I was not able to pull these cards {await get_emoji(interaction, 'slight_frown')}"
|
|
)
|
|
return
|
|
|
|
await display_cards(
|
|
all_cards, team, pack_channel, interaction.user, self.bot
|
|
)
|
|
await refresh_sheet(team, self.bot)
|
|
return
|
|
|
|
# 1st, 3rd check-ins
|
|
else:
|
|
d_1000 = random.randint(1, 1000)
|
|
|
|
m_reward = 0
|
|
if d_1000 < 500:
|
|
m_reward = 10
|
|
elif d_1000 < 900:
|
|
m_reward = 15
|
|
elif d_1000 < 990:
|
|
m_reward = 20
|
|
else:
|
|
m_reward = 25
|
|
|
|
team = await db_post(f"teams/{team['id']}/money/{m_reward}")
|
|
await interaction.edit_original_response(
|
|
content=f"You just earned {m_reward}₼! That brings your wallet to {team['wallet']}₼!"
|
|
)
|
|
|
|
@app_commands.command(
|
|
name="open-packs", description="Open packs from your inventory"
|
|
)
|
|
@app_commands.checks.has_any_role(PD_PLAYERS)
|
|
async def open_packs_slash(self, interaction: discord.Interaction):
|
|
if interaction.channel.name in [
|
|
"paper-dynasty-chat",
|
|
"pd-news-ticker",
|
|
"pd-network-news",
|
|
]:
|
|
await interaction.response.send_message(
|
|
f"Please head to down to {get_channel(interaction, 'pd-bot-hole')} to run this command.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
owner_team = await get_team_by_owner(interaction.user.id)
|
|
if not owner_team:
|
|
await interaction.response.send_message(
|
|
"I don't see a team for you, yet. You can sign up with the `/newteam` command!"
|
|
)
|
|
return
|
|
|
|
p_query = await db_get(
|
|
"packs", params=[("team_id", owner_team["id"]), ("opened", False)]
|
|
)
|
|
if p_query["count"] == 0:
|
|
await interaction.response.send_message(
|
|
"Looks like you are clean out of packs, friendo. You can earn them by playing PD games or by "
|
|
"donating to the league."
|
|
)
|
|
return
|
|
|
|
# Pack types that are auto-opened and should not appear in the manual open menu
|
|
AUTO_OPEN_TYPES = {"Check-In Player"}
|
|
|
|
# Group packs by type and customization (e.g. Standard, Standard-Orioles, Standard-2012, Premium)
|
|
p_count = 0
|
|
p_data = {
|
|
"Standard": [],
|
|
"Premium": [],
|
|
"Daily": [],
|
|
"MVP": [],
|
|
"All Star": [],
|
|
"Mario": [],
|
|
"Team Choice": [],
|
|
}
|
|
logger.debug("Parsing packs...")
|
|
for pack in p_query["packs"]:
|
|
p_group = None
|
|
logger.debug(f"pack: {pack}")
|
|
logger.debug(f"pack cardset: {pack['pack_cardset']}")
|
|
if pack["pack_type"]["name"] in AUTO_OPEN_TYPES:
|
|
logger.debug(
|
|
f"Skipping auto-open pack type: {pack['pack_type']['name']}"
|
|
)
|
|
continue
|
|
if pack["pack_team"] is None and pack["pack_cardset"] is None:
|
|
p_group = pack["pack_type"]["name"]
|
|
# Add to p_data if this is a new pack type
|
|
if p_group not in p_data:
|
|
p_data[p_group] = []
|
|
|
|
elif pack["pack_team"] is not None:
|
|
if pack["pack_type"]["name"] == "Standard":
|
|
p_group = f"Standard-Team-{pack['pack_team']['id']}-{pack['pack_team']['sname']}"
|
|
elif pack["pack_type"]["name"] == "Premium":
|
|
p_group = f"Premium-Team-{pack['pack_team']['id']}-{pack['pack_team']['sname']}"
|
|
elif pack["pack_type"]["name"] == "Team Choice":
|
|
p_group = f"Team Choice-Team-{pack['pack_team']['id']}-{pack['pack_team']['sname']}"
|
|
elif pack["pack_type"]["name"] == "MVP":
|
|
p_group = f"MVP-Team-{pack['pack_team']['id']}-{pack['pack_team']['sname']}"
|
|
|
|
if pack["pack_cardset"] is not None:
|
|
p_group += f"-Cardset-{pack['pack_cardset']['id']}"
|
|
|
|
elif pack["pack_cardset"] is not None:
|
|
if pack["pack_type"]["name"] == "Standard":
|
|
p_group = f"Standard-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
|
elif pack["pack_type"]["name"] == "Premium":
|
|
p_group = f"Premium-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
|
elif pack["pack_type"]["name"] == "Team Choice":
|
|
p_group = f"Team Choice-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
|
elif pack["pack_type"]["name"] == "All Star":
|
|
p_group = f"All Star-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
|
elif pack["pack_type"]["name"] == "MVP":
|
|
p_group = f"MVP-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
|
elif pack["pack_type"]["name"] == "Promo Choice":
|
|
p_group = f"Promo Choice-Cardset-{pack['pack_cardset']['id']}-{pack['pack_cardset']['name']}"
|
|
|
|
logger.info(f"p_group: {p_group}")
|
|
if p_group is not None:
|
|
p_count += 1
|
|
if p_group not in p_data:
|
|
p_data[p_group] = [pack]
|
|
else:
|
|
p_data[p_group].append(pack)
|
|
|
|
if p_count == 0:
|
|
await interaction.response.send_message(
|
|
"Looks like you are clean out of packs, friendo. You can earn them by playing PD games or by "
|
|
"donating to the league."
|
|
)
|
|
return
|
|
|
|
# Display options and ask which group to open
|
|
embed = get_team_embed("Unopened Packs", team=owner_team)
|
|
embed.description = owner_team["lname"]
|
|
select_options = []
|
|
for key in p_data:
|
|
if len(p_data[key]) > 0:
|
|
pretty_name = None
|
|
# Not a specific pack
|
|
if "-" not in key:
|
|
pretty_name = key
|
|
elif "Team" in key:
|
|
pretty_name = f"{key.split('-')[0]} - {key.split('-')[3]}"
|
|
elif "Cardset" in key:
|
|
pretty_name = f"{key.split('-')[0]} - {key.split('-')[3]}"
|
|
else:
|
|
# Pack type name contains a hyphen (e.g. "Check-In Player")
|
|
pretty_name = key
|
|
|
|
if pretty_name is not None:
|
|
embed.add_field(name=pretty_name, value=f"Qty: {len(p_data[key])}")
|
|
select_options.append(
|
|
discord.SelectOption(label=pretty_name, value=key)
|
|
)
|
|
|
|
view = SelectView(
|
|
select_objects=[SelectOpenPack(select_options, owner_team)], timeout=15
|
|
)
|
|
await interaction.response.send_message(embed=embed, view=view)
|
|
|
|
|
|
async def setup(bot):
|
|
"""Setup function for the Packs cog."""
|
|
await bot.add_cog(Packs(bot))
|