feat(crafting): add /craft command for duplicate card tier-up (#49) #171
231
cogs/economy_new/crafting.py
Normal file
231
cogs/economy_new/crafting.py
Normal 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))
|
||||
@ -53,6 +53,7 @@ COGS = [
|
||||
"cogs.players",
|
||||
"cogs.gameplay",
|
||||
"cogs.economy_new.scouting",
|
||||
"cogs.economy_new.crafting",
|
||||
"cogs.refractor",
|
||||
]
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user