All checks were successful
Build Docker Image / build (pull_request) Successful in 1m23s
Add /evo status command showing paginated evolution progress: - Progress bar with formula value vs next threshold - Tier display names (Unranked/Initiate/Rising/Ascendant/Evolved) - Formula shorthands (PA+TB×2, IP+K) - Filters: card_type, tier, progress="close" (within 80%) - Pagination at 10 per page - Evolution cog registered in players_new/__init__.py - 15 unit tests for pure helper functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
207 lines
6.3 KiB
Python
207 lines
6.3 KiB
Python
# 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)
|