All checks were successful
Build Docker Image / build (pull_request) Successful in 1m32s
- Update module docstring: replace evolution/cards with refractor/cards,
drop old tier names (Unranked/Initiate/Rising/Ascendant/Evolved), add
correct tier names (Base Card/Base Chrome/Refractor/Gold Refractor/
Superfractor)
- Fix API call: db_get("evolution/cards") → db_get("refractor/cards")
- Add TIER_BADGES dict {1:"[BC]", 2:"[R]", 3:"[GR]", 4:"[SF]"}
- Update format_refractor_entry to prepend badge label for T1-T4 (T0 has
no badge)
- Add TestTierBadges test class (11 tests) asserting badge values and
presence in formatted output
- Update test_player_name_in_output to accommodate badge-prefixed bold name
Dead utilities/evolution_notifications.py has no source file on this branch
(WP-14/PR #112 already delivered the replacement).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
214 lines
6.6 KiB
Python
214 lines
6.6 KiB
Python
"""
|
||
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.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")
|
||
card_type = card_state.get("card_type", "batter")
|
||
current_tier = card_state.get("current_tier", 0)
|
||
formula_value = card_state.get("formula_value", 0)
|
||
next_threshold = card_state.get("next_threshold")
|
||
|
||
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 = state.get("formula_value", 0)
|
||
next_threshold = state.get("next_threshold")
|
||
if current_tier >= 4 or not next_threshold:
|
||
continue
|
||
if formula_value >= 0.8 * 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 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="Card type filter (batter, sp, rp)",
|
||
season="Season number (default: current)",
|
||
tier="Filter by current tier (0-4)",
|
||
progress='Use "close" to show cards within 80% of their next tier',
|
||
page="Page number (default: 1, 10 cards per page)",
|
||
)
|
||
async def refractor_status(
|
||
self,
|
||
interaction: discord.Interaction,
|
||
card_type: Optional[str] = None,
|
||
season: Optional[int] = None,
|
||
tier: Optional[int] = None,
|
||
progress: Optional[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
|
||
|
||
params = [("team_id", team["id"])]
|
||
if card_type:
|
||
params.append(("card_type", card_type))
|
||
if season is not None:
|
||
params.append(("season", season))
|
||
if tier is not None:
|
||
params.append(("tier", tier))
|
||
|
||
data = await db_get("refractor/cards", params=params)
|
||
if not data:
|
||
await interaction.edit_original_response(
|
||
content="No refractor data found for your team."
|
||
)
|
||
return
|
||
|
||
items = data if isinstance(data, list) else data.get("cards", [])
|
||
if not items:
|
||
await interaction.edit_original_response(
|
||
content="No refractor data found for your team."
|
||
)
|
||
return
|
||
|
||
if progress == "close":
|
||
items = apply_close_filter(items)
|
||
if not items:
|
||
await interaction.edit_original_response(
|
||
content="No cards are currently close to a tier advancement."
|
||
)
|
||
return
|
||
|
||
page_items, total_pages = paginate(items, page)
|
||
lines = [format_refractor_entry(state) for state in page_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} · {len(items)} card(s) total")
|
||
|
||
await interaction.edit_original_response(embed=embed)
|
||
|
||
|
||
async def setup(bot):
|
||
await bot.add_cog(Refractor(bot))
|