feat: refractor card art pipeline — render trigger + /player view #139
333
cogs/players.py
333
cogs/players.py
@ -28,7 +28,7 @@ import helpers
|
||||
from in_game.gameplay_queries import get_team_or_none
|
||||
from in_game.simulations import get_pos_embeds, get_result
|
||||
from in_game.gameplay_models import Lineup, Play, Session, engine
|
||||
from api_calls import db_get, db_post, db_patch, get_team_by_abbrev
|
||||
from api_calls import db_get, db_post, db_patch, get_team_by_abbrev, DB_URL
|
||||
from helpers import (
|
||||
ACTIVE_EVENT_LITERAL,
|
||||
PD_PLAYERS_ROLE_NAME,
|
||||
@ -293,29 +293,29 @@ def get_ai_records(short_games, long_games):
|
||||
if line["away_team"]["is_ai"]:
|
||||
all_results[line["away_team"]["abbrev"]][league[line["game_type"]]][
|
||||
"w"
|
||||
] += (1 if home_win else 0)
|
||||
] += 1 if home_win else 0
|
||||
all_results[line["away_team"]["abbrev"]][league[line["game_type"]]][
|
||||
"l"
|
||||
] += (1 if not home_win else 0)
|
||||
] += 1 if not home_win else 0
|
||||
all_results[line["away_team"]["abbrev"]][league[line["game_type"]]][
|
||||
"points"
|
||||
] += (2 if home_win else 1)
|
||||
] += 2 if home_win else 1
|
||||
all_results[line["away_team"]["abbrev"]][league[line["game_type"]]][
|
||||
"rd"
|
||||
] += (line["home_score"] - line["away_score"])
|
||||
] += line["home_score"] - line["away_score"]
|
||||
elif line["home_team"]["is_ai"]:
|
||||
all_results[line["home_team"]["abbrev"]][league[line["game_type"]]][
|
||||
"w"
|
||||
] += (1 if not home_win else 0)
|
||||
] += 1 if not home_win else 0
|
||||
all_results[line["home_team"]["abbrev"]][league[line["game_type"]]][
|
||||
"l"
|
||||
] += (1 if home_win else 0)
|
||||
] += 1 if home_win else 0
|
||||
all_results[line["home_team"]["abbrev"]][league[line["game_type"]]][
|
||||
"points"
|
||||
] += (2 if not home_win else 1)
|
||||
] += 2 if not home_win else 1
|
||||
all_results[line["home_team"]["abbrev"]][league[line["game_type"]]][
|
||||
"rd"
|
||||
] += (line["away_score"] - line["home_score"])
|
||||
] += line["away_score"] - line["home_score"]
|
||||
logger.debug(f"done league games")
|
||||
|
||||
return all_results
|
||||
@ -367,51 +367,51 @@ def get_record_embed_legacy(embed: discord.Embed, results: dict, league: str):
|
||||
|
||||
embed.add_field(
|
||||
name=f"AL East ({ale_points} pts)",
|
||||
value=f'BAL: {results["BAL"][league]["w"]} - {results["BAL"][league]["l"]} ({results["BAL"][league]["rd"]} RD)\n'
|
||||
f'BOS: {results["BOS"][league]["w"]} - {results["BOS"][league]["l"]} ({results["BOS"][league]["rd"]} RD)\n'
|
||||
f'NYY: {results["NYY"][league]["w"]} - {results["NYY"][league]["l"]} ({results["NYY"][league]["rd"]} RD)\n'
|
||||
f'TBR: {results["TBR"][league]["w"]} - {results["TBR"][league]["l"]} ({results["TBR"][league]["rd"]} RD)\n'
|
||||
f'TOR: {results["TOR"][league]["w"]} - {results["TOR"][league]["l"]} ({results["TOR"][league]["rd"]} RD)\n',
|
||||
value=f"BAL: {results['BAL'][league]['w']} - {results['BAL'][league]['l']} ({results['BAL'][league]['rd']} RD)\n"
|
||||
f"BOS: {results['BOS'][league]['w']} - {results['BOS'][league]['l']} ({results['BOS'][league]['rd']} RD)\n"
|
||||
f"NYY: {results['NYY'][league]['w']} - {results['NYY'][league]['l']} ({results['NYY'][league]['rd']} RD)\n"
|
||||
f"TBR: {results['TBR'][league]['w']} - {results['TBR'][league]['l']} ({results['TBR'][league]['rd']} RD)\n"
|
||||
f"TOR: {results['TOR'][league]['w']} - {results['TOR'][league]['l']} ({results['TOR'][league]['rd']} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"AL Central ({alc_points} pts)",
|
||||
value=f'CLE: {results["CLE"][league]["w"]} - {results["CLE"][league]["l"]} ({results["CLE"][league]["rd"]} RD)\n'
|
||||
f'CHW: {results["CHW"][league]["w"]} - {results["CHW"][league]["l"]} ({results["CHW"][league]["rd"]} RD)\n'
|
||||
f'DET: {results["DET"][league]["w"]} - {results["DET"][league]["l"]} ({results["DET"][league]["rd"]} RD)\n'
|
||||
f'KCR: {results["KCR"][league]["w"]} - {results["KCR"][league]["l"]} ({results["KCR"][league]["rd"]} RD)\n'
|
||||
f'MIN: {results["MIN"][league]["w"]} - {results["MIN"][league]["l"]} ({results["MIN"][league]["rd"]} RD)\n',
|
||||
value=f"CLE: {results['CLE'][league]['w']} - {results['CLE'][league]['l']} ({results['CLE'][league]['rd']} RD)\n"
|
||||
f"CHW: {results['CHW'][league]['w']} - {results['CHW'][league]['l']} ({results['CHW'][league]['rd']} RD)\n"
|
||||
f"DET: {results['DET'][league]['w']} - {results['DET'][league]['l']} ({results['DET'][league]['rd']} RD)\n"
|
||||
f"KCR: {results['KCR'][league]['w']} - {results['KCR'][league]['l']} ({results['KCR'][league]['rd']} RD)\n"
|
||||
f"MIN: {results['MIN'][league]['w']} - {results['MIN'][league]['l']} ({results['MIN'][league]['rd']} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"AL West ({alw_points} pts)",
|
||||
value=f'HOU: {results["HOU"][league]["w"]} - {results["HOU"][league]["l"]} ({results["HOU"][league]["rd"]} RD)\n'
|
||||
f'LAA: {results["LAA"][league]["w"]} - {results["LAA"][league]["l"]} ({results["LAA"][league]["rd"]} RD)\n'
|
||||
f'OAK: {results["OAK"][league]["w"]} - {results["OAK"][league]["l"]} ({results["OAK"][league]["rd"]} RD)\n'
|
||||
f'SEA: {results["SEA"][league]["w"]} - {results["SEA"][league]["l"]} ({results["SEA"][league]["rd"]} RD)\n'
|
||||
f'TEX: {results["TEX"][league]["w"]} - {results["TEX"][league]["l"]} ({results["TEX"][league]["rd"]} RD)\n',
|
||||
value=f"HOU: {results['HOU'][league]['w']} - {results['HOU'][league]['l']} ({results['HOU'][league]['rd']} RD)\n"
|
||||
f"LAA: {results['LAA'][league]['w']} - {results['LAA'][league]['l']} ({results['LAA'][league]['rd']} RD)\n"
|
||||
f"OAK: {results['OAK'][league]['w']} - {results['OAK'][league]['l']} ({results['OAK'][league]['rd']} RD)\n"
|
||||
f"SEA: {results['SEA'][league]['w']} - {results['SEA'][league]['l']} ({results['SEA'][league]['rd']} RD)\n"
|
||||
f"TEX: {results['TEX'][league]['w']} - {results['TEX'][league]['l']} ({results['TEX'][league]['rd']} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"NL East ({nle_points} pts)",
|
||||
value=f'ATL: {results["ATL"][league]["w"]} - {results["ATL"][league]["l"]} ({results["ATL"][league]["rd"]} RD)\n'
|
||||
f'MIA: {results["MIA"][league]["w"]} - {results["MIA"][league]["l"]} ({results["MIA"][league]["rd"]} RD)\n'
|
||||
f'NYM: {results["NYM"][league]["w"]} - {results["NYM"][league]["l"]} ({results["NYM"][league]["rd"]} RD)\n'
|
||||
f'PHI: {results["PHI"][league]["w"]} - {results["PHI"][league]["l"]} ({results["PHI"][league]["rd"]} RD)\n'
|
||||
f'WSN: {results["WSN"][league]["w"]} - {results["WSN"][league]["l"]} ({results["WSN"][league]["rd"]} RD)\n',
|
||||
value=f"ATL: {results['ATL'][league]['w']} - {results['ATL'][league]['l']} ({results['ATL'][league]['rd']} RD)\n"
|
||||
f"MIA: {results['MIA'][league]['w']} - {results['MIA'][league]['l']} ({results['MIA'][league]['rd']} RD)\n"
|
||||
f"NYM: {results['NYM'][league]['w']} - {results['NYM'][league]['l']} ({results['NYM'][league]['rd']} RD)\n"
|
||||
f"PHI: {results['PHI'][league]['w']} - {results['PHI'][league]['l']} ({results['PHI'][league]['rd']} RD)\n"
|
||||
f"WSN: {results['WSN'][league]['w']} - {results['WSN'][league]['l']} ({results['WSN'][league]['rd']} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"NL Central ({nlc_points} pts)",
|
||||
value=f'CHC: {results["CHC"][league]["w"]} - {results["CHC"][league]["l"]} ({results["CHC"][league]["rd"]} RD)\n'
|
||||
f'CHW: {results["CIN"][league]["w"]} - {results["CIN"][league]["l"]} ({results["CIN"][league]["rd"]} RD)\n'
|
||||
f'MIL: {results["MIL"][league]["w"]} - {results["MIL"][league]["l"]} ({results["MIL"][league]["rd"]} RD)\n'
|
||||
f'PIT: {results["PIT"][league]["w"]} - {results["PIT"][league]["l"]} ({results["PIT"][league]["rd"]} RD)\n'
|
||||
f'STL: {results["STL"][league]["w"]} - {results["STL"][league]["l"]} ({results["STL"][league]["rd"]} RD)\n',
|
||||
value=f"CHC: {results['CHC'][league]['w']} - {results['CHC'][league]['l']} ({results['CHC'][league]['rd']} RD)\n"
|
||||
f"CHW: {results['CIN'][league]['w']} - {results['CIN'][league]['l']} ({results['CIN'][league]['rd']} RD)\n"
|
||||
f"MIL: {results['MIL'][league]['w']} - {results['MIL'][league]['l']} ({results['MIL'][league]['rd']} RD)\n"
|
||||
f"PIT: {results['PIT'][league]['w']} - {results['PIT'][league]['l']} ({results['PIT'][league]['rd']} RD)\n"
|
||||
f"STL: {results['STL'][league]['w']} - {results['STL'][league]['l']} ({results['STL'][league]['rd']} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"NL West ({nlw_points} pts)",
|
||||
value=f'ARI: {results["ARI"][league]["w"]} - {results["ARI"][league]["l"]} ({results["ARI"][league]["rd"]} RD)\n'
|
||||
f'COL: {results["COL"][league]["w"]} - {results["COL"][league]["l"]} ({results["COL"][league]["rd"]} RD)\n'
|
||||
f'LAD: {results["LAD"][league]["w"]} - {results["LAD"][league]["l"]} ({results["LAD"][league]["rd"]} RD)\n'
|
||||
f'SDP: {results["SDP"][league]["w"]} - {results["SDP"][league]["l"]} ({results["SDP"][league]["rd"]} RD)\n'
|
||||
f'SFG: {results["SFG"][league]["w"]} - {results["SFG"][league]["l"]} ({results["SFG"][league]["rd"]} RD)\n',
|
||||
value=f"ARI: {results['ARI'][league]['w']} - {results['ARI'][league]['l']} ({results['ARI'][league]['rd']} RD)\n"
|
||||
f"COL: {results['COL'][league]['w']} - {results['COL'][league]['l']} ({results['COL'][league]['rd']} RD)\n"
|
||||
f"LAD: {results['LAD'][league]['w']} - {results['LAD'][league]['l']} ({results['LAD'][league]['rd']} RD)\n"
|
||||
f"SDP: {results['SDP'][league]['w']} - {results['SDP'][league]['l']} ({results['SDP'][league]['rd']} RD)\n"
|
||||
f"SFG: {results['SFG'][league]['w']} - {results['SFG'][league]['l']} ({results['SFG'][league]['rd']} RD)\n",
|
||||
)
|
||||
|
||||
return embed
|
||||
@ -421,56 +421,134 @@ def get_record_embed(team: dict, results: dict, league: str):
|
||||
embed = get_team_embed(league, team)
|
||||
embed.add_field(
|
||||
name=f"AL East",
|
||||
value=f'BAL: {results["BAL"][0]} - {results["BAL"][1]} ({results["BAL"][2]} RD)\n'
|
||||
f'BOS: {results["BOS"][0]} - {results["BOS"][1]} ({results["BOS"][2]} RD)\n'
|
||||
f'NYY: {results["NYY"][0]} - {results["NYY"][1]} ({results["NYY"][2]} RD)\n'
|
||||
f'TBR: {results["TBR"][0]} - {results["TBR"][1]} ({results["TBR"][2]} RD)\n'
|
||||
f'TOR: {results["TOR"][0]} - {results["TOR"][1]} ({results["TOR"][2]} RD)\n',
|
||||
value=f"BAL: {results['BAL'][0]} - {results['BAL'][1]} ({results['BAL'][2]} RD)\n"
|
||||
f"BOS: {results['BOS'][0]} - {results['BOS'][1]} ({results['BOS'][2]} RD)\n"
|
||||
f"NYY: {results['NYY'][0]} - {results['NYY'][1]} ({results['NYY'][2]} RD)\n"
|
||||
f"TBR: {results['TBR'][0]} - {results['TBR'][1]} ({results['TBR'][2]} RD)\n"
|
||||
f"TOR: {results['TOR'][0]} - {results['TOR'][1]} ({results['TOR'][2]} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"AL Central",
|
||||
value=f'CLE: {results["CLE"][0]} - {results["CLE"][1]} ({results["CLE"][2]} RD)\n'
|
||||
f'CHW: {results["CHW"][0]} - {results["CHW"][1]} ({results["CHW"][2]} RD)\n'
|
||||
f'DET: {results["DET"][0]} - {results["DET"][1]} ({results["DET"][2]} RD)\n'
|
||||
f'KCR: {results["KCR"][0]} - {results["KCR"][1]} ({results["KCR"][2]} RD)\n'
|
||||
f'MIN: {results["MIN"][0]} - {results["MIN"][1]} ({results["MIN"][2]} RD)\n',
|
||||
value=f"CLE: {results['CLE'][0]} - {results['CLE'][1]} ({results['CLE'][2]} RD)\n"
|
||||
f"CHW: {results['CHW'][0]} - {results['CHW'][1]} ({results['CHW'][2]} RD)\n"
|
||||
f"DET: {results['DET'][0]} - {results['DET'][1]} ({results['DET'][2]} RD)\n"
|
||||
f"KCR: {results['KCR'][0]} - {results['KCR'][1]} ({results['KCR'][2]} RD)\n"
|
||||
f"MIN: {results['MIN'][0]} - {results['MIN'][1]} ({results['MIN'][2]} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"AL West",
|
||||
value=f'HOU: {results["HOU"][0]} - {results["HOU"][1]} ({results["HOU"][2]} RD)\n'
|
||||
f'LAA: {results["LAA"][0]} - {results["LAA"][1]} ({results["LAA"][2]} RD)\n'
|
||||
f'OAK: {results["OAK"][0]} - {results["OAK"][1]} ({results["OAK"][2]} RD)\n'
|
||||
f'SEA: {results["SEA"][0]} - {results["SEA"][1]} ({results["SEA"][2]} RD)\n'
|
||||
f'TEX: {results["TEX"][0]} - {results["TEX"][1]} ({results["TEX"][2]} RD)\n',
|
||||
value=f"HOU: {results['HOU'][0]} - {results['HOU'][1]} ({results['HOU'][2]} RD)\n"
|
||||
f"LAA: {results['LAA'][0]} - {results['LAA'][1]} ({results['LAA'][2]} RD)\n"
|
||||
f"OAK: {results['OAK'][0]} - {results['OAK'][1]} ({results['OAK'][2]} RD)\n"
|
||||
f"SEA: {results['SEA'][0]} - {results['SEA'][1]} ({results['SEA'][2]} RD)\n"
|
||||
f"TEX: {results['TEX'][0]} - {results['TEX'][1]} ({results['TEX'][2]} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"NL East",
|
||||
value=f'ATL: {results["ATL"][0]} - {results["ATL"][1]} ({results["ATL"][2]} RD)\n'
|
||||
f'MIA: {results["MIA"][0]} - {results["MIA"][1]} ({results["MIA"][2]} RD)\n'
|
||||
f'NYM: {results["NYM"][0]} - {results["NYM"][1]} ({results["NYM"][2]} RD)\n'
|
||||
f'PHI: {results["PHI"][0]} - {results["PHI"][1]} ({results["PHI"][2]} RD)\n'
|
||||
f'WSN: {results["WSN"][0]} - {results["WSN"][1]} ({results["WSN"][2]} RD)\n',
|
||||
value=f"ATL: {results['ATL'][0]} - {results['ATL'][1]} ({results['ATL'][2]} RD)\n"
|
||||
f"MIA: {results['MIA'][0]} - {results['MIA'][1]} ({results['MIA'][2]} RD)\n"
|
||||
f"NYM: {results['NYM'][0]} - {results['NYM'][1]} ({results['NYM'][2]} RD)\n"
|
||||
f"PHI: {results['PHI'][0]} - {results['PHI'][1]} ({results['PHI'][2]} RD)\n"
|
||||
f"WSN: {results['WSN'][0]} - {results['WSN'][1]} ({results['WSN'][2]} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"NL Central",
|
||||
value=f'CHC: {results["CHC"][0]} - {results["CHC"][1]} ({results["CHC"][2]} RD)\n'
|
||||
f'CIN: {results["CIN"][0]} - {results["CIN"][1]} ({results["CIN"][2]} RD)\n'
|
||||
f'MIL: {results["MIL"][0]} - {results["MIL"][1]} ({results["MIL"][2]} RD)\n'
|
||||
f'PIT: {results["PIT"][0]} - {results["PIT"][1]} ({results["PIT"][2]} RD)\n'
|
||||
f'STL: {results["STL"][0]} - {results["STL"][1]} ({results["STL"][2]} RD)\n',
|
||||
value=f"CHC: {results['CHC'][0]} - {results['CHC'][1]} ({results['CHC'][2]} RD)\n"
|
||||
f"CIN: {results['CIN'][0]} - {results['CIN'][1]} ({results['CIN'][2]} RD)\n"
|
||||
f"MIL: {results['MIL'][0]} - {results['MIL'][1]} ({results['MIL'][2]} RD)\n"
|
||||
f"PIT: {results['PIT'][0]} - {results['PIT'][1]} ({results['PIT'][2]} RD)\n"
|
||||
f"STL: {results['STL'][0]} - {results['STL'][1]} ({results['STL'][2]} RD)\n",
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"NL West",
|
||||
value=f'ARI: {results["ARI"][0]} - {results["ARI"][1]} ({results["ARI"][2]} RD)\n'
|
||||
f'COL: {results["COL"][0]} - {results["COL"][1]} ({results["COL"][2]} RD)\n'
|
||||
f'LAD: {results["LAD"][0]} - {results["LAD"][1]} ({results["LAD"][2]} RD)\n'
|
||||
f'SDP: {results["SDP"][0]} - {results["SDP"][1]} ({results["SDP"][2]} RD)\n'
|
||||
f'SFG: {results["SFG"][0]} - {results["SFG"][1]} ({results["SFG"][2]} RD)\n',
|
||||
value=f"ARI: {results['ARI'][0]} - {results['ARI'][1]} ({results['ARI'][2]} RD)\n"
|
||||
f"COL: {results['COL'][0]} - {results['COL'][1]} ({results['COL'][2]} RD)\n"
|
||||
f"LAD: {results['LAD'][0]} - {results['LAD'][1]} ({results['LAD'][2]} RD)\n"
|
||||
f"SDP: {results['SDP'][0]} - {results['SDP'][1]} ({results['SDP'][2]} RD)\n"
|
||||
f"SFG: {results['SFG'][0]} - {results['SFG'][1]} ({results['SFG'][2]} RD)\n",
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
REFRACTOR_TIER_NAMES = {
|
||||
0: "Base Card",
|
||||
1: "Base Chrome",
|
||||
2: "Refractor",
|
||||
3: "Gold Refractor",
|
||||
4: "Superfractor",
|
||||
}
|
||||
|
||||
|
||||
async def _build_refractor_response(
|
||||
player_name: str,
|
||||
player_id: int,
|
||||
refractor_tier: int,
|
||||
refractor_data: dict,
|
||||
) -> dict:
|
||||
"""Build response data for a /player refractor_tier request.
|
||||
|
||||
Returns a dict with:
|
||||
- found: bool
|
||||
- image_url: str or None
|
||||
- needs_render: bool
|
||||
- variant: int
|
||||
- card_type: str
|
||||
- player_name: str
|
||||
- tier_name: str
|
||||
- current_tier: int (when found)
|
||||
- top_cards: list (when not found)
|
||||
"""
|
||||
items = refractor_data.get("items", [])
|
||||
|
||||
match = None
|
||||
for item in items:
|
||||
if item["player_id"] == player_id and item["current_tier"] >= refractor_tier:
|
||||
match = item
|
||||
break
|
||||
|
||||
if match:
|
||||
# Map track card_type to the URL card_type format
|
||||
track_type = match.get("track", {}).get("card_type", "batter")
|
||||
card_type = "pitching" if track_type in ("sp", "rp") else "batting"
|
||||
return {
|
||||
"found": True,
|
||||
"image_url": match.get("image_url"),
|
||||
"needs_render": match.get("image_url") is None,
|
||||
"variant": match.get("variant", 0),
|
||||
"card_type": card_type,
|
||||
"player_name": player_name,
|
||||
"tier_name": REFRACTOR_TIER_NAMES.get(refractor_tier, f"T{refractor_tier}"),
|
||||
"current_tier": match["current_tier"],
|
||||
"top_cards": [],
|
||||
}
|
||||
|
||||
sorted_cards = sorted(
|
||||
items, key=lambda x: (-x["current_tier"], -x.get("current_value", 0))
|
||||
)
|
||||
top_cards = []
|
||||
for card in sorted_cards[:5]:
|
||||
tier = card["current_tier"]
|
||||
top_cards.append(
|
||||
{
|
||||
"player_name": card.get("player_name", "Unknown"),
|
||||
"tier": tier,
|
||||
"tier_name": REFRACTOR_TIER_NAMES.get(tier, f"T{tier}"),
|
||||
"image_url": card.get("image_url"),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"found": False,
|
||||
"image_url": None,
|
||||
"needs_render": False,
|
||||
"variant": 0,
|
||||
"player_name": player_name,
|
||||
"tier_name": REFRACTOR_TIER_NAMES.get(refractor_tier, f"T{refractor_tier}"),
|
||||
"top_cards": top_cards,
|
||||
}
|
||||
|
||||
|
||||
class Players(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
@ -650,12 +728,83 @@ class Players(commands.Cog):
|
||||
name="player", description="Display one or more of the player's cards"
|
||||
)
|
||||
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)
|
||||
@app_commands.describe(
|
||||
refractor_tier="View a refractor tier of this card (1-4)",
|
||||
)
|
||||
@app_commands.autocomplete(
|
||||
player_name=player_autocomplete, cardset=cardset_autocomplete
|
||||
)
|
||||
async def player_slash_command(
|
||||
self, interaction: discord.Interaction, player_name: str, cardset: str = "All"
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
player_name: str,
|
||||
cardset: str = "All",
|
||||
refractor_tier: Optional[int] = None,
|
||||
):
|
||||
if refractor_tier is not None:
|
||||
await interaction.response.defer()
|
||||
|
||||
team = await get_team_by_owner(interaction.user.id)
|
||||
if not team:
|
||||
await interaction.edit_original_response(
|
||||
content="You don't have a team yet. Join a team first!"
|
||||
)
|
||||
return
|
||||
|
||||
this_player = fuzzy_search(player_name, self.player_list)
|
||||
player_data = await db_get("players", params=[("name", this_player)])
|
||||
if not player_data or not player_data.get("players"):
|
||||
await interaction.edit_original_response(
|
||||
content=f"Player '{player_name}' not found."
|
||||
)
|
||||
return
|
||||
pid = player_data["players"][0]["id"]
|
||||
|
||||
refractor_data = await db_get(
|
||||
"refractor/cards", params=[("team_id", team["id"]), ("limit", 100)]
|
||||
)
|
||||
if not refractor_data:
|
||||
refractor_data = {"count": 0, "items": []}
|
||||
|
||||
result = await _build_refractor_response(
|
||||
player_name=this_player,
|
||||
player_id=pid,
|
||||
refractor_tier=refractor_tier,
|
||||
refractor_data=refractor_data,
|
||||
)
|
||||
|
||||
if result["found"]:
|
||||
embed = discord.Embed(
|
||||
title=f"{result['player_name']} — {result['tier_name']}",
|
||||
color=discord.Color.gold(),
|
||||
)
|
||||
|
||||
if result["needs_render"]:
|
||||
today = datetime.date.today().isoformat()
|
||||
card_type = result.get("card_type", "batting")
|
||||
render_url = f"{DB_URL}/v2/players/{pid}/{card_type}card/{today}/{result['variant']}"
|
||||
embed.set_image(url=render_url)
|
||||
embed.set_footer(text="First render — image generating...")
|
||||
else:
|
||||
embed.set_image(url=result["image_url"])
|
||||
|
||||
await interaction.edit_original_response(embed=embed)
|
||||
else:
|
||||
msg = f"You don't have a T{refractor_tier} refractor of **{this_player}**."
|
||||
if result["top_cards"]:
|
||||
msg += "\n\nYour top refractor cards:"
|
||||
for card in result["top_cards"]:
|
||||
tier_label = f"T{card['tier']} {card['tier_name']}"
|
||||
if card["image_url"]:
|
||||
msg += f"\n> [{card['player_name']} — {tier_label}]({card['image_url']})"
|
||||
else:
|
||||
msg += f"\n> {card['player_name']} — {tier_label}"
|
||||
else:
|
||||
msg += "\n\nYou don't have any refractor cards yet. Play games to earn them!"
|
||||
|
||||
await interaction.edit_original_response(content=msg)
|
||||
return
|
||||
|
||||
ephemeral = False
|
||||
if interaction.channel.name in ["paper-dynasty-chat", "pd-news-ticker"]:
|
||||
ephemeral = True
|
||||
@ -694,7 +843,7 @@ class Players(commands.Cog):
|
||||
|
||||
if len(all_embeds) > 1:
|
||||
await interaction.edit_original_response(
|
||||
content=f'# {all_players["players"][0]["p_name"]}'
|
||||
content=f"# {all_players['players'][0]['p_name']}"
|
||||
)
|
||||
await embed_pagination(
|
||||
all_embeds,
|
||||
@ -788,11 +937,11 @@ class Players(commands.Cog):
|
||||
current = await db_get("current")
|
||||
|
||||
await interaction.response.send_message(
|
||||
f'I\'m tallying the {team["lname"]} results now...', ephemeral=ephemeral
|
||||
f"I'm tallying the {team['lname']} results now...", ephemeral=ephemeral
|
||||
)
|
||||
|
||||
st_query = await db_get(
|
||||
f'teams/{team["id"]}/season-record', object_id=current["season"]
|
||||
f"teams/{team['id']}/season-record", object_id=current["season"]
|
||||
)
|
||||
|
||||
minor_embed = get_record_embed(team, st_query["minor-league"], "Minor League")
|
||||
@ -812,7 +961,7 @@ class Players(commands.Cog):
|
||||
start_page = 3
|
||||
|
||||
await interaction.edit_original_response(
|
||||
content=f'Here are the {team["lname"]} campaign records'
|
||||
content=f"Here are the {team['lname']} campaign records"
|
||||
)
|
||||
await embed_pagination(
|
||||
[minor_embed, major_embed, flashback_embed, hof_embed],
|
||||
@ -856,18 +1005,18 @@ class Players(commands.Cog):
|
||||
c_query = await db_get("cards", object_id=card_id)
|
||||
if c_query:
|
||||
c_string = (
|
||||
f'Card ID {card_id} is a {helpers.player_desc(c_query["player"])}'
|
||||
f"Card ID {card_id} is a {helpers.player_desc(c_query['player'])}"
|
||||
)
|
||||
if c_query["team"] is not None:
|
||||
c_string += f' owned by the {c_query["team"]["sname"]}'
|
||||
c_string += f" owned by the {c_query['team']['sname']}"
|
||||
if c_query["pack"] is not None:
|
||||
c_string += (
|
||||
f' pulled from a {c_query["pack"]["pack_type"]["name"]} pack.'
|
||||
f" pulled from a {c_query['pack']['pack_type']['name']} pack."
|
||||
)
|
||||
else:
|
||||
c_query["team"] = c_query["pack"]["team"]
|
||||
c_string += (
|
||||
f' used by the {c_query["pack"]["team"]["sname"]} in a gauntlet'
|
||||
f" used by the {c_query['pack']['team']['sname']} in a gauntlet"
|
||||
)
|
||||
|
||||
await interaction.edit_original_response(
|
||||
@ -947,7 +1096,7 @@ class Players(commands.Cog):
|
||||
await ctx.send(f"Who?")
|
||||
return
|
||||
|
||||
await ctx.send(f'{t_query["teams"][0]["sname"]} are a bunch of cuties!')
|
||||
await ctx.send(f"{t_query['teams'][0]['sname']} are a bunch of cuties!")
|
||||
|
||||
@commands.hybrid_command(name="random", help="Check out a random card")
|
||||
@commands.has_any_role(PD_PLAYERS_ROLE_NAME)
|
||||
@ -1074,7 +1223,7 @@ class Players(commands.Cog):
|
||||
r_query = await db_get("results", params=params)
|
||||
if not r_query["count"]:
|
||||
await ctx.send(
|
||||
f'There are no Ranked games on record this {"week" if which == "week" else "season"}.'
|
||||
f"There are no Ranked games on record this {'week' if which == 'week' else 'season'}."
|
||||
)
|
||||
return
|
||||
|
||||
@ -1116,8 +1265,8 @@ class Players(commands.Cog):
|
||||
|
||||
# await ctx.send(f'sorted: {sorted_records}')
|
||||
embed = get_team_embed(
|
||||
title=f'{"Season" if which == "season" else "Week"} '
|
||||
f'{current["season"] if which == "season" else current["week"]} Standings'
|
||||
title=f"{'Season' if which == 'season' else 'Week'} "
|
||||
f"{current['season'] if which == 'season' else current['week']} Standings"
|
||||
)
|
||||
|
||||
chunk_string = ""
|
||||
@ -1126,8 +1275,8 @@ class Players(commands.Cog):
|
||||
team = await db_get("teams", object_id=record[0])
|
||||
if team:
|
||||
chunk_string += (
|
||||
f'{record[1]["points"]} pt{"s" if record[1]["points"] != 1 else ""} '
|
||||
f'({record[1]["wins"]}-{record[1]["losses"]}) - {team["sname"]} [{team["ranking"]}]\n'
|
||||
f"{record[1]['points']} pt{'s' if record[1]['points'] != 1 else ''} "
|
||||
f"({record[1]['wins']}-{record[1]['losses']}) - {team['sname']} [{team['ranking']}]\n"
|
||||
)
|
||||
|
||||
else:
|
||||
@ -1237,7 +1386,7 @@ class Players(commands.Cog):
|
||||
this_run = r_query["runs"][0]
|
||||
else:
|
||||
await interaction.channel.send(
|
||||
content=f'I do not see an active run for the {this_team["lname"]}.'
|
||||
content=f"I do not see an active run for the {this_team['lname']}."
|
||||
)
|
||||
else:
|
||||
await interaction.channel.send(
|
||||
@ -1311,7 +1460,7 @@ class Players(commands.Cog):
|
||||
|
||||
if r_query["count"] != 0:
|
||||
await interaction.edit_original_response(
|
||||
content=f'Looks like you already have a {r_query["runs"][0]["gauntlet"]["name"]} run active! '
|
||||
content=f"Looks like you already have a {r_query['runs'][0]['gauntlet']['name']} run active! "
|
||||
f"You can check it out with the `/gauntlets status` command."
|
||||
)
|
||||
return
|
||||
@ -1322,7 +1471,7 @@ class Players(commands.Cog):
|
||||
)
|
||||
except ZeroDivisionError as e:
|
||||
logger.error(
|
||||
f'ZeroDivisionError in {this_event["name"]} draft for the {main_team.sname}: {e}'
|
||||
f"ZeroDivisionError in {this_event['name']} draft for the {main_team.sname}: {e}"
|
||||
)
|
||||
await gauntlets.wipe_team(draft_team, interaction)
|
||||
await interaction.channel.send(
|
||||
@ -1333,7 +1482,7 @@ class Players(commands.Cog):
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Failed to run {this_event["name"]} draft for the {main_team.sname}: {e}'
|
||||
f"Failed to run {this_event['name']} draft for the {main_team.sname}: {e}"
|
||||
)
|
||||
await gauntlets.wipe_team(draft_team, interaction)
|
||||
await interaction.channel.send(
|
||||
@ -1348,7 +1497,7 @@ class Players(commands.Cog):
|
||||
f"Good luck, champ in the making! To start playing, follow these steps:\n\n"
|
||||
f"1) Make a copy of the Team Sheet Template found in `/help-pd links`\n"
|
||||
f"2) Run `/newsheet` to link it to your Gauntlet team\n"
|
||||
f'3) Go play your first game with `/new-game gauntlet {this_event["name"]}`'
|
||||
f"3) Go play your first game with `/new-game gauntlet {this_event['name']}`"
|
||||
)
|
||||
else:
|
||||
await interaction.channel.send(
|
||||
@ -1360,7 +1509,7 @@ class Players(commands.Cog):
|
||||
await helpers.send_to_channel(
|
||||
bot=self.bot,
|
||||
channel_name="pd-news-ticker",
|
||||
content=f'The {main_team.lname} have entered the {this_event["name"]} Gauntlet!',
|
||||
content=f"The {main_team.lname} have entered the {this_event['name']} Gauntlet!",
|
||||
embed=draft_embed,
|
||||
)
|
||||
|
||||
@ -1373,7 +1522,7 @@ class Players(commands.Cog):
|
||||
): # type: ignore
|
||||
await interaction.response.defer()
|
||||
main_team = await get_team_by_owner(interaction.user.id)
|
||||
draft_team = await get_team_by_abbrev(f'Gauntlet-{main_team["abbrev"]}')
|
||||
draft_team = await get_team_by_abbrev(f"Gauntlet-{main_team['abbrev']}")
|
||||
if draft_team is None:
|
||||
await interaction.edit_original_response(
|
||||
content="Hmm, I can't find a gauntlet team for you. Have you signed up already?"
|
||||
@ -1404,7 +1553,7 @@ class Players(commands.Cog):
|
||||
this_run = r_query["runs"][0]
|
||||
else:
|
||||
await interaction.edit_original_response(
|
||||
content=f'I do not see an active run for the {draft_team["lname"]}.'
|
||||
content=f"I do not see an active run for the {draft_team['lname']}."
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
import discord
|
||||
from discord import SelectOption
|
||||
@ -4243,6 +4244,34 @@ async def get_game_summary_embed(
|
||||
return game_embed
|
||||
|
||||
|
||||
async def _trigger_variant_renders(tier_ups: list) -> None:
|
||||
"""Fire-and-forget: hit card render URLs to trigger S3 upload for new variants.
|
||||
|
||||
Each tier-up with a variant_created value gets a GET request to the card
|
||||
render endpoint, which triggers Playwright render + S3 upload as a side effect.
|
||||
Failures are logged but never raised.
|
||||
"""
|
||||
today = datetime.date.today().isoformat()
|
||||
for tier_up in tier_ups:
|
||||
variant = tier_up.get("variant_created")
|
||||
if variant is None:
|
||||
continue
|
||||
player_id = tier_up["player_id"]
|
||||
track = tier_up.get("track_name", "Batter")
|
||||
card_type = "pitching" if track.lower() == "pitcher" else "batting"
|
||||
try:
|
||||
await db_get(
|
||||
f"players/{player_id}/{card_type}card/{today}/{variant}",
|
||||
none_okay=True,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to trigger variant render for player %d variant %d (non-fatal)",
|
||||
player_id,
|
||||
variant,
|
||||
)
|
||||
|
||||
|
||||
async def complete_game(
|
||||
session: Session,
|
||||
interaction: discord.Interaction,
|
||||
@ -4353,6 +4382,7 @@ async def complete_game(
|
||||
if evo_result and evo_result.get("tier_ups"):
|
||||
for tier_up in evo_result["tier_ups"]:
|
||||
await notify_tier_completion(interaction.channel, tier_up)
|
||||
await _trigger_variant_renders(evo_result["tier_ups"])
|
||||
except Exception as e:
|
||||
logger.warning(f"Post-game refractor processing failed (non-fatal): {e}")
|
||||
|
||||
|
||||
141
tests/test_player_refractor_view.py
Normal file
141
tests/test_player_refractor_view.py
Normal file
@ -0,0 +1,141 @@
|
||||
"""Tests for /player refractor_tier view.
|
||||
|
||||
Tests cover _build_refractor_response, a module-level helper that processes
|
||||
raw API refractor data and returns structured response data for the slash command.
|
||||
The function is pure (no network calls) so tests run without mocks for the
|
||||
happy path cases, keeping tests readable and fast.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
# Make the repo root importable
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from cogs.players import _build_refractor_response
|
||||
|
||||
|
||||
REFRACTOR_CARDS_RESPONSE = {
|
||||
"count": 3,
|
||||
"items": [
|
||||
{
|
||||
"player_id": 100,
|
||||
"player_name": "Mike Trout",
|
||||
"current_tier": 3,
|
||||
"current_value": 160,
|
||||
"variant": 7,
|
||||
"track": {"card_type": "batter"},
|
||||
"image_url": "https://s3.example.com/cards/cardset-027/player-100/v7/battingcard.png",
|
||||
},
|
||||
{
|
||||
"player_id": 200,
|
||||
"player_name": "Barry Bonds",
|
||||
"current_tier": 2,
|
||||
"current_value": 110,
|
||||
"variant": 3,
|
||||
"track": {"card_type": "batter"},
|
||||
"image_url": "https://s3.example.com/cards/cardset-027/player-200/v3/battingcard.png",
|
||||
},
|
||||
{
|
||||
"player_id": 300,
|
||||
"player_name": "Ken Griffey Jr.",
|
||||
"current_tier": 1,
|
||||
"current_value": 55,
|
||||
"variant": 1,
|
||||
"track": {"card_type": "batter"},
|
||||
"image_url": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class TestBuildRefractorResponse:
|
||||
"""Build embed content for /player refractor_tier views."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_happy_path_returns_embed_with_image(self):
|
||||
"""When user has the refractor at requested tier, embed includes S3 image.
|
||||
|
||||
Verifies that when a player_id match is found at or above the requested
|
||||
tier, the result is marked as found and the image_url is passed through.
|
||||
"""
|
||||
result = await _build_refractor_response(
|
||||
player_name="Mike Trout",
|
||||
player_id=100,
|
||||
refractor_tier=3,
|
||||
refractor_data=REFRACTOR_CARDS_RESPONSE,
|
||||
)
|
||||
assert result["found"] is True
|
||||
assert "s3.example.com" in result["image_url"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_found_returns_top_5(self):
|
||||
"""When user doesn't have the refractor, show top 5 cards.
|
||||
|
||||
Verifies that when no match is found for the given player_id + tier,
|
||||
the response includes the top cards sorted by tier descending, and
|
||||
the highest-tier card appears first.
|
||||
"""
|
||||
result = await _build_refractor_response(
|
||||
player_name="Nobody",
|
||||
player_id=999,
|
||||
refractor_tier=2,
|
||||
refractor_data=REFRACTOR_CARDS_RESPONSE,
|
||||
)
|
||||
assert result["found"] is False
|
||||
assert len(result["top_cards"]) <= 5
|
||||
assert result["top_cards"][0]["player_name"] == "Mike Trout"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_image_url_none_triggers_render(self):
|
||||
"""When refractor exists but image_url is None, result signals render needed.
|
||||
|
||||
A card may exist at the requested tier without a cached S3 image URL
|
||||
if it has never been rendered. The response should set needs_render=True
|
||||
so the caller can construct a render endpoint URL and show a placeholder.
|
||||
"""
|
||||
result = await _build_refractor_response(
|
||||
player_name="Ken Griffey Jr.",
|
||||
player_id=300,
|
||||
refractor_tier=1,
|
||||
refractor_data=REFRACTOR_CARDS_RESPONSE,
|
||||
)
|
||||
assert result["found"] is True
|
||||
assert result["image_url"] is None
|
||||
assert result["needs_render"] is True
|
||||
assert result["variant"] == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_refractors_at_all(self):
|
||||
"""When user has zero refractor cards, clean message.
|
||||
|
||||
An empty items list should produce found=False with an empty top_cards
|
||||
list, allowing the caller to show a "no refractors yet" message.
|
||||
"""
|
||||
empty_data = {"count": 0, "items": []}
|
||||
result = await _build_refractor_response(
|
||||
player_name="Someone",
|
||||
player_id=500,
|
||||
refractor_tier=1,
|
||||
refractor_data=empty_data,
|
||||
)
|
||||
assert result["found"] is False
|
||||
assert result["top_cards"] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tier_higher_than_current_not_found(self):
|
||||
"""Requesting T4 when player is at T3 returns not found.
|
||||
|
||||
The match condition requires current_tier >= refractor_tier. Requesting
|
||||
a tier the player hasn't reached should return found=False so the
|
||||
caller can show what tier they do have.
|
||||
"""
|
||||
result = await _build_refractor_response(
|
||||
player_name="Mike Trout",
|
||||
player_id=100,
|
||||
refractor_tier=4,
|
||||
refractor_data=REFRACTOR_CARDS_RESPONSE,
|
||||
)
|
||||
assert result["found"] is False
|
||||
65
tests/test_post_game_render.py
Normal file
65
tests/test_post_game_render.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Tests for post-game refractor card render trigger."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from command_logic.logic_gameplay import _trigger_variant_renders
|
||||
|
||||
|
||||
class TestTriggerVariantRenders:
|
||||
"""Fire-and-forget card render calls after tier-ups."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calls_render_url_for_each_tier_up(self):
|
||||
"""Each tier-up with variant_created triggers a card render GET request."""
|
||||
tier_ups = [
|
||||
{"player_id": 100, "variant_created": 7, "track_name": "Batter"},
|
||||
{"player_id": 200, "variant_created": 3, "track_name": "Pitcher"},
|
||||
]
|
||||
|
||||
with patch(
|
||||
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
||||
) as mock_get:
|
||||
mock_get.return_value = None
|
||||
await _trigger_variant_renders(tier_ups)
|
||||
|
||||
assert mock_get.call_count == 2
|
||||
call_args_list = [call.args[0] for call in mock_get.call_args_list]
|
||||
assert any("100" in url and "7" in url for url in call_args_list)
|
||||
assert any("200" in url and "3" in url for url in call_args_list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_tier_ups_without_variant(self):
|
||||
"""Tier-ups without variant_created are skipped."""
|
||||
tier_ups = [
|
||||
{"player_id": 100, "track_name": "Batter"},
|
||||
]
|
||||
|
||||
with patch(
|
||||
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
||||
) as mock_get:
|
||||
await _trigger_variant_renders(tier_ups)
|
||||
mock_get.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_failure_does_not_raise(self):
|
||||
"""Render trigger failures are swallowed — fire-and-forget."""
|
||||
tier_ups = [
|
||||
{"player_id": 100, "variant_created": 7, "track_name": "Batter"},
|
||||
]
|
||||
|
||||
with patch(
|
||||
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
||||
) as mock_get:
|
||||
mock_get.side_effect = Exception("API down")
|
||||
await _trigger_variant_renders(tier_ups)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_tier_ups_is_noop(self):
|
||||
"""Empty tier_ups list does nothing."""
|
||||
with patch(
|
||||
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
||||
) as mock_get:
|
||||
await _trigger_variant_renders([])
|
||||
mock_get.assert_not_called()
|
||||
Loading…
Reference in New Issue
Block a user