""" Refractor cog — /refractor status slash command. Displays a team's refractor progress: formula value vs next threshold with a progress bar, paginated 10 cards per page. Tier names: Base Card (T0) / Base Chrome (T1) / Refractor (T2) / Gold Refractor (T3) / Superfractor (T4). Depends on WP-07 (refractor/cards API endpoint). """ import logging from typing import Optional import discord from discord import app_commands from discord.app_commands import Choice from discord.ext import commands from api_calls import db_get from helpers.discord_utils import get_team_embed from helpers.main import get_team_by_owner logger = logging.getLogger("discord_app") PAGE_SIZE = 10 TIER_NAMES = { 0: "Base Card", 1: "Base Chrome", 2: "Refractor", 3: "Gold Refractor", 4: "Superfractor", } # Tier-specific labels for the status display. TIER_SYMBOLS = { 0: "Base", # Base Card — used in summary only, not in per-card display 1: "T1", # Base Chrome 2: "T2", # Refractor 3: "T3", # Gold Refractor 4: "T4★", # Superfractor } _FULL_BAR = "▰" * 12 # Embed accent colors per tier (used for single-tier filtered views). TIER_COLORS = { 0: 0x95A5A6, # slate grey 1: 0xBDC3C7, # silver/chrome 2: 0x3498DB, # refractor blue 3: 0xF1C40F, # gold 4: 0x1ABC9C, # teal superfractor } def render_progress_bar(current: int, threshold: int, width: int = 12) -> str: """ Render a Unicode block progress bar. Examples: render_progress_bar(120, 149) -> '▰▰▰▰▰▰▰▰▰▰▱▱' render_progress_bar(0, 100) -> '▱▱▱▱▱▱▱▱▱▱▱▱' render_progress_bar(100, 100) -> '▰▰▰▰▰▰▰▰▰▰▰▰' """ if threshold <= 0: filled = width else: ratio = max(0.0, min(current / threshold, 1.0)) filled = round(ratio * width) empty = width - filled return f"{'▰' * filled}{'▱' * empty}" def _pct_label(current: int, threshold: int) -> str: """Return a percentage string like '80%'.""" if threshold <= 0: return "100%" return f"{min(current / threshold, 1.0):.0%}" def format_refractor_entry(card_state: dict) -> str: """ Format a single card state dict as a compact two-line display string. Output example (base card — no suffix): **Mike Trout** ▰▰▰▰▰▰▰▰▰▰▱▱ 120/149 (80%) Output example (evolved — suffix tag): **Mike Trout** — Base Chrome [T1] ▰▰▰▰▰▰▰▰▰▰▱▱ 120/149 (80%) Output example (fully evolved): **Barry Bonds** — Superfractor [T4★] ▰▰▰▰▰▰▰▰▰▰▰▰ `MAX` """ player_name = card_state.get("player_name", "Unknown") current_tier = card_state.get("current_tier", 0) formula_value = int(card_state.get("current_value", 0)) next_threshold = int(card_state.get("next_threshold") or 0) or None if current_tier == 0: first_line = f"**{player_name}**" else: tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}") symbol = TIER_SYMBOLS.get(current_tier, "") first_line = f"**{player_name}** — {tier_label} [{symbol}]" if current_tier >= 4 or next_threshold is None: second_line = f"{_FULL_BAR} `MAX`" else: bar = render_progress_bar(formula_value, next_threshold) pct = _pct_label(formula_value, next_threshold) second_line = f"{bar} {formula_value}/{next_threshold} ({pct})" return f"{first_line}\n{second_line}" def build_tier_summary(items: list, total_count: int) -> str: """ Build a one-line summary of tier distribution from the current page items. Returns something like: 'T0: 3 T1: 12 T2: 8 T3: 5 T4★: 2 — 30 total' """ counts = {t: 0 for t in range(5)} for item in items: t = item.get("current_tier", 0) if t in counts: counts[t] += 1 parts = [] for t in range(5): if counts[t] > 0: parts.append(f"{TIER_SYMBOLS[t]}: {counts[t]}") summary = " ".join(parts) if parts else "No cards" return f"{summary} — {total_count} total" def build_status_embed( team: dict, items: list, page: int, total_pages: int, total_count: int, tier_filter: Optional[int] = None, ) -> discord.Embed: """ Build the refractor status embed with team branding. Uses get_team_embed for consistent team color/logo/footer, then layers on the refractor-specific content. """ embed = get_team_embed(f"{team['sname']} — Refractor Status", team=team) # Override color for single-tier views to match the tier's identity. if tier_filter is not None and tier_filter in TIER_COLORS: embed.color = TIER_COLORS[tier_filter] header = build_tier_summary(items, total_count) lines = [format_refractor_entry(state) for state in items] body = "\n\n".join(lines) if lines else "*No cards found.*" embed.description = f"```{header}```\n{body}" existing_footer = embed.footer.text or "" page_text = f"Page {page}/{total_pages}" embed.set_footer( text=f"{page_text} · {existing_footer}" if existing_footer else page_text, icon_url=embed.footer.icon_url, ) return embed def apply_close_filter(card_states: list) -> list: """ Return only cards within 80% of their next tier threshold. Fully evolved cards (T4 or no next_threshold) are excluded. """ result = [] for state in card_states: current_tier = state.get("current_tier", 0) formula_value = int(state.get("current_value", 0)) next_threshold = state.get("next_threshold") if current_tier >= 4 or not next_threshold: continue if formula_value >= 0.8 * int(next_threshold): result.append(state) return result def paginate(items: list, page: int, page_size: int = PAGE_SIZE) -> tuple: """ Slice items for the given 1-indexed page. Returns (page_items, total_pages). Page is clamped to valid range. """ total_pages = max(1, (len(items) + page_size - 1) // page_size) page = max(1, min(page, total_pages)) start = (page - 1) * page_size return items[start : start + page_size], total_pages class RefractorPaginationView(discord.ui.View): """Prev/Next buttons for refractor status pagination.""" def __init__( self, team: dict, page: int, total_pages: int, total_count: int, params: list, owner_id: int, tier_filter: Optional[int] = None, timeout: float = 120.0, ): super().__init__(timeout=timeout) self.team = team self.page = page self.total_pages = total_pages self.total_count = total_count self.base_params = params self.owner_id = owner_id self.tier_filter = tier_filter self._update_buttons() def _update_buttons(self): self.prev_btn.disabled = self.page <= 1 self.next_btn.disabled = self.page >= self.total_pages async def _fetch_and_update(self, interaction: discord.Interaction): offset = (self.page - 1) * PAGE_SIZE params = [(k, v) for k, v in self.base_params if k != "offset"] params.append(("offset", offset)) data = await db_get("refractor/cards", params=params) items = data.get("items", []) if isinstance(data, dict) else [] self.total_count = ( data.get("count", self.total_count) if isinstance(data, dict) else self.total_count ) self.total_pages = max(1, (self.total_count + PAGE_SIZE - 1) // PAGE_SIZE) self.page = min(self.page, self.total_pages) embed = build_status_embed( self.team, items, self.page, self.total_pages, self.total_count, tier_filter=self.tier_filter, ) self._update_buttons() await interaction.response.edit_message(embed=embed, view=self) @discord.ui.button(label="◀ Prev", style=discord.ButtonStyle.grey) async def prev_btn( self, interaction: discord.Interaction, button: discord.ui.Button ): if interaction.user.id != self.owner_id: return self.page = max(1, self.page - 1) await self._fetch_and_update(interaction) @discord.ui.button(label="Next ▶", style=discord.ButtonStyle.grey) async def next_btn( self, interaction: discord.Interaction, button: discord.ui.Button ): if interaction.user.id != self.owner_id: return self.page = min(self.total_pages, self.page + 1) await self._fetch_and_update(interaction) async def on_timeout(self): self.prev_btn.disabled = True self.next_btn.disabled = True class Refractor(commands.Cog): """Refractor progress tracking slash commands.""" def __init__(self, bot): self.bot = bot group_refractor = app_commands.Group( name="refractor", description="Refractor tracking commands" ) @group_refractor.command( name="status", description="Show your team's refractor progress" ) @app_commands.describe( card_type="Filter by card type", tier="Filter by current tier", progress="Filter by advancement progress", page="Page number (default: 1, 10 cards per page)", ) @app_commands.choices( card_type=[ Choice(value="batter", name="Batter"), Choice(value="sp", name="Starting Pitcher"), Choice(value="rp", name="Relief Pitcher"), ], tier=[ Choice(value="0", name="T0 — Base Card"), Choice(value="1", name="T1 — Base Chrome"), Choice(value="2", name="T2 — Refractor"), Choice(value="3", name="T3 — Gold Refractor"), Choice(value="4", name="T4 — Superfractor"), ], progress=[ Choice(value="close", name="Close to next tier (≥80%)"), ], ) async def refractor_status( self, interaction: discord.Interaction, card_type: Optional[Choice[str]] = None, tier: Optional[Choice[str]] = None, progress: Optional[Choice[str]] = None, page: int = 1, ): """Show a paginated view of the invoking user's team refractor progress.""" await interaction.response.defer(ephemeral=True) team = await get_team_by_owner(interaction.user.id) if not team: await interaction.edit_original_response( content="You don't have a team. Sign up with /newteam first." ) return page = max(1, page) offset = (page - 1) * PAGE_SIZE params = [("team_id", team["id"]), ("limit", PAGE_SIZE), ("offset", offset)] if card_type: params.append(("card_type", card_type.value)) if tier is not None: params.append(("tier", tier.value)) if progress: params.append(("progress", progress.value)) tier_filter = int(tier.value) if tier is not None else None data = await db_get("refractor/cards", params=params) if not data: logger.error( "Refractor API returned empty response for team %s", team["id"] ) await interaction.edit_original_response( content="No refractor data found for your team." ) return # API error responses contain "detail" key if isinstance(data, dict) and "detail" in data: logger.error( "Refractor API error for team %s: %s", team["id"], data["detail"] ) await interaction.edit_original_response( content="Something went wrong fetching refractor data. Please try again later." ) return items = data if isinstance(data, list) else data.get("items", []) total_count = ( data.get("count", len(items)) if isinstance(data, dict) else len(items) ) logger.debug( "Refractor status for team %s: %d items returned, %d total (page %d)", team["id"], len(items), total_count, page, ) if not items: has_filters = card_type or tier is not None or progress if has_filters: parts = [] if card_type: parts.append(f"**{card_type.name}**") if tier is not None: parts.append(f"**{tier.name}**") if progress: parts.append(f"progress: **{progress.name}**") filter_str = ", ".join(parts) await interaction.edit_original_response( content=f"No cards match your filters ({filter_str}). Try `/refractor status` with no filters to see all cards." ) else: await interaction.edit_original_response( content="No refractor data found for your team." ) return total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE) page = min(page, total_pages) embed = build_status_embed( team, items, page, total_pages, total_count, tier_filter=tier_filter ) if total_pages > 1: view = RefractorPaginationView( team=team, page=page, total_pages=total_pages, total_count=total_count, params=params, owner_id=interaction.user.id, tier_filter=tier_filter, ) await interaction.edit_original_response(embed=embed, view=view) else: await interaction.edit_original_response(embed=embed) async def setup(bot): await bot.add_cog(Refractor(bot))