diff --git a/cogs/evolution.py b/cogs/refractor.py similarity index 77% rename from cogs/evolution.py rename to cogs/refractor.py index 99999ae..9b0bc44 100644 --- a/cogs/evolution.py +++ b/cogs/refractor.py @@ -1,7 +1,7 @@ """ -Evolution cog — /evo status slash command. +Refractor cog — /refractor status slash command. -Displays a team's evolution progress: formula value vs next threshold +Displays a team's refractor progress: formula value vs next threshold with a progress bar, paginated 10 cards per page. Depends on WP-07 (evolution/cards API endpoint). @@ -15,22 +15,22 @@ from discord import app_commands from discord.ext import commands from api_calls import db_get -from helpers import get_team_by_owner +from helpers.main import get_team_by_owner logger = logging.getLogger("discord_app") PAGE_SIZE = 10 TIER_NAMES = { - 0: "Unranked", - 1: "Initiate", - 2: "Rising", - 3: "Ascendant", - 4: "Evolved", + 0: "Base Chrome", + 1: "Refractor", + 2: "Gold Refractor", + 3: "Superfractor", + 4: "Superfractor", } FORMULA_LABELS = { - "batter": "PA+TB\u00d72", + "batter": "PA+TB×2", "sp": "IP+K", "rp": "IP+K", } @@ -54,7 +54,7 @@ def render_progress_bar(current: int, threshold: int, width: int = 10) -> str: return f"[{'=' * filled}{'-' * empty}]" -def format_evo_entry(card_state: dict) -> str: +def format_refractor_entry(card_state: dict) -> str: """ Format a single card state dict as a display string. @@ -62,7 +62,7 @@ def format_evo_entry(card_state: dict) -> str: next_threshold (None if fully evolved). Output example: - **Mike Trout** (Initiate) + **Mike Trout** (Refractor) [========--] 120/149 (PA+TB×2) — T1 → T2 """ player_name = card_state.get("player_name", "Unknown") @@ -76,10 +76,10 @@ def format_evo_entry(card_state: dict) -> str: if current_tier >= 4 or next_threshold is None: bar = "[==========]" - detail = "FULLY EVOLVED \u2605" + detail = "FULLY EVOLVED ★" else: bar = render_progress_bar(formula_value, next_threshold) - detail = f"{formula_value}/{next_threshold} ({formula_label}) \u2014 T{current_tier} \u2192 T{current_tier + 1}" + detail = f"{formula_value}/{next_threshold} ({formula_label}) — T{current_tier} → T{current_tier + 1}" first_line = f"**{player_name}** ({tier_label})" second_line = f"{bar} {detail}" @@ -116,34 +116,36 @@ def paginate(items: list, page: int, page_size: int = PAGE_SIZE) -> tuple: return items[start : start + page_size], total_pages -class Evolution(commands.Cog): - """Evolution progress tracking slash commands.""" +class Refractor(commands.Cog): + """Refractor progress tracking slash commands.""" def __init__(self, bot): self.bot = bot - evo_group = app_commands.Group( - name="evo", description="Evolution tracking commands" + refractor_group = app_commands.Group( + name="refractor", description="Refractor tracking commands" ) - @evo_group.command(name="status", description="Show your team's evolution progress") + @refractor_group.command( + name="status", description="Show your team's refractor progress" + ) @app_commands.describe( - type="Card type filter (batter, sp, rp)", + 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 evo_status( + async def refractor_status( self, interaction: discord.Interaction, - type: Optional[str] = None, + 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 evolution progress.""" + """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) @@ -154,8 +156,8 @@ class Evolution(commands.Cog): return params = [("team_id", team["id"])] - if type: - params.append(("card_type", type)) + if card_type: + params.append(("card_type", card_type)) if season is not None: params.append(("season", season)) if tier is not None: @@ -164,14 +166,14 @@ class Evolution(commands.Cog): data = await db_get("evolution/cards", params=params) if not data: await interaction.edit_original_response( - content="No evolution data found for your team." + 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 evolution data found for your team." + content="No refractor data found for your team." ) return @@ -184,19 +186,17 @@ class Evolution(commands.Cog): return page_items, total_pages = paginate(items, page) - lines = [format_evo_entry(state) for state in page_items] + lines = [format_refractor_entry(state) for state in page_items] embed = discord.Embed( - title=f"{team['sname']} Evolution Status", + title=f"{team['sname']} Refractor Status", description="\n\n".join(lines), color=0x6F42C1, ) - embed.set_footer( - text=f"Page {page}/{total_pages} \u00b7 {len(items)} card(s) total" - ) + 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(Evolution(bot)) + await bot.add_cog(Refractor(bot)) diff --git a/paperdynasty.py b/paperdynasty.py index 203703a..219ce90 100644 --- a/paperdynasty.py +++ b/paperdynasty.py @@ -53,7 +53,7 @@ COGS = [ "cogs.players", "cogs.gameplay", "cogs.economy_new.scouting", - "cogs.evolution", + "cogs.refractor", ] intents = discord.Intents.default() diff --git a/tests/test_evolution_commands.py b/tests/test_refractor_commands.py similarity index 82% rename from tests/test_evolution_commands.py rename to tests/test_refractor_commands.py index 8aab128..1e44e48 100644 --- a/tests/test_evolution_commands.py +++ b/tests/test_refractor_commands.py @@ -1,9 +1,9 @@ """ -Unit tests for evolution command helper functions (WP-11). +Unit tests for refractor command helper functions (WP-11). Tests cover: - render_progress_bar: ASCII bar rendering at various fill levels -- format_evo_entry: Full card state formatting including fully evolved case +- format_refractor_entry: Full card state formatting including fully evolved case - apply_close_filter: 80% proximity filter logic - paginate: 1-indexed page slicing and total-page calculation - TIER_NAMES: Display names for all tiers @@ -23,9 +23,9 @@ from discord.ext import commands # Make the repo root importable sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from cogs.evolution import ( +from cogs.refractor import ( render_progress_bar, - format_evo_entry, + format_refractor_entry, apply_close_filter, paginate, TIER_NAMES, @@ -118,13 +118,13 @@ class TestRenderProgressBar: # --------------------------------------------------------------------------- -# format_evo_entry +# format_refractor_entry # --------------------------------------------------------------------------- -class TestFormatEvoEntry: +class TestFormatRefractorEntry: """ - Tests for format_evo_entry(). + Tests for format_refractor_entry(). Verifies player name, tier label, progress bar, formula label, and the special fully-evolved formatting. @@ -132,54 +132,54 @@ class TestFormatEvoEntry: def test_player_name_in_output(self, batter_state): """Player name is bold in the first line.""" - result = format_evo_entry(batter_state) + result = format_refractor_entry(batter_state) assert "**Mike Trout**" in result def test_tier_label_in_output(self, batter_state): - """Current tier name (Initiate for T1) appears in output.""" - result = format_evo_entry(batter_state) - assert "(Initiate)" in result + """Current tier name (Refractor for T1) appears in output.""" + result = format_refractor_entry(batter_state) + assert "(Refractor)" in result def test_progress_values_in_output(self, batter_state): """current/threshold values appear in output.""" - result = format_evo_entry(batter_state) + result = format_refractor_entry(batter_state) assert "120/149" in result def test_formula_label_batter(self, batter_state): """Batter formula label PA+TB×2 appears in output.""" - result = format_evo_entry(batter_state) - assert "PA+TB\u00d72" in result + result = format_refractor_entry(batter_state) + assert "PA+TB×2" in result def test_tier_progression_arrow(self, batter_state): """T1 → T2 arrow progression appears for non-evolved cards.""" - result = format_evo_entry(batter_state) - assert "T1 \u2192 T2" in result + result = format_refractor_entry(batter_state) + assert "T1 → T2" in result def test_sp_formula_label(self, sp_state): """SP formula label IP+K appears for starting pitchers.""" - result = format_evo_entry(sp_state) + result = format_refractor_entry(sp_state) assert "IP+K" in result def test_fully_evolved_no_threshold(self, evolved_state): """T4 card with next_threshold=None shows FULLY EVOLVED.""" - result = format_evo_entry(evolved_state) + result = format_refractor_entry(evolved_state) assert "FULLY EVOLVED" in result def test_fully_evolved_by_tier(self, batter_state): """current_tier=4 triggers fully evolved display even with a threshold.""" batter_state["current_tier"] = 4 batter_state["next_threshold"] = 200 - result = format_evo_entry(batter_state) + result = format_refractor_entry(batter_state) assert "FULLY EVOLVED" in result def test_fully_evolved_no_arrow(self, evolved_state): """Fully evolved cards don't show a tier arrow.""" - result = format_evo_entry(evolved_state) - assert "\u2192" not in result + result = format_refractor_entry(evolved_state) + assert "→" not in result def test_two_line_output(self, batter_state): """Output always has exactly two lines (name line + bar line).""" - result = format_evo_entry(batter_state) + result = format_refractor_entry(batter_state) lines = result.split("\n") assert len(lines) == 2 @@ -307,23 +307,23 @@ class TestTierNames: """ Verify all tier display names are correctly defined. - T0=Unranked, T1=Initiate, T2=Rising, T3=Ascendant, T4=Evolved + T0=Base Chrome, T1=Refractor, T2=Gold Refractor, T3=Superfractor, T4=Superfractor """ - def test_t0_unranked(self): - assert TIER_NAMES[0] == "Unranked" + def test_t0_base_chrome(self): + assert TIER_NAMES[0] == "Base Chrome" - def test_t1_initiate(self): - assert TIER_NAMES[1] == "Initiate" + def test_t1_refractor(self): + assert TIER_NAMES[1] == "Refractor" - def test_t2_rising(self): - assert TIER_NAMES[2] == "Rising" + def test_t2_gold_refractor(self): + assert TIER_NAMES[2] == "Gold Refractor" - def test_t3_ascendant(self): - assert TIER_NAMES[3] == "Ascendant" + def test_t3_superfractor(self): + assert TIER_NAMES[3] == "Superfractor" - def test_t4_evolved(self): - assert TIER_NAMES[4] == "Evolved" + def test_t4_superfractor(self): + assert TIER_NAMES[4] == "Superfractor" # --------------------------------------------------------------------------- @@ -349,7 +349,7 @@ def mock_interaction(): @pytest.mark.asyncio -async def test_evo_status_no_team(mock_bot, mock_interaction): +async def test_refractor_status_no_team(mock_bot, mock_interaction): """ When the user has no team, the command replies with a signup prompt and does not call db_get. @@ -357,13 +357,13 @@ async def test_evo_status_no_team(mock_bot, mock_interaction): Why: get_team_by_owner returning None means the user is unregistered; the command must short-circuit before hitting the API. """ - from cogs.evolution import Evolution + from cogs.refractor import Refractor - cog = Evolution(mock_bot) + cog = Refractor(mock_bot) - with patch("cogs.evolution.get_team_by_owner", new=AsyncMock(return_value=None)): - with patch("cogs.evolution.db_get", new=AsyncMock()) as mock_db: - await cog.evo_status.callback(cog, mock_interaction) + with patch("cogs.refractor.get_team_by_owner", new=AsyncMock(return_value=None)): + with patch("cogs.refractor.db_get", new=AsyncMock()) as mock_db: + await cog.refractor_status.callback(cog, mock_interaction) mock_db.assert_not_called() call_kwargs = mock_interaction.edit_original_response.call_args @@ -372,23 +372,23 @@ async def test_evo_status_no_team(mock_bot, mock_interaction): @pytest.mark.asyncio -async def test_evo_status_empty_roster(mock_bot, mock_interaction): +async def test_refractor_status_empty_roster(mock_bot, mock_interaction): """ When the API returns an empty card list, the command sends an informative 'no data' message rather than an empty embed. - Why: An empty list is valid (team has no evolved cards yet); + Why: An empty list is valid (team has no refractor cards yet); the command should not crash or send a blank embed. """ - from cogs.evolution import Evolution + from cogs.refractor import Refractor - cog = Evolution(mock_bot) + cog = Refractor(mock_bot) team = {"id": 1, "sname": "Test"} - with patch("cogs.evolution.get_team_by_owner", new=AsyncMock(return_value=team)): - with patch("cogs.evolution.db_get", new=AsyncMock(return_value={"cards": []})): - await cog.evo_status.callback(cog, mock_interaction) + with patch("cogs.refractor.get_team_by_owner", new=AsyncMock(return_value=team)): + with patch("cogs.refractor.db_get", new=AsyncMock(return_value={"cards": []})): + await cog.refractor_status.callback(cog, mock_interaction) call_kwargs = mock_interaction.edit_original_response.call_args content = call_kwargs.kwargs.get("content", "") - assert "no evolution data" in content.lower() + assert "no refractor data" in content.lower()