When a player opens a pack, a scout opportunity is posted to #pack-openings with face-down card buttons. Other players can blind-pick one card using daily scout tokens (2/day), receiving a copy. The opener keeps all cards. New files: - discord_ui/scout_view.py: ScoutView with dynamic buttons and claim logic - helpers/scouting.py: create_scout_opportunity() and embed builder - cogs/economy_new/scouting.py: /scout-tokens command and cleanup task Modified: - helpers/main.py: Hook into open_st_pr_packs() after display_cards() - paperdynasty.py: Register scouting cog Requires new API endpoints in paper-dynasty-database (scout_opportunities). Tracks #44. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
174 lines
5.1 KiB
Python
174 lines
5.1 KiB
Python
"""
|
|
Scouting Helper Functions
|
|
|
|
Handles creation of scout opportunities after pack openings
|
|
and embed formatting for the scouting feature.
|
|
"""
|
|
|
|
import asyncio
|
|
import datetime
|
|
import logging
|
|
import random
|
|
|
|
import discord
|
|
|
|
from api_calls import db_post
|
|
from helpers.utils import int_timestamp
|
|
from helpers.discord_utils import get_team_embed
|
|
from helpers.constants import IMAGES, PD_SEASON
|
|
|
|
logger = logging.getLogger("discord_app")
|
|
|
|
SCOUT_WINDOW_SECONDS = 1800 # 30 minutes
|
|
|
|
# Rarity value → display symbol
|
|
RARITY_SYMBOLS = {
|
|
8: "\U0001f7e1", # HoF — yellow
|
|
5: "\U0001f7e3", # MVP — purple
|
|
3: "\U0001f535", # All-Star — blue
|
|
2: "\U0001f7e2", # Starter — green
|
|
1: "\u26aa", # Reserve — white
|
|
0: "\u26ab", # Replacement — black
|
|
}
|
|
|
|
|
|
def _build_card_lines(cards: list[dict]) -> list[tuple[int, str]]:
|
|
"""Build a shuffled list of (player_id, display_line) tuples."""
|
|
lines = []
|
|
for card in cards:
|
|
player = card["player"]
|
|
rarity_val = player["rarity"]["value"]
|
|
symbol = RARITY_SYMBOLS.get(rarity_val, "\u26ab")
|
|
lines.append(
|
|
(
|
|
player["player_id"],
|
|
f"{symbol} {player['rarity']['name']} — {player['p_name']}",
|
|
)
|
|
)
|
|
random.shuffle(lines)
|
|
return lines
|
|
|
|
|
|
def build_scout_embed(
|
|
opener_team: dict,
|
|
cards: list[dict],
|
|
card_lines: list[tuple[int, str]] = None,
|
|
) -> discord.Embed:
|
|
"""Build the embed shown above the scout buttons.
|
|
|
|
Shows a shuffled list of cards (rarity + player name) so scouters
|
|
know what's in the pack but not which button maps to which card.
|
|
Returns (embed, card_lines) so the view can store the shuffled order.
|
|
"""
|
|
embed = get_team_embed(title="Scout Opportunity!", team=opener_team)
|
|
|
|
if card_lines is None:
|
|
card_lines = _build_card_lines(cards)
|
|
|
|
card_list = "\n".join(line for _, line in card_lines)
|
|
|
|
embed.description = (
|
|
f"**{opener_team['lname']}** just opened a pack!\n\n"
|
|
f"**Cards in this pack:**\n{card_list}\n\n"
|
|
f"Pick a card — but which is which?\n"
|
|
f"Costs 1 Scout Token (2 per day, resets at midnight Central).\n"
|
|
f"This window closes in **30 minutes**."
|
|
)
|
|
embed.set_footer(
|
|
text=f"Paper Dynasty Season {PD_SEASON} \u2022 One player per pack",
|
|
icon_url=IMAGES["logo"],
|
|
)
|
|
return embed, card_lines
|
|
|
|
|
|
def build_scouted_card_list(
|
|
card_lines: list[tuple[int, str]],
|
|
scouted_cards: dict[int, str],
|
|
) -> str:
|
|
"""Rebuild the card list marking scouted cards with the scouter's team name.
|
|
|
|
Parameters
|
|
----------
|
|
card_lines : shuffled list of (player_id, display_line) tuples
|
|
scouted_cards : {player_id: scouter_team_name} for each claimed card
|
|
"""
|
|
result = []
|
|
for player_id, line in card_lines:
|
|
if player_id in scouted_cards:
|
|
team_name = scouted_cards[player_id]
|
|
result.append(f"{line} \u2014 \u2714\ufe0f *{team_name}*")
|
|
else:
|
|
result.append(line)
|
|
return "\n".join(result)
|
|
|
|
|
|
async def create_scout_opportunity(
|
|
pack_cards: list[dict],
|
|
opener_team: dict,
|
|
channel: discord.TextChannel,
|
|
opener_user,
|
|
context,
|
|
) -> None:
|
|
"""Create a scout opportunity and post the ScoutView to the channel.
|
|
|
|
Called after display_cards() completes in open_st_pr_packs().
|
|
Wrapped in try/except so scouting failures never crash pack opening.
|
|
|
|
Parameters
|
|
----------
|
|
pack_cards : list of card dicts from a single pack
|
|
opener_team : team dict for the pack opener
|
|
channel : the #pack-openings channel
|
|
opener_user : discord.Member or discord.User who opened the pack
|
|
context : the command context (Context or Interaction), used to get bot
|
|
"""
|
|
from discord_ui.scout_view import ScoutView
|
|
|
|
# Only create scout opportunities in the pack-openings channel
|
|
if not channel or channel.name != "pack-openings":
|
|
return
|
|
|
|
if not pack_cards:
|
|
return
|
|
|
|
now = datetime.datetime.now()
|
|
expires_at = int_timestamp(now + datetime.timedelta(seconds=SCOUT_WINDOW_SECONDS))
|
|
created = int_timestamp(now)
|
|
|
|
card_ids = [c["id"] for c in pack_cards]
|
|
|
|
try:
|
|
scout_opp = await db_post(
|
|
"scout_opportunities",
|
|
payload={
|
|
"pack_id": pack_cards[0].get("pack_id"),
|
|
"opener_team_id": opener_team["id"],
|
|
"card_ids": card_ids,
|
|
"expires_at": expires_at,
|
|
"created": created,
|
|
},
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to create scout opportunity: {e}")
|
|
return
|
|
|
|
embed, card_lines = build_scout_embed(opener_team, pack_cards)
|
|
|
|
# Get bot reference from context
|
|
bot = getattr(context, "bot", None) or getattr(context, "client", None)
|
|
|
|
view = ScoutView(
|
|
scout_opp_id=scout_opp["id"],
|
|
cards=pack_cards,
|
|
opener_team=opener_team,
|
|
opener_user_id=opener_user.id,
|
|
bot=bot,
|
|
)
|
|
view.card_lines = card_lines
|
|
|
|
try:
|
|
msg = await channel.send(embed=embed, view=view)
|
|
view.message = msg
|
|
except Exception as e:
|
|
logger.error(f"Failed to post scout opportunity message: {e}")
|