feat(crafting): add /craft command for duplicate card tier-up (#49) #171

Open
Claude wants to merge 1 commits from issue/49-feature-duplicate-card-crafting-tier-up-system into main
2 changed files with 232 additions and 0 deletions

View File

@ -0,0 +1,231 @@
# Card Crafting Module
# Combine 2 same-rarity cards into 1 random card of the next rarity tier up.
import logging
from typing import Optional
import discord
from discord import app_commands
from discord.ext import commands
from api_calls import db_get, team_hash
from helpers.constants import PD_PLAYERS
from helpers import get_team_by_owner, get_card_embeds, get_channel
from helpers.discord_utils import get_team_embed
from discord_ui import Confirm, ButtonOptions, SelectView
logger = logging.getLogger("discord_app")
# Crafting tier chain: source rarity name → result rarity name
CRAFT_TIER_UP = {
"Replacement": "Reserve",
"Reserve": "Starter",
"Starter": "All-Star",
"All-Star": "MVP",
"MVP": "HoF",
}
class SelectCraftCards(discord.ui.Select):
"""Select menu for picking exactly 2 cards to combine."""
def __init__(self, cards: list, team: dict, rarity_name: str):
self.owner_team = team
self.rarity_name = rarity_name
self.cards_by_id = {c["id"]: c for c in cards}
options = []
for card in cards[:25]:
player = card["player"]
label = f"{player['p_name']}{player['cardset']['name']}"[:100]
options.append(
discord.SelectOption(
label=label,
value=str(card["id"]),
description=f"Card ID: {card['id']}",
)
)
super().__init__(
placeholder=f"Select 2 {rarity_name} cards to combine",
min_values=2,
max_values=2,
options=options,
)
async def callback(self, interaction: discord.Interaction):
card1_id = int(self.values[0])
card2_id = int(self.values[1])
card1 = self.cards_by_id[card1_id]
card2 = self.cards_by_id[card2_id]
target_rarity = CRAFT_TIER_UP[self.rarity_name]
embed = get_team_embed(title="Confirm Craft", team=self.owner_team)
embed.description = (
f"**Combining:**\n"
f"{card1['player']['p_name']} ({card1['player']['cardset']['name']})\n"
f"{card2['player']['p_name']} ({card2['player']['cardset']['name']})\n\n"
f"**Result:** 1 random **{target_rarity}** card\n\n"
f"⚠️ These 2 cards will be permanently consumed."
)
view = Confirm(responders=[interaction.user], label_type="yes")
await interaction.response.edit_message(content=None, embed=embed, view=None)
question = await interaction.channel.send(
content="Proceed with crafting?", view=view
)
await view.wait()
if not view.value:
await question.edit(
content="Craft cancelled. Your cards are safe.", view=None
)
return
await question.edit(content="Crafting...", view=None)
result = await db_get(
f"teams/{self.owner_team['id']}/craft",
params=[
("ts", team_hash(self.owner_team)),
("ids", f"{card1_id},{card2_id}"),
],
)
if not result:
await question.edit(
content="That didn't go through. If this keeps happening, go ping Cal.",
view=None,
)
return
new_card = result.get("card", result)
await question.edit(
content=f"✨ Craft complete! You received a **{target_rarity}** card:",
view=None,
)
try:
await interaction.channel.send(embeds=await get_card_embeds(new_card))
except Exception:
player = new_card.get("player", {})
await interaction.channel.send(
content=f"**{player.get('p_name', 'Unknown')}** ({player.get('cardset', {}).get('name', '')})"
)
class Crafting(commands.Cog):
"""Card crafting: combine 2 same-rarity cards into 1 higher-rarity card."""
def __init__(self, bot):
self.bot = bot
@app_commands.command(
name="craft",
description="Combine 2 cards of the same rarity into 1 higher-rarity card",
)
@app_commands.checks.has_any_role(PD_PLAYERS)
@app_commands.describe(rarity="Rarity tier to craft from")
@app_commands.choices(
rarity=[
app_commands.Choice(
name="Replacement → Reserve (2× Rep = 1× Res)", value="Replacement"
),
app_commands.Choice(
name="Reserve → Starter (2× Res = 1× Sta)", value="Reserve"
),
app_commands.Choice(
name="Starter → All-Star (2× Sta = 1× All)", value="Starter"
),
app_commands.Choice(
name="All-Star → MVP (2× All = 1× MVP)", value="All-Star"
),
app_commands.Choice(
name="MVP → Hall of Fame (2× MVP = 1× HoF)", value="MVP"
),
]
)
async def craft_command(
self, interaction: discord.Interaction, rarity: Optional[str] = None
):
if interaction.channel.name in [
"paper-dynasty-chat",
"pd-news-ticker",
"pd-network-news",
]:
await interaction.response.send_message(
f"Please head to {get_channel(interaction, 'pd-bot-hole')} to run this command.",
ephemeral=True,
)
return
team = await get_team_by_owner(interaction.user.id)
if not team:
await interaction.response.send_message(
"I don't see a team for you, yet. You can sign up with the `/newteam` command!",
ephemeral=True,
)
return
if rarity is None:
embed = get_team_embed(title="Card Crafting", team=team)
embed.description = (
"Combine 2 cards of the same rarity to receive 1 random card of the next tier up.\n\n"
"**Craft Rates:**\n"
"2× Replacement → 1× Reserve\n"
"2× Reserve → 1× Starter\n"
"2× Starter → 1× All-Star\n"
"2× All-Star → 1× MVP\n"
"2× MVP → 1× Hall of Fame\n\n"
"Which rarity would you like to craft from?"
)
view = ButtonOptions(
responders=[interaction.user],
timeout=60,
labels=["Replacement", "Reserve", "Starter", "All-Star", "MVP"],
)
await interaction.response.send_message(embed=embed)
question = await interaction.channel.send(content=None, view=view)
await view.wait()
if not view.value:
await question.delete()
return
rarity = str(view.value)
await question.delete()
else:
await interaction.response.defer()
c_query = await db_get("cards", params=[("team_id", team["id"])], timeout=15)
if not c_query or c_query.get("count", 0) == 0:
await interaction.followup.send("You don't have any cards.", ephemeral=True)
return
rarity_cards = sorted(
[c for c in c_query["cards"] if c["player"]["rarity"]["name"] == rarity],
key=lambda c: c["player"]["p_name"],
)
if len(rarity_cards) < 2:
await interaction.followup.send(
f"You need at least 2 **{rarity}** cards to craft. "
f"You currently have {len(rarity_cards)}.",
ephemeral=True,
)
return
target = CRAFT_TIER_UP[rarity]
shown = min(len(rarity_cards), 25)
select = SelectCraftCards(rarity_cards, team, rarity)
view = SelectView([select], timeout=120)
await interaction.followup.send(
content=(
f"You have **{len(rarity_cards)}** {rarity} card{'s' if len(rarity_cards) != 1 else ''}. "
f"Select 2 to combine into a random **{target}** card"
f"{f' (showing first {shown})' if len(rarity_cards) > 25 else ''}:"
),
view=view,
)
async def setup(bot):
await bot.add_cog(Crafting(bot))

View File

@ -53,6 +53,7 @@ COGS = [
"cogs.players",
"cogs.gameplay",
"cogs.economy_new.scouting",
"cogs.economy_new.crafting",
"cogs.refractor",
]