""" 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.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", } FORMULA_LABELS = { "batter": "PA+TB×2", "sp": "IP+K", "rp": "IP+K", } TIER_BADGES = {1: "[BC]", 2: "[R]", 3: "[GR]", 4: "[SF]"} def render_progress_bar(current: int, threshold: int, width: int = 10) -> str: """ Render a fixed-width ASCII 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 = min(current / threshold, 1.0) filled = round(ratio * width) empty = width - filled return f"[{'=' * filled}{'-' * empty}]" def format_refractor_entry(card_state: dict) -> str: """ Format a single card state dict as a display string. Expected keys: player_name, card_type, current_tier, formula_value, next_threshold (None if fully evolved). A tier badge prefix (e.g. [BC], [R], [GR], [SF]) is prepended to the player name for tiers 1-4. T0 cards have no badge. Output example: **[BC] Mike Trout** (Base Chrome) [========--] 120/149 (PA+TB×2) — T1 → T2 """ player_name = card_state.get("player_name", "Unknown") track = card_state.get("track", {}) card_type = track.get("card_type", "batter") 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 tier_label = TIER_NAMES.get(current_tier, f"T{current_tier}") formula_label = FORMULA_LABELS.get(card_type, card_type) badge = TIER_BADGES.get(current_tier, "") display_name = f"{badge} {player_name}" if badge else player_name if current_tier >= 4 or next_threshold is None: bar = "[==========]" detail = "FULLY EVOLVED ★" else: bar = render_progress_bar(formula_value, next_threshold) detail = f"{formula_value}/{next_threshold} ({formula_label}) — T{current_tier} → T{current_tier + 1}" first_line = f"**{display_name}** ({tier_label})" second_line = f"{bar} {detail}" return f"{first_line}\n{second_line}" 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, 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._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) lines = [format_refractor_entry(state) for state in items] embed = discord.Embed( title=f"{self.team['sname']} Refractor Status", description="\n\n".join(lines) if lines else "No cards found.", color=0x6F42C1, ) embed.set_footer( text=f"Page {self.page}/{self.total_pages} · {self.total_count} card(s) total" ) self._update_buttons() await interaction.response.edit_message(embed=embed, view=self) @discord.ui.button(label="◀ Prev", style=discord.ButtonStyle.blurple) 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.blurple) 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)) 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: if progress == "close": await interaction.edit_original_response( content="No cards are currently close to a tier advancement." ) 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) lines = [format_refractor_entry(state) for state in items] embed = discord.Embed( title=f"{team['sname']} Refractor Status", description="\n\n".join(lines), color=0x6F42C1, ) embed.set_footer( text=f"Page {page}/{total_pages} · {total_count} card(s) total" ) 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, ) 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))