All checks were successful
Build Docker Image / build (pull_request) Successful in 1m10s
- Add section headers (Active Roster, Minor League, Injured List) to the main summary embed in _create_roster_embeds so each roster section is clearly labeled for users - Fix incorrect docstring in team_service.get_team_roster that had the shortil/longil mapping backwards Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
226 lines
8.1 KiB
Python
226 lines
8.1 KiB
Python
"""
|
|
Team roster commands for Discord Bot v2.0
|
|
"""
|
|
|
|
from typing import Dict, Any, List
|
|
|
|
import discord
|
|
from discord.ext import commands
|
|
|
|
from config import get_config
|
|
from models.player import Player
|
|
from services import team_service
|
|
from models.team import Team
|
|
from utils.logging import get_contextual_logger
|
|
from utils.decorators import logged_command
|
|
from utils.permissions import requires_team
|
|
from views.embeds import EmbedTemplate, EmbedColors
|
|
|
|
|
|
class TeamRosterCommands(commands.Cog):
|
|
"""Team roster command handlers."""
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
self.logger = get_contextual_logger(f"{__name__}.TeamRosterCommands")
|
|
self.logger.info("TeamRosterCommands cog initialized")
|
|
|
|
@discord.app_commands.command(name="roster", description="Display team roster")
|
|
@discord.app_commands.describe(
|
|
abbrev="Team abbreviation (e.g., BSG, DEN, WV, etc.)",
|
|
roster_type="Roster week: current or next (defaults to current)",
|
|
)
|
|
@discord.app_commands.choices(
|
|
roster_type=[
|
|
discord.app_commands.Choice(name="Current Week", value="current"),
|
|
discord.app_commands.Choice(name="Next Week", value="next"),
|
|
]
|
|
)
|
|
@requires_team()
|
|
@logged_command("/roster")
|
|
async def team_roster(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
abbrev: str,
|
|
roster_type: str = "current",
|
|
):
|
|
"""Display team roster with position breakdowns."""
|
|
await interaction.response.defer()
|
|
|
|
# Get team by abbreviation
|
|
team = await team_service.get_team_by_abbrev(abbrev, get_config().sba_season)
|
|
|
|
if team is None:
|
|
self.logger.info("Team not found", team_abbrev=abbrev)
|
|
embed = EmbedTemplate.error(
|
|
title="Team Not Found",
|
|
description=f"No team found with abbreviation '{abbrev.upper()}'",
|
|
)
|
|
await interaction.followup.send(embed=embed)
|
|
return
|
|
|
|
# Get roster data
|
|
roster_data = await team_service.get_team_roster(team.id, roster_type)
|
|
|
|
if not roster_data:
|
|
embed = EmbedTemplate.error(
|
|
title="Roster Not Available",
|
|
description=f"No {roster_type} roster data available for {team.abbrev}",
|
|
)
|
|
await interaction.followup.send(embed=embed)
|
|
return
|
|
|
|
# Create roster embeds
|
|
embeds = await self._create_roster_embeds(team, roster_data, roster_type)
|
|
|
|
# Send first embed and follow up with others if needed
|
|
await interaction.followup.send(embed=embeds[0])
|
|
for embed in embeds[1:]:
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
async def _create_roster_embeds(
|
|
self, team: Team, roster_data: Dict[str, Any], roster_type: str
|
|
) -> List[discord.Embed]:
|
|
"""Create embeds for team roster data."""
|
|
embeds = []
|
|
|
|
# Main roster embed
|
|
embed = EmbedTemplate.create_base_embed(
|
|
title=f"{team.abbrev} - {roster_type.title()} Week",
|
|
description=f"{team.lname} Roster Breakdown",
|
|
color=int(team.color, 16) if team.color else EmbedColors.PRIMARY,
|
|
)
|
|
|
|
# Position counts for active roster
|
|
roster_titles = {
|
|
"active": "Active Roster",
|
|
"longil": "Minor League",
|
|
"shortil": "Injured List",
|
|
}
|
|
for key in ["active", "longil", "shortil"]:
|
|
if key in roster_data:
|
|
this_roster = roster_data[key]
|
|
|
|
embed.add_field(name=roster_titles[key], value="\u200b", inline=False)
|
|
|
|
players = this_roster.get("players")
|
|
if len(players) > 0:
|
|
this_team = players[0].get(
|
|
"team", {"id": "Unknown", "sname": "Unknown"}
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Team (ID)",
|
|
value=f'{this_team.get("sname")} ({this_team.get("id")})',
|
|
inline=True,
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Player Count", value=f"{len(players)} Players"
|
|
)
|
|
|
|
# Total WAR
|
|
total_war = this_roster.get("WARa", 0)
|
|
embed.add_field(
|
|
name="Total sWAR",
|
|
value=(
|
|
f"{total_war:.2f}"
|
|
if isinstance(total_war, (int, float))
|
|
else str(total_war)
|
|
),
|
|
inline=True,
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Position Counts",
|
|
value=self._position_code_block(this_roster),
|
|
inline=False,
|
|
)
|
|
|
|
embeds.append(embed)
|
|
|
|
# Create detailed player list embeds if there are players
|
|
for roster_name, roster_info in roster_data.items():
|
|
if (
|
|
roster_name in ["active", "longil", "shortil"]
|
|
and "players" in roster_info
|
|
):
|
|
players = sorted(
|
|
roster_info["players"],
|
|
key=lambda player: player.get("wara", 0),
|
|
reverse=True,
|
|
)
|
|
if players:
|
|
player_embed = self._create_player_list_embed(
|
|
team, roster_name, players
|
|
)
|
|
embeds.append(player_embed)
|
|
|
|
return embeds
|
|
|
|
def _position_code_block(self, roster_data: dict) -> str:
|
|
return (
|
|
f"```\n C 1B 2B 3B SS\n"
|
|
f' {roster_data.get("C", 0)} {roster_data.get("1B", 0)} {roster_data.get("2B", 0)} '
|
|
f'{roster_data.get("3B", 0)} {roster_data.get("SS", 0)}\n\nLF CF RF SP RP\n'
|
|
f' {roster_data.get("LF", 0)} {roster_data.get("CF", 0)} {roster_data.get("RF", 0)} '
|
|
f'{roster_data.get("SP", 0)} {roster_data.get("RP", 0)}\n```'
|
|
)
|
|
|
|
def _create_player_list_embed(
|
|
self, team: Team, roster_name: str, players: List[Dict[str, Any]]
|
|
) -> discord.Embed:
|
|
"""Create an embed with detailed player list."""
|
|
roster_titles = {
|
|
"active": "Active Roster",
|
|
"longil": "Minor League",
|
|
"shortil": "Injured List",
|
|
}
|
|
|
|
embed = EmbedTemplate.create_base_embed(
|
|
title=f"{team.abbrev} - {roster_titles.get(roster_name, roster_name.title())}",
|
|
color=int(team.color, 16) if team.color else EmbedColors.PRIMARY,
|
|
)
|
|
|
|
# Group players by position for better organization
|
|
batters = []
|
|
pitchers = []
|
|
|
|
for player in players:
|
|
try:
|
|
this_player = Player.from_api_data(player)
|
|
player_line = f"{this_player} - sWAR: {this_player.wara}"
|
|
|
|
if this_player.is_pitcher:
|
|
pitchers.append(player_line)
|
|
else:
|
|
batters.append(player_line)
|
|
except Exception as e:
|
|
self.logger.warning(
|
|
f"Failed to create player from data: {e}",
|
|
player_id=player.get("id"),
|
|
)
|
|
|
|
# Add player lists to embed
|
|
if batters:
|
|
# Split long lists into multiple fields if needed
|
|
batter_chunks = self._chunk_list(batters, 16)
|
|
for i, chunk in enumerate(batter_chunks):
|
|
field_name = "Batters" if i == 0 else f"Batters (cont.)"
|
|
embed.add_field(name=field_name, value="\n".join(chunk), inline=True)
|
|
embed.add_field(name="", value="", inline=False)
|
|
|
|
if pitchers:
|
|
pitcher_chunks = self._chunk_list(pitchers, 16)
|
|
for i, chunk in enumerate(pitcher_chunks):
|
|
field_name = "Pitchers" if i == 0 else f"Pitchers (cont.)"
|
|
embed.add_field(name=field_name, value="\n".join(chunk), inline=False)
|
|
|
|
embed.set_footer(text=f"Total players: {len(players)}")
|
|
|
|
return embed
|
|
|
|
def _chunk_list(self, lst: List[str], chunk_size: int) -> List[List[str]]:
|
|
"""Split a list into chunks of specified size."""
|
|
return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)]
|