# Evolution Status Module # Displays evolution tier progress for a team's cards from discord.ext import commands from discord import app_commands import discord from typing import Optional import logging from api_calls import db_get from helpers import get_team_by_owner, is_ephemeral_channel logger = logging.getLogger("discord_app") # Tier display names TIER_NAMES = { 0: "Unranked", 1: "Initiate", 2: "Rising", 3: "Ascendant", 4: "Evolved", } # Formula shorthands by card_type FORMULA_SHORTHANDS = { "batter": "PA+TB×2", "sp": "IP+K", "rp": "IP+K", } def render_progress_bar( current_value: float, next_threshold: float | None, width: int = 10 ) -> str: """Render a text progress bar. Args: current_value: Current formula value. next_threshold: Threshold for the next tier. None if fully evolved. width: Number of characters in the bar. Returns: A string like '[========--] 120/149' or '[==========] FULLY EVOLVED'. """ if next_threshold is None or next_threshold <= 0: return f"[{'=' * width}] FULLY EVOLVED" ratio = min(current_value / next_threshold, 1.0) filled = round(ratio * width) empty = width - filled bar = f"[{'=' * filled}{'-' * empty}]" return f"{bar} {int(current_value)}/{int(next_threshold)}" def format_evo_entry(state: dict) -> str: """Format a single evolution card state into a display line. Args: state: Card state dict from the API with nested track info. Returns: Formatted string like 'Mike Trout [========--] 120/149 (PA+TB×2) T1 → T2' """ track = state.get("track", {}) card_type = track.get("card_type", "batter") formula = FORMULA_SHORTHANDS.get(card_type, "???") current_tier = state.get("current_tier", 0) current_value = state.get("current_value", 0.0) next_threshold = state.get("next_threshold") fully_evolved = state.get("fully_evolved", False) bar = render_progress_bar(current_value, next_threshold) if fully_evolved: tier_label = f"T4 — {TIER_NAMES[4]}" else: next_tier = current_tier + 1 tier_label = ( f"{TIER_NAMES.get(current_tier, '?')} → {TIER_NAMES.get(next_tier, '?')}" ) return f"{bar} ({formula}) {tier_label}" def is_close_to_tierup(state: dict, threshold_pct: float = 0.80) -> bool: """Check if a card is close to its next tier-up. Args: state: Card state dict from the API. threshold_pct: Fraction of next_threshold that counts as "close". Returns: True if current_value >= threshold_pct * next_threshold. """ next_threshold = state.get("next_threshold") if next_threshold is None or next_threshold <= 0: return False current_value = state.get("current_value", 0.0) return current_value >= threshold_pct * next_threshold class Evolution(commands.Cog): """Evolution tier progress for Paper Dynasty cards.""" def __init__(self, bot): self.bot = bot evo_group = app_commands.Group(name="evo", description="Evolution commands") @evo_group.command(name="status", description="View your team's evolution progress") @app_commands.describe( type="Filter by card type (batter, sp, rp)", tier="Filter by minimum tier (0-4)", progress="Show only cards close to tier-up (type 'close')", page="Page number (default: 1)", ) async def evo_status( self, interaction: discord.Interaction, type: Optional[str] = None, tier: Optional[int] = None, progress: Optional[str] = None, page: int = 1, ): await interaction.response.defer( ephemeral=is_ephemeral_channel(interaction.channel) ) # Look up the user's team team = await get_team_by_owner(interaction.user.id) if not team: await interaction.followup.send( "You don't have a team registered. Use `/register` first.", ephemeral=True, ) return team_id = team.get("team_id") or team.get("id") # Build query params params = [("page", page), ("per_page", 10)] if type: params.append(("card_type", type)) if tier is not None: params.append(("tier", tier)) try: result = await db_get( f"teams/{team_id}/evolutions", params=params, none_okay=True, ) except Exception: logger.warning( f"Failed to fetch evolution data for team {team_id}", exc_info=True, ) await interaction.followup.send( "Could not fetch evolution data. Please try again later.", ephemeral=True, ) return if not result or not result.get("items"): await interaction.followup.send( "No evolution cards found for your team.", ephemeral=True, ) return items = result["items"] total_count = result.get("count", len(items)) # Apply "close" filter client-side if progress and progress.lower() == "close": items = [s for s in items if is_close_to_tierup(s)] if not items: await interaction.followup.send( "No cards are close to a tier-up right now.", ephemeral=True, ) return # Build embed embed = discord.Embed( title=f"Evolution Progress — {team.get('lname', 'Your Team')}", color=discord.Color.purple(), ) lines = [] for state in items: # Try to get player name from the state player_name = state.get( "player_name", f"Player #{state.get('player_id', '?')}" ) entry = format_evo_entry(state) lines.append(f"**{player_name}**\n{entry}") embed.description = "\n\n".join(lines) if lines else "No evolution data." # Pagination footer per_page = 10 total_pages = max(1, (total_count + per_page - 1) // per_page) embed.set_footer(text=f"Page {page}/{total_pages} • {total_count} total cards") await interaction.followup.send(embed=embed)