paper-dynasty-discord/cogs/players_new/evolution.py
Cal Corum eae897ddd6 fix: address PR review — wire up tier-up embeds, fix logger, clean up tests
- Import notify_tier_completion from helpers.evolution_notifs instead of
  using the local stub in logic_gameplay.py (WP-14 embeds were dead code)
- Add module-level logger to helpers/main.py, replace bare logging.warning()
- Remove duplicate @pytest.mark.asyncio decorator in test_card_embed_evolution.py
- Fix progress='close' filter to use filtered count in pagination footer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:54:55 -05:00

208 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)]
total_count = len(items)
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)